├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── dist ├── lucky-canvas.cjs.js ├── lucky-canvas.cjs.min.js ├── lucky-canvas.umd.js └── lucky-canvas.umd.min.js ├── examples ├── dev.js ├── dev.js.map ├── ggk │ └── index.html ├── index.html ├── ymc │ ├── img │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ └── button.png │ └── index.html └── yyjk │ ├── img │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── btn.png │ └── button.png │ └── index.html ├── index.js ├── logo.png ├── package.json ├── rollup.config.build.js ├── rollup.config.dev.js ├── src ├── index.ts ├── lib │ ├── card.ts │ ├── grid.ts │ ├── lucky.ts │ └── wheel.ts ├── observer │ ├── array.ts │ ├── dep.ts │ ├── index.ts │ ├── utils.ts │ └── watcher.ts ├── types │ ├── card.ts │ ├── grid.ts │ ├── index.ts │ └── wheel.ts └── utils │ ├── index.ts │ ├── math.ts │ ├── polyfill.js │ └── tween.ts ├── tsconfig.json ├── umd.min.js ├── web.svg └── wx.jpg /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "modules": false 6 | } 7 | ] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | // "plugins": ['prettier'], 9 | "extends": "eslint:recommended", 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly", 13 | "ENV": true 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "rules": { 20 | "linebreak-style": 'off' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | yarn.lock 4 | package-lock.json 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2021] [Li Dong Qi] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ```diff 2 | - 该库已迁移到 https://github.com/LuckDraw/lucky-canvas 3 | ``` 4 | 5 |
6 | logo 7 |

lucky-canvas 抽奖插件

8 |

一个基于 JavaScript 的 ( 大转盘 / 九宫格 ) 抽奖插件

9 |

10 | 11 | stars 12 | 13 | 14 | forks 15 | 16 | 17 | version 18 | 19 | 20 | downloads 21 | 22 | 23 | downloads 24 | 25 |

26 |

27 | 28 | author 29 | 30 | 31 | license 32 | 33 |

34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /dist/lucky-canvas.cjs.min.js: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0}); 2 | /*! ***************************************************************************** 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | ***************************************************************************** */ 16 | var t=function(e,i){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i])})(e,i)};function e(e,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=e}t(e,i),e.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return(i=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0&&r[r.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!r||o[1]>r[0]&&o[1]this.length)&&-1!==this.indexOf(t,e)}),Array.prototype.find||Object.defineProperty(Array.prototype,"find",{value:function(t){if(null==this)throw new TypeError('"this" is null or not defined');var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError("predicate must be a function");for(var n=arguments[1],r=0;r '"+e.src+"' 不能为空或不合法"),"WEB"===n.config.flag){var s=new Image;s.src=t,s.onload=function(){return r(s)},s.onerror=function(){return o("=> '"+e.src+"' 图片加载失败")}}else e[i]=r}))},t.prototype.drawImage=function(t,e,i,n,r){var o,s=this.config,a=this.ctx;return["WEB","MP-WX"].includes(s.flag)?o=t:["UNI-H5","UNI-MP","TARO-H5","TARO-MP"].includes(s.flag)&&(o=t.path),a.drawImage(o,e,i,n,r)},t.prototype.getLength=function(t){return s(t,"number")?t:s(t,"string")?this.changeUnits(t):0},t.prototype.changeUnits=function(t,e){var i=this;return void 0===e&&(e=1),Number(t.replace(/^([-]*[0-9.]*)([a-z%]*)$/,(function(t,n,r){var o={"%":function(t){return t*(e/100)},px:function(t){return 1*t},rem:function(t){return t*i.htmlFontSize}}[r];if(o)return o(n);var s=i.config.unitFunc;return s?s(n,r):n})))},t.prototype.$set=function(t,e,i){t&&"object"==typeof t&&m(t,e,i)},t.prototype.$computed=function(t,e,i){var n=this;Object.defineProperty(t,e,{get:function(){return i.call(n)}})},t.prototype.$watch=function(t,e,i){void 0===i&&(i={}),"object"==typeof e&&(e=(i=e).handler);var n=new b(this,t,e,i);return i.immediate&&e.call(this,n.value),function(){}},t}(),w=function(t){return Math.PI/180*t},x=function(t,e){return[+(Math.cos(t)*e).toFixed(8),+(Math.sin(t)*e).toFixed(8)]},z=function(t,e){var i=-t/e;return[i,-i*t+e]},I=function(t,e,i,n,r,o){var s;if(void 0===o&&(o=!0),Math.abs(r-n).toFixed(8)>=w(180).toFixed(8)){var a=(r+n)/2;return o?(I(t,e,i,n,a,o),I(t,e,i,a,r,o)):(I(t,e,i,a,r,o),I(t,e,i,n,a,o)),!1}o||(n=(s=[r,n])[0],r=s[1]);var u=x(n,i),h=u[0],c=u[1],l=x(r,i),f=l[0],d=l[1],p=z(h,c),g=p[0],m=p[1],v=z(f,d),b=v[0],y=v[1],k=(y-m)/(g-b),S=(b*m-g*y)/(b-g);isNaN(k)&&(Math.abs(h)===+i.toFixed(8)&&(k=h),Math.abs(f)===+i.toFixed(8)&&(k=f)),g===1/0||g===-1/0?S=b*k+y:b!==1/0&&b!==-1/0||(S=g*k+m),e.lineTo(h,c),t.indexOf("MP")>0?e.quadraticCurveTo(k,S,f,d):e.arcTo(k,S,f,d,i)},k=function(t,e,i,n,r,o,s,a){i||(i=s);var u=w(90/Math.PI/n*s),h=w(90/Math.PI/i*s),c=r+u,l=o-u,f=r+h,d=o-h;e.beginPath(),e.fillStyle=a,e.moveTo.apply(e,x(c,n)),I(t,e,n,c,l,!0),d>f?I(t,e,i,f,d,!1):e.lineTo.apply(e,x((r+o)/2,s/2/Math.abs(Math.sin((r-o)/2)))),e.closePath(),e.fill()},S=function(t,e,i,n,r,o,s){var a=Math.min(n,r);o>a/2&&(o=a/2),t.beginPath(),t.fillStyle=s,t.moveTo(e+o,i),t.lineTo(e+o,i),t.lineTo(e+n-o,i),t.quadraticCurveTo(e+n,i,e+n,i+o),t.lineTo(e+n,i+r-o),t.quadraticCurveTo(e+n,i+r,e+n-o,i+r),t.lineTo(e+o,i+r),t.quadraticCurveTo(e,i+r,e,i+r-o),t.lineTo(e,i+o),t.quadraticCurveTo(e,i,e+o,i),t.closePath(),t.fill()},T={easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),-i*(t/=n)*(t-2)+e}},C={easeIn:function(t,e,i,n){return t>=n&&(t=n),-i*Math.cos(t/n*(Math.PI/2))+i+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*Math.sin(t/n*(Math.PI/2))+e}},W={easeIn:function(t,e,i,n){return t>=n&&(t=n),0==t?e:i*Math.pow(2,10*(t/n-1))+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),t==n?e+i:i*(1-Math.pow(2,-10*t/n))+e}},O={easeIn:function(t,e,i,n){return t>=n&&(t=n),-i*(Math.sqrt(1-(t/=n)*t)-1)+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*Math.sqrt(1-(t=t/n-1)*t)+e}},_=Object.freeze({__proto__:null,quad:T,cubic:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*((t=t/n-1)*t*t+1)+e}},quart:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),-i*((t=t/n-1)*t*t*t-1)+e}},quint:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*((t=t/n-1)*t*t*t*t+1)+e}},sine:C,expo:W,circ:O}),E=function(t){function o(e,i){var n;void 0===i&&(i={});var r=t.call(this,e)||this;return r.blocks=[],r.prizes=[],r.buttons=[],r.defaultConfig={},r._defaultConfig={gutter:"0px",offsetDegree:0,speed:20,speedFunction:"quad",accelerationTime:2500,decelerationTime:2500,stopRange:.8},r.defaultStyle={},r._defaultStyle={fontSize:"18px",fontColor:"#000",fontStyle:"sans-serif",fontWeight:"400",lineHeight:"",background:"rgba(0,0,0,0)",wordWrap:!0,lengthLimit:"90%"},r.Radius=0,r.prizeRadius=0,r.prizeDeg=0,r.prizeRadian=0,r.rotateDeg=0,r.maxBtnRadius=0,r.startTime=0,r.endTime=0,r.stopDeg=0,r.endDeg=0,r.FPS=16.6,r.blockImgs=[[]],r.prizeImgs=[[]],r.btnImgs=[[]],e.ob&&(r.initData(i),r.initWatch()),r.initComputed(),null===(n=e.beforeCreate)||void 0===n||n.call(r),r.init({blockImgs:r.blocks.map((function(t){return t.imgs})),prizeImgs:r.prizes.map((function(t){return t.imgs})),btnImgs:r.buttons.map((function(t){return t.imgs}))}),r}return e(o,t),o.prototype.initData=function(t){this.$set(this,"blocks",t.blocks||[]),this.$set(this,"prizes",t.prizes||[]),this.$set(this,"buttons",t.buttons||[]),this.$set(this,"defaultConfig",t.defaultConfig||{}),this.$set(this,"defaultStyle",t.defaultStyle||{}),this.$set(this,"startCallback",t.start),this.$set(this,"endCallback",t.end)},o.prototype.initComputed=function(){var t=this;this.$computed(this,"_defaultConfig",(function(){return i({gutter:"0px",offsetDegree:0,speed:20,speedFunction:"quad",accelerationTime:2500,decelerationTime:2500,stopRange:.8},t.defaultConfig)})),this.$computed(this,"_defaultStyle",(function(){return i({fontSize:"18px",fontColor:"#000",fontStyle:"sans-serif",fontWeight:"400",background:"rgba(0,0,0,0)",wordWrap:!0,lengthLimit:"90%"},t.defaultStyle)}))},o.prototype.initWatch=function(){var t=this;this.$watch("blocks",(function(e){return t.init({blockImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("prizes",(function(e){return t.init({prizeImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("buttons",(function(e){return t.init({btnImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("defaultConfig",(function(){return t.draw()}),{deep:!0}),this.$watch("defaultStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("startCallback",(function(){return t.init({})})),this.$watch("endCallback",(function(){return t.init({})}))},o.prototype.init=function(e){var i,n,r=this;t.prototype.init.call(this);var o=this.config,s=this.ctx;this.Radius=Math.min(this.boxWidth,this.boxHeight)/2,null===(i=o.beforeInit)||void 0===i||i.call(this),s.translate(this.Radius,this.Radius),this.draw(),this.draw(),Object.keys(e).forEach((function(t){var i=t,n={blockImgs:"blocks",prizeImgs:"prizes",btnImgs:"buttons"}[i],o=e[i];o&&o.forEach((function(t,e){t&&t.forEach((function(t,o){r.loadAndCacheImg(n,e,i,o,(function(){r.draw()}))}))}))})),null===(n=o.afterInit)||void 0===n||n.call(this)},o.prototype.handleClick=function(t){var e,i=this.ctx;i.beginPath(),i.arc(0,0,this.maxBtnRadius,0,2*Math.PI,!1),i.isPointInPath(t.offsetX,t.offsetY)&&(this.startTime||null===(e=this.startCallback)||void 0===e||e.call(this,t))},o.prototype.loadAndCacheImg=function(t,e,i,o,s){return n(this,void 0,void 0,(function(){var n,a,u=this;return r(this,(function(r){return(n=this[t][e])&&n.imgs&&(a=n.imgs[o])?(this[i][e]||(this[i][e]=[]),this.loadImg(a.src,a).then((function(t){u[i][e][o]=t,s.call(u)})).catch((function(i){console.error(t+"["+e+"].imgs["+o+"] "+i)})),[2]):[2]}))}))},o.prototype.computedWidthAndHeight=function(t,e,i,n){if(!e.width&&!e.height)return[t.width,t.height];if(e.width&&!e.height){var r=this.getWidth(e.width,i);return[r,t.height*(r/t.width)]}if(!e.width&&e.height){var o=this.getHeight(e.height,n);return[t.width*(o/t.height),o]}return[this.getWidth(e.width,i),this.getHeight(e.height,n)]},o.prototype.draw=function(){var t,e,i=this,n=this,r=n.config,o=n.ctx,s=n._defaultConfig,h=n._defaultStyle;null===(t=r.beforeDraw)||void 0===t||t.call(this,o),o.clearRect(-this.Radius,-this.Radius,2*this.Radius,2*this.Radius),this.prizeRadius=this.blocks.reduce((function(t,e,n){return u(e.background)&&(o.beginPath(),o.fillStyle=e.background,o.arc(0,0,t,0,2*Math.PI,!1),o.fill()),e.imgs&&e.imgs.forEach((function(e,r){if(i.blockImgs[n]){var s=i.blockImgs[n][r];if(s){var a=i.computedWidthAndHeight(s,e,2*t,2*t),u=a[0],h=a[1],c=[i.getOffsetX(u),i.getHeight(e.top,2*t)-t],l=c[0],f=c[1];o.save(),e.rotate&&o.rotate(w(i.rotateDeg)),i.drawImage(s,l,f,u,h),o.restore()}}})),t-i.getLength(e.padding&&e.padding.split(" ")[0])}),this.Radius),this.prizeDeg=360/this.prizes.length,this.prizeRadian=w(this.prizeDeg);var c=w(-90+this.rotateDeg+s.offsetDegree),l=function(t){return i.getOffsetX(o.measureText(t).width)},f=function(t,e,n){var r=t.lineHeight||h.lineHeight||t.fontSize||h.fontSize;return i.getHeight(t.top,e)+(n+1)*i.getLength(r)};o.save(),this.prizes.forEach((function(t,e){var n=c+e*i.prizeRadian,d=i.prizeRadius-i.maxBtnRadius,p=t.background||h.background;u(p)&&function(t,e,i,n,r,o,s,a){s?k(t,e,i,n,r,o,s,a):(e.beginPath(),e.fillStyle=a,e.moveTo(0,0),e.arc(0,0,n,r,o,!1),e.closePath(),e.fill())}(r.flag,o,i.maxBtnRadius,i.prizeRadius,n-i.prizeRadian/2,n+i.prizeRadian/2,i.getLength(s.gutter),p);var g=Math.cos(n)*i.prizeRadius,m=Math.sin(n)*i.prizeRadius;o.translate(g,m),o.rotate(n+w(90)),t.imgs&&t.imgs.forEach((function(t,n){if(i.prizeImgs[e]){var r=i.prizeImgs[e][n];if(r){var o=i.computedWidthAndHeight(r,t,i.prizeRadian*i.prizeRadius,d),s=o[0],a=o[1],u=[i.getOffsetX(s),i.getHeight(t.top,d)],h=u[0],c=u[1];i.drawImage(r,h,c,s,a)}}})),t.fonts&&t.fonts.forEach((function(t){var e=t.fontColor||h.fontColor,n=t.fontWeight||h.fontWeight,r=i.getLength(t.fontSize||h.fontSize),u=t.fontStyle||h.fontStyle;o.fillStyle=e,o.font=n+" "+(r>>0)+"px "+u;var c=[],p=String(t.text);if(Object.prototype.hasOwnProperty.call(t,"wordWrap")?t.wordWrap:h.wordWrap){p=a(p);for(var g="",m=0;mi.getWidth(t.lengthLimit||h.lengthLimit,b)&&(c.push(g.slice(0,-1)),g=p[m])}g&&c.push(g),c.length||c.push(p)}else c=p.split("\n");c.filter((function(t){return!!t})).forEach((function(e,i){o.fillText(e,l(e),f(t,d,i))}))})),o.rotate(w(360)-n-w(90)),o.translate(-g,-m)})),o.restore(),this.buttons.forEach((function(t,e){var n=i.getHeight(t.radius);i.maxBtnRadius=Math.max(i.maxBtnRadius,n),u(t.background)&&(o.beginPath(),o.fillStyle=t.background,o.arc(0,0,n,0,2*Math.PI,!1),o.fill()),t.pointer&&u(t.background)&&(o.beginPath(),o.fillStyle=t.background,o.moveTo(-n,0),o.lineTo(n,0),o.lineTo(0,2*-n),o.closePath(),o.fill()),t.imgs&&t.imgs.forEach((function(t,r){if(i.btnImgs[e]){var o=i.btnImgs[e][r];if(o){var s=i.computedWidthAndHeight(o,t,2*n,2*n),a=s[0],u=s[1],h=[i.getOffsetX(a),i.getHeight(t.top,n)],c=h[0],l=h[1];i.drawImage(o,c,l,a,u)}}})),t.fonts&&t.fonts.forEach((function(t){var e=t.fontColor||h.fontColor,r=t.fontWeight||h.fontWeight,s=i.getLength(t.fontSize||h.fontSize),a=t.fontStyle||h.fontStyle;o.fillStyle=e,o.font=r+" "+(s>>0)+"px "+a,String(t.text).split("\n").forEach((function(e,i){o.fillText(e,l(e),f(t,n,i))}))}))})),null===(e=r.afterDraw)||void 0===e||e.call(this,o)},o.prototype.play=function(){this.startTime||(this.startTime=Date.now(),this.prizeFlag=void 0,this.run())},o.prototype.stop=function(t){this.prizeFlag=t<0?-1:t%this.prizes.length,-1===this.prizeFlag&&(this.rotateDeg=this.prizeDeg/2-this._defaultConfig.offsetDegree,this.draw())},o.prototype.run=function(t){void 0===t&&(t=0);var e=this,i=e.rAF,n=e.prizeFlag,r=e.prizeDeg,o=e.rotateDeg,s=e._defaultConfig;if(-1!==n){var a=Date.now()-this.startTime;if(a>=s.accelerationTime&&void 0!==n){this.FPS=a/t,this.endTime=Date.now(),this.stopDeg=o;for(var u=(Math.random()*r-r/2)*this.getLength(s.stopRange),h=0;++h;){var c=360*h-n*r-o-s.offsetDegree+u;if(_[s.speedFunction].easeOut(this.FPS,this.stopDeg,c,s.decelerationTime)-this.stopDeg>s.speed){this.endDeg=c;break}}return this.slowDown()}this.rotateDeg=(o+_[s.speedFunction].easeIn(a,0,s.speed,s.accelerationTime))%360,this.draw(),i(this.run.bind(this,t+1))}else this.startTime=0},o.prototype.slowDown=function(){var t,e=this,n=e.rAF,r=e.prizes,o=e.prizeFlag,s=e.stopDeg,a=e.endDeg,u=e._defaultConfig,h=Date.now()-this.endTime;if(-1!==o){if(h>=u.decelerationTime)return this.startTime=0,void(null===(t=this.endCallback)||void 0===t||t.call(this,i({},r.find((function(t,e){return e===o})))));this.rotateDeg=_[u.speedFunction].easeOut(h,s,a,u.decelerationTime)%360,this.draw(),n(this.slowDown.bind(this))}else this.startTime=0},o.prototype.getWidth=function(t,e){return void 0===e&&(e=this.prizeRadian*this.prizeRadius),s(t,"number")?t:s(t,"string")?this.changeUnits(t,e):0},o.prototype.getHeight=function(t,e){return void 0===e&&(e=this.prizeRadius),s(t,"number")?t:s(t,"string")?this.changeUnits(t,e):0},o.prototype.getOffsetX=function(t){return-t/2},o.prototype.conversionAxis=function(t,e){var i=this.config;return[t/i.dpr-this.Radius,e/i.dpr-this.Radius]},o}(y),P=function(t){function h(e,i){var n;void 0===i&&(i={});var r=t.call(this,e)||this;r.rows=3,r.cols=3,r.blocks=[],r.prizes=[],r.buttons=[],r.defaultConfig={},r._defaultConfig={gutter:5,speed:20,accelerationTime:2500,decelerationTime:2500},r.defaultStyle={},r._defaultStyle={borderRadius:20,fontColor:"#000",fontSize:"18px",fontStyle:"sans-serif",fontWeight:"400",lineHeight:"",background:"rgba(0,0,0,0)",shadow:"",wordWrap:!0,lengthLimit:"90%"},r.activeStyle={},r._activeStyle={background:"#ffce98",shadow:"",fontStyle:"",fontWeight:"",fontSize:"",lineHeight:"",fontColor:""},r.cellWidth=0,r.cellHeight=0,r.startTime=0,r.endTime=0,r.currIndex=0,r.stopIndex=0,r.endIndex=0,r.demo=!1,r.timer=0,r.FPS=16.6,r.prizeFlag=-1,r.cells=[],r.blockImgs=[[]],r.btnImgs=[[]],r.prizeImgs=[],e.ob&&(r.initData(i),r.initWatch()),r.initComputed(),null===(n=e.beforeCreate)||void 0===n||n.call(r);var o=r.buttons.map((function(t){return t.imgs}));return r.button&&o.push(r.button.imgs),r.init({blockImgs:r.blocks.map((function(t){return t.imgs})),prizeImgs:r.prizes.map((function(t){return t.imgs})),btnImgs:o}),r}return e(h,t),h.prototype.initData=function(t){this.$set(this,"rows",Number(t.rows)||3),this.$set(this,"cols",Number(t.cols)||3),this.$set(this,"blocks",t.blocks||[]),this.$set(this,"prizes",t.prizes||[]),this.$set(this,"buttons",t.buttons||[]),this.$set(this,"button",t.button),this.$set(this,"defaultConfig",t.defaultConfig||{}),this.$set(this,"defaultStyle",t.defaultStyle||{}),this.$set(this,"activeStyle",t.activeStyle||{}),this.$set(this,"startCallback",t.start),this.$set(this,"endCallback",t.end)},h.prototype.initComputed=function(){var t=this;this.$computed(this,"_defaultConfig",(function(){var e=i({gutter:5,speed:20,accelerationTime:2500,decelerationTime:2500},t.defaultConfig);return e.gutter=t.getLength(e.gutter),e.speed=e.speed/40,e})),this.$computed(this,"_defaultStyle",(function(){return i({borderRadius:20,fontColor:"#000",fontSize:"18px",fontStyle:"sans-serif",fontWeight:"400",background:"rgba(0,0,0,0)",shadow:"",wordWrap:!0,lengthLimit:"90%"},t.defaultStyle)})),this.$computed(this,"_activeStyle",(function(){return i({background:"#ffce98",shadow:""},t.activeStyle)}))},h.prototype.initWatch=function(){var t=this;this.$watch("blocks",(function(e){return t.init({blockImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("prizes",(function(e){return t.init({prizeImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("buttons",(function(e){var i=e.map((function(t){return t.imgs}));return t.button&&i.push(t.button.imgs),t.init({btnImgs:i})}),{deep:!0}),this.$watch("button",(function(){var e=t.buttons.map((function(t){return t.imgs}));return t.button&&e.push(t.button.imgs),t.init({btnImgs:e})}),{deep:!0}),this.$watch("rows",(function(){return t.init({})})),this.$watch("cols",(function(){return t.init({})})),this.$watch("defaultConfig",(function(){return t.draw()}),{deep:!0}),this.$watch("defaultStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("activeStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("startCallback",(function(){return t.init({})})),this.$watch("endCallback",(function(){return t.init({})}))},h.prototype.init=function(e){var i,n,r=this;t.prototype.init.call(this);var o=this,s=o.config;o.ctx,o.button,null===(i=s.beforeInit)||void 0===i||i.call(this),this.draw(),Object.keys(e).forEach((function(t){var i=t,n=e[i],o={blockImgs:"blocks",prizeImgs:"prizes",btnImgs:"buttons"}[i];n&&n.forEach((function(t,e){t&&t.forEach((function(t,n){r.loadAndCacheImg(o,e,i,n,(function(){r.draw()}))}))}))})),null===(n=s.afterInit)||void 0===n||n.call(this)},h.prototype.handleClick=function(t){var e=this,i=this.ctx;o(o([],this.buttons),[this.button]).forEach((function(n){var r;if(n){var o=e.getGeometricProperty([n.x,n.y,n.col||1,n.row||1]),s=o[0],a=o[1],u=o[2],h=o[3];i.beginPath(),i.rect(s,a,u,h),i.isPointInPath(t.offsetX,t.offsetY)&&(e.startTime||("function"==typeof n.callback&&n.callback.call(e,n),null===(r=e.startCallback)||void 0===r||r.call(e,t,n)))}}))},h.prototype.loadAndCacheImg=function(t,e,i,o,s){return n(this,void 0,void 0,(function(){var n,a,u,h=this;return r(this,(function(r){return n=this[t][e],"buttons"===t&&!this.buttons.length&&this.button&&(n=this.button),n&&n.imgs&&(a=n.imgs[o])?(this[i][e]||(this[i][e]=[]),u=[this.loadImg(a.src,a),a.activeSrc&&this.loadImg(a.activeSrc,a,"$activeResolve")],Promise.all(u).then((function(t){var n=t[0],r=t[1];h[i][e][o]={defaultImg:n,activeImg:r},s.call(h)})).catch((function(i){console.error(t+"["+e+"].imgs["+o+"] "+i)})),[2]):[2]}))}))},h.prototype.computedWidthAndHeight=function(t,e,i){if(!e.width&&!e.height)return[t.width,t.height];if(e.width&&!e.height){var n=this.getWidth(e.width,i.col);return[n,t.height*(n/t.width)]}if(!e.width&&e.height){var r=this.getHeight(e.height,i.row);return[t.width*(r/t.height),r]}return[this.getWidth(e.width,i.col),this.getHeight(e.height,i.row)]},h.prototype.draw=function(){var t,e,i=this,n=this,r=n.config,h=n.ctx,c=n._defaultConfig,l=n._defaultStyle,f=n._activeStyle;null===(t=r.beforeDraw)||void 0===t||t.call(this,h),h.clearRect(0,0,this.boxWidth,this.boxHeight),this.cells=o(o([],this.prizes),this.buttons),this.button&&this.cells.push(this.button),this.cells.forEach((function(t){t.col=t.col||1,t.row=t.row||1})),this.prizeArea=this.blocks.reduce((function(t,e){var n=t.x,r=t.y,o=t.w,a=t.h,c=function(t){var e,i=(null===(e=t.padding)||void 0===e?void 0:e.replace(/px/g,"").split(" ").map((function(t){return~~t})))||[0],n=0,r=0,o=0,a=0;switch(i.length){case 1:n=r=o=a=i[0];break;case 2:n=r=i[0],o=a=i[1];break;case 3:n=i[0],o=a=i[1],r=i[2];break;default:n=i[0],r=i[1],o=i[2],a=i[3]}var u={paddingTop:n,paddingBottom:r,paddingLeft:o,paddingRight:a};for(var h in u)u[h]=Object.prototype.hasOwnProperty.call(t,h)&&s(t[h],"string","number")?~~String(t[h]).replace(/px/g,""):u[h];return[n,r,o,a]}(e),f=c[0],d=c[1],p=c[2],g=c[3],m=e.borderRadius?i.getLength(e.borderRadius):0,v=e.background||l.background;return u(v)&&S(h,n,r,o,a,m,i.handleBackground(n,r,o,a,v)),{x:n+p,y:r+f,w:o-p-g,h:a-f-d}}),{x:0,y:0,w:this.boxWidth,h:this.boxHeight}),this.cellWidth=(this.prizeArea.w-c.gutter*(this.cols-1))/this.cols,this.cellHeight=(this.prizeArea.h-c.gutter*(this.rows-1))/this.rows,this.cells.forEach((function(t,e){var n=i.getGeometricProperty([t.x,t.y,t.col,t.row]),o=n[0],s=n[1],c=n[2],d=n[3],p=!1;(void 0===i.prizeFlag||i.prizeFlag>-1)&&(p=e===i.currIndex%i.prizes.length>>0);var g=p?f.background:t.background||l.background;if(u(g)){var m=(p?f.shadow:t.shadow||l.shadow).replace(/px/g,"").split(",")[0].split(" ").map((function(t,e){return e<3?Number(t):t}));4===m.length&&(h.shadowColor=m[3],h.shadowOffsetX=m[0]*r.dpr,h.shadowOffsetY=m[1]*r.dpr,h.shadowBlur=m[2],m[0]>0?c-=m[0]:(c+=m[0],o-=m[0]),m[1]>0?d-=m[1]:(d+=m[1],s-=m[1])),S(h,o,s,c,d,i.getLength(t.borderRadius?t.borderRadius:l.borderRadius),i.handleBackground(o,s,c,d,g)),h.shadowColor="rgba(0, 0, 0, 0)",h.shadowOffsetX=0,h.shadowOffsetY=0,h.shadowBlur=0}var v="prizeImgs";e>=i.prizes.length&&(v="btnImgs",e-=i.prizes.length),t.imgs&&t.imgs.forEach((function(n,r){if(i[v][e]){var a=i[v][e][r];if(a){var u=p&&a.activeImg||a.defaultImg;if(u){var h=i.computedWidthAndHeight(u,n,t),c=h[0],l=h[1],f=[o+i.getOffsetX(c,t.col),s+i.getHeight(n.top,t.row)],d=f[0],g=f[1];i.drawImage(u,d,g,c,l)}}}})),t.fonts&&t.fonts.forEach((function(e){var n=p&&f.fontStyle?f.fontStyle:e.fontStyle||l.fontStyle,r=p&&f.fontWeight?f.fontWeight:e.fontWeight||l.fontWeight,u=p&&f.fontSize?i.getLength(f.fontSize):i.getLength(e.fontSize||l.fontSize),c=p&&f.lineHeight?f.lineHeight:e.lineHeight||l.lineHeight||e.fontSize||l.fontSize;h.font=r+" "+(u>>0)+"px "+n,h.fillStyle=p&&f.fontColor?f.fontColor:e.fontColor||l.fontColor;var d=[],g=String(e.text);if(Object.prototype.hasOwnProperty.call(e,"wordWrap")?e.wordWrap:l.wordWrap){g=a(g);for(var m="",v=0;vi.getWidth(e.lengthLimit||l.lengthLimit,t.col)&&(d.push(m.slice(0,-1)),m=g[v])}m&&d.push(m),d.length||d.push(g)}else d=g.split("\n");d.forEach((function(n,r){h.fillText(n,o+i.getOffsetX(h.measureText(n).width,t.col),s+i.getHeight(e.top,t.row)+(r+1)*i.getLength(c))}))}))})),null===(e=r.afterDraw)||void 0===e||e.call(this,h)},h.prototype.handleBackground=function(t,e,i,n,r){var o=this.ctx;return r.includes("linear-gradient")&&(r=function(t,e,i,n,r,o){var s=/linear-gradient\((.+)\)/.exec(o)[1].split(",").map((function(t){return t.trim()})),a=s.shift(),u=[0,0,0,0];if(a.includes("deg")){var h=function(t){return Math.tan(t/180*Math.PI)};(a=a.slice(0,-3)%360)>=0&&a<45?u=[e,i+r,e+n,i+r-n*h(a-0)]:a>=45&&a<90?u=[e,i+r,e+n-r*h(a-45),i]:a>=90&&a<135?u=[e+n,i+r,e+n-r*h(a-90),i]:a>=135&&a<180?u=[e+n,i+r,e,i+n*h(a-135)]:a>=180&&a<225?u=[e+n,i,e,i+n*h(a-180)]:a>=225&&a<270?u=[e+n,i,e+r*h(a-225),i+r]:a>=270&&a<315?u=[e,i,e+r*h(a-270),i+r]:a>=315&&a<360&&(u=[e,i,e+n,i+r-n*h(a-315)])}else a.includes("top")?u=[e,i+r,e,i]:a.includes("bottom")?u=[e,i,e,i+r]:a.includes("left")?u=[e+n,i,e,i]:a.includes("right")&&(u=[e,i,e+n,i]);var c=t.createLinearGradient.apply(t,u.map((function(t){return t>>0})));return s.reduce((function(t,e,i){var n=e.split(" ");return 1===n.length?t.addColorStop(i,n[0]):2===n.length&&t.addColorStop.apply(t,n),t}),c)}(o,t,e,i,n,r)),r},h.prototype.play=function(){var t=this.config.clearInterval;this.startTime||(t(this.timer),this.startTime=Date.now(),this.prizeFlag=void 0,this.run())},h.prototype.stop=function(t){this.prizeFlag=t<0?-1:t%this.prizes.length,-1===this.prizeFlag&&(this.currIndex=0,this.draw())},h.prototype.run=function(t){void 0===t&&(t=0);var e=this,i=e.rAF,n=e.currIndex,r=e.prizes,o=e.prizeFlag,s=e.startTime,a=e._defaultConfig,u=Date.now()-s;if(u>=a.accelerationTime&&void 0!==o){this.FPS=u/t,this.endTime=Date.now(),this.stopIndex=n;for(var h=0;++h;){var c=r.length*h+o-(n>>0);if(T.easeOut(this.FPS,this.stopIndex,c,a.decelerationTime)-this.stopIndex>a.speed){this.endIndex=c;break}}return this.slowDown()}this.currIndex=(n+T.easeIn(u,.1,a.speed,a.accelerationTime))%r.length,this.draw(),i(this.run.bind(this,t+1))},h.prototype.slowDown=function(){var t,e=this,n=e.rAF,r=e.prizes,o=e.prizeFlag,s=e.stopIndex,a=e.endIndex,u=e._defaultConfig,h=Date.now()-this.endTime;if(-1!==o){if(h>u.decelerationTime)return this.startTime=0,void(null===(t=this.endCallback)||void 0===t||t.call(this,i({},r.find((function(t,e){return e===o})))));this.currIndex=T.easeOut(h,s,a,u.decelerationTime)%r.length,this.draw(),n(this.slowDown.bind(this))}else this.startTime=0},h.prototype.walk=function(){var t=this,e=this.config,i=e.setInterval;(0,e.clearInterval)(this.timer),this.timer=i((function(){t.currIndex+=1,t.draw()}),1300)},h.prototype.getGeometricProperty=function(t){var e=t[0],i=t[1],n=t[2],r=t[3],o=this.cellWidth,s=this.cellHeight,a=this._defaultConfig.gutter,u=[this.prizeArea.x+(o+a)*e,this.prizeArea.y+(s+a)*i];return n&&r&&u.push(o*n+a*(n-1),s*r+a*(r-1)),u},h.prototype.getWidth=function(t,e){return void 0===e&&(e=1),s(t,"number")?t:s(t,"string")?this.changeUnits(t,this.cellWidth*e+this._defaultConfig.gutter*(e-1)):0},h.prototype.getHeight=function(t,e){return void 0===e&&(e=1),s(t,"number")?t:s(t,"string")?this.changeUnits(t,this.cellHeight*e+this._defaultConfig.gutter*(e-1)):0},h.prototype.getOffsetX=function(t,e){return void 0===e&&(e=1),(this.cellWidth*e+this._defaultConfig.gutter*(e-1)-t)/2},h.prototype.conversionAxis=function(t,e){var i=this.config;return[t/i.dpr,e/i.dpr]},h}(y);exports.LuckyGrid=P,exports.LuckyWheel=E; 17 | -------------------------------------------------------------------------------- /dist/lucky-canvas.umd.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).LuckyCanvas={})}(this,(function(t){"use strict"; 2 | /*! ***************************************************************************** 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | ***************************************************************************** */var e=function(t,i){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i])})(t,i)};function i(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var n=function(){return(n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0&&r[r.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!r||o[1]>r[0]&&o[1]this.length)&&-1!==this.indexOf(t,e)}),Array.prototype.find||Object.defineProperty(Array.prototype,"find",{value:function(t){if(null==this)throw new TypeError('"this" is null or not defined');var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError("predicate must be a function");for(var n=arguments[1],r=0;r '"+e.src+"' 不能为空或不合法"),"WEB"===n.config.flag){var s=new Image;s.src=t,s.onload=function(){return r(s)},s.onerror=function(){return o("=> '"+e.src+"' 图片加载失败")}}else e[i]=r}))},t.prototype.drawImage=function(t,e,i,n,r){var o,s=this.config,a=this.ctx;return["WEB","MP-WX"].includes(s.flag)?o=t:["UNI-H5","UNI-MP","TARO-H5","TARO-MP"].includes(s.flag)&&(o=t.path),a.drawImage(o,e,i,n,r)},t.prototype.getLength=function(t){return a(t,"number")?t:a(t,"string")?this.changeUnits(t):0},t.prototype.changeUnits=function(t,e){var i=this;return void 0===e&&(e=1),Number(t.replace(/^([-]*[0-9.]*)([a-z%]*)$/,(function(t,n,r){var o={"%":function(t){return t*(e/100)},px:function(t){return 1*t},rem:function(t){return t*i.htmlFontSize}}[r];if(o)return o(n);var s=i.config.unitFunc;return s?s(n,r):n})))},t.prototype.$set=function(t,e,i){t&&"object"==typeof t&&v(t,e,i)},t.prototype.$computed=function(t,e,i){var n=this;Object.defineProperty(t,e,{get:function(){return i.call(n)}})},t.prototype.$watch=function(t,e,i){void 0===i&&(i={}),"object"==typeof e&&(e=(i=e).handler);var n=new y(this,t,e,i);return i.immediate&&e.call(this,n.value),function(){}},t}(),x=function(t){return Math.PI/180*t},z=function(t,e){return[+(Math.cos(t)*e).toFixed(8),+(Math.sin(t)*e).toFixed(8)]},I=function(t,e){var i=-t/e;return[i,-i*t+e]},k=function(t,e,i,n,r,o){var s;if(void 0===o&&(o=!0),Math.abs(r-n).toFixed(8)>=x(180).toFixed(8)){var a=(r+n)/2;return o?(k(t,e,i,n,a,o),k(t,e,i,a,r,o)):(k(t,e,i,a,r,o),k(t,e,i,n,a,o)),!1}o||(n=(s=[r,n])[0],r=s[1]);var u=z(n,i),h=u[0],c=u[1],l=z(r,i),f=l[0],d=l[1],p=I(h,c),g=p[0],m=p[1],v=I(f,d),b=v[0],y=v[1],w=(y-m)/(g-b),S=(b*m-g*y)/(b-g);isNaN(w)&&(Math.abs(h)===+i.toFixed(8)&&(w=h),Math.abs(f)===+i.toFixed(8)&&(w=f)),g===1/0||g===-1/0?S=b*w+y:b!==1/0&&b!==-1/0||(S=g*w+m),e.lineTo(h,c),t.indexOf("MP")>0?e.quadraticCurveTo(w,S,f,d):e.arcTo(w,S,f,d,i)},S=function(t,e,i,n,r,o,s,a){i||(i=s);var u=x(90/Math.PI/n*s),h=x(90/Math.PI/i*s),c=r+u,l=o-u,f=r+h,d=o-h;e.beginPath(),e.fillStyle=a,e.moveTo.apply(e,z(c,n)),k(t,e,n,c,l,!0),d>f?k(t,e,i,f,d,!1):e.lineTo.apply(e,z((r+o)/2,s/2/Math.abs(Math.sin((r-o)/2)))),e.closePath(),e.fill()},T=function(t,e,i,n,r,o,s){var a=Math.min(n,r);o>a/2&&(o=a/2),t.beginPath(),t.fillStyle=s,t.moveTo(e+o,i),t.lineTo(e+o,i),t.lineTo(e+n-o,i),t.quadraticCurveTo(e+n,i,e+n,i+o),t.lineTo(e+n,i+r-o),t.quadraticCurveTo(e+n,i+r,e+n-o,i+r),t.lineTo(e+o,i+r),t.quadraticCurveTo(e,i+r,e,i+r-o),t.lineTo(e,i+o),t.quadraticCurveTo(e,i,e+o,i),t.closePath(),t.fill()},C={easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),-i*(t/=n)*(t-2)+e}},W={easeIn:function(t,e,i,n){return t>=n&&(t=n),-i*Math.cos(t/n*(Math.PI/2))+i+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*Math.sin(t/n*(Math.PI/2))+e}},O={easeIn:function(t,e,i,n){return t>=n&&(t=n),0==t?e:i*Math.pow(2,10*(t/n-1))+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),t==n?e+i:i*(1-Math.pow(2,-10*t/n))+e}},_={easeIn:function(t,e,i,n){return t>=n&&(t=n),-i*(Math.sqrt(1-(t/=n)*t)-1)+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*Math.sqrt(1-(t=t/n-1)*t)+e}},E=Object.freeze({__proto__:null,quad:C,cubic:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*((t=t/n-1)*t*t+1)+e}},quart:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),-i*((t=t/n-1)*t*t*t-1)+e}},quint:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*((t=t/n-1)*t*t*t*t+1)+e}},sine:W,expo:O,circ:_}),P=function(t){function e(e,i){var n;void 0===i&&(i={});var r=t.call(this,e)||this;return r.blocks=[],r.prizes=[],r.buttons=[],r.defaultConfig={},r._defaultConfig={gutter:"0px",offsetDegree:0,speed:20,speedFunction:"quad",accelerationTime:2500,decelerationTime:2500,stopRange:.8},r.defaultStyle={},r._defaultStyle={fontSize:"18px",fontColor:"#000",fontStyle:"sans-serif",fontWeight:"400",lineHeight:"",background:"rgba(0,0,0,0)",wordWrap:!0,lengthLimit:"90%"},r.Radius=0,r.prizeRadius=0,r.prizeDeg=0,r.prizeRadian=0,r.rotateDeg=0,r.maxBtnRadius=0,r.startTime=0,r.endTime=0,r.stopDeg=0,r.endDeg=0,r.FPS=16.6,r.blockImgs=[[]],r.prizeImgs=[[]],r.btnImgs=[[]],e.ob&&(r.initData(i),r.initWatch()),r.initComputed(),null===(n=e.beforeCreate)||void 0===n||n.call(r),r.init({blockImgs:r.blocks.map((function(t){return t.imgs})),prizeImgs:r.prizes.map((function(t){return t.imgs})),btnImgs:r.buttons.map((function(t){return t.imgs}))}),r}return i(e,t),e.prototype.initData=function(t){this.$set(this,"blocks",t.blocks||[]),this.$set(this,"prizes",t.prizes||[]),this.$set(this,"buttons",t.buttons||[]),this.$set(this,"defaultConfig",t.defaultConfig||{}),this.$set(this,"defaultStyle",t.defaultStyle||{}),this.$set(this,"startCallback",t.start),this.$set(this,"endCallback",t.end)},e.prototype.initComputed=function(){var t=this;this.$computed(this,"_defaultConfig",(function(){return n({gutter:"0px",offsetDegree:0,speed:20,speedFunction:"quad",accelerationTime:2500,decelerationTime:2500,stopRange:.8},t.defaultConfig)})),this.$computed(this,"_defaultStyle",(function(){return n({fontSize:"18px",fontColor:"#000",fontStyle:"sans-serif",fontWeight:"400",background:"rgba(0,0,0,0)",wordWrap:!0,lengthLimit:"90%"},t.defaultStyle)}))},e.prototype.initWatch=function(){var t=this;this.$watch("blocks",(function(e){return t.init({blockImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("prizes",(function(e){return t.init({prizeImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("buttons",(function(e){return t.init({btnImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("defaultConfig",(function(){return t.draw()}),{deep:!0}),this.$watch("defaultStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("startCallback",(function(){return t.init({})})),this.$watch("endCallback",(function(){return t.init({})}))},e.prototype.init=function(e){var i,n,r=this;t.prototype.init.call(this);var o=this.config,s=this.ctx;this.Radius=Math.min(this.boxWidth,this.boxHeight)/2,null===(i=o.beforeInit)||void 0===i||i.call(this),s.translate(this.Radius,this.Radius),this.draw(),this.draw(),Object.keys(e).forEach((function(t){var i=t,n={blockImgs:"blocks",prizeImgs:"prizes",btnImgs:"buttons"}[i],o=e[i];o&&o.forEach((function(t,e){t&&t.forEach((function(t,o){r.loadAndCacheImg(n,e,i,o,(function(){r.draw()}))}))}))})),null===(n=o.afterInit)||void 0===n||n.call(this)},e.prototype.handleClick=function(t){var e,i=this.ctx;i.beginPath(),i.arc(0,0,this.maxBtnRadius,0,2*Math.PI,!1),i.isPointInPath(t.offsetX,t.offsetY)&&(this.startTime||null===(e=this.startCallback)||void 0===e||e.call(this,t))},e.prototype.loadAndCacheImg=function(t,e,i,n,s){return r(this,void 0,void 0,(function(){var r,a,u=this;return o(this,(function(o){return(r=this[t][e])&&r.imgs&&(a=r.imgs[n])?(this[i][e]||(this[i][e]=[]),this.loadImg(a.src,a).then((function(t){u[i][e][n]=t,s.call(u)})).catch((function(i){console.error(t+"["+e+"].imgs["+n+"] "+i)})),[2]):[2]}))}))},e.prototype.computedWidthAndHeight=function(t,e,i,n){if(!e.width&&!e.height)return[t.width,t.height];if(e.width&&!e.height){var r=this.getWidth(e.width,i);return[r,t.height*(r/t.width)]}if(!e.width&&e.height){var o=this.getHeight(e.height,n);return[t.width*(o/t.height),o]}return[this.getWidth(e.width,i),this.getHeight(e.height,n)]},e.prototype.draw=function(){var t,e,i=this,n=this,r=n.config,o=n.ctx,s=n._defaultConfig,a=n._defaultStyle;null===(t=r.beforeDraw)||void 0===t||t.call(this,o),o.clearRect(-this.Radius,-this.Radius,2*this.Radius,2*this.Radius),this.prizeRadius=this.blocks.reduce((function(t,e,n){return h(e.background)&&(o.beginPath(),o.fillStyle=e.background,o.arc(0,0,t,0,2*Math.PI,!1),o.fill()),e.imgs&&e.imgs.forEach((function(e,r){if(i.blockImgs[n]){var s=i.blockImgs[n][r];if(s){var a=i.computedWidthAndHeight(s,e,2*t,2*t),u=a[0],h=a[1],c=[i.getOffsetX(u),i.getHeight(e.top,2*t)-t],l=c[0],f=c[1];o.save(),e.rotate&&o.rotate(x(i.rotateDeg)),i.drawImage(s,l,f,u,h),o.restore()}}})),t-i.getLength(e.padding&&e.padding.split(" ")[0])}),this.Radius),this.prizeDeg=360/this.prizes.length,this.prizeRadian=x(this.prizeDeg);var c=x(-90+this.rotateDeg+s.offsetDegree),l=function(t){return i.getOffsetX(o.measureText(t).width)},f=function(t,e,n){var r=t.lineHeight||a.lineHeight||t.fontSize||a.fontSize;return i.getHeight(t.top,e)+(n+1)*i.getLength(r)};o.save(),this.prizes.forEach((function(t,e){var n=c+e*i.prizeRadian,d=i.prizeRadius-i.maxBtnRadius,p=t.background||a.background;h(p)&&function(t,e,i,n,r,o,s,a){s?S(t,e,i,n,r,o,s,a):(e.beginPath(),e.fillStyle=a,e.moveTo(0,0),e.arc(0,0,n,r,o,!1),e.closePath(),e.fill())}(r.flag,o,i.maxBtnRadius,i.prizeRadius,n-i.prizeRadian/2,n+i.prizeRadian/2,i.getLength(s.gutter),p);var g=Math.cos(n)*i.prizeRadius,m=Math.sin(n)*i.prizeRadius;o.translate(g,m),o.rotate(n+x(90)),t.imgs&&t.imgs.forEach((function(t,n){if(i.prizeImgs[e]){var r=i.prizeImgs[e][n];if(r){var o=i.computedWidthAndHeight(r,t,i.prizeRadian*i.prizeRadius,d),s=o[0],a=o[1],u=[i.getOffsetX(s),i.getHeight(t.top,d)],h=u[0],c=u[1];i.drawImage(r,h,c,s,a)}}})),t.fonts&&t.fonts.forEach((function(t){var e=t.fontColor||a.fontColor,n=t.fontWeight||a.fontWeight,r=i.getLength(t.fontSize||a.fontSize),h=t.fontStyle||a.fontStyle;o.fillStyle=e,o.font=n+" "+(r>>0)+"px "+h;var c=[],p=String(t.text);if(Object.prototype.hasOwnProperty.call(t,"wordWrap")?t.wordWrap:a.wordWrap){p=u(p);for(var g="",m=0;mi.getWidth(t.lengthLimit||a.lengthLimit,b)&&(c.push(g.slice(0,-1)),g=p[m])}g&&c.push(g),c.length||c.push(p)}else c=p.split("\n");c.filter((function(t){return!!t})).forEach((function(e,i){o.fillText(e,l(e),f(t,d,i))}))})),o.rotate(x(360)-n-x(90)),o.translate(-g,-m)})),o.restore(),this.buttons.forEach((function(t,e){var n=i.getHeight(t.radius);i.maxBtnRadius=Math.max(i.maxBtnRadius,n),h(t.background)&&(o.beginPath(),o.fillStyle=t.background,o.arc(0,0,n,0,2*Math.PI,!1),o.fill()),t.pointer&&h(t.background)&&(o.beginPath(),o.fillStyle=t.background,o.moveTo(-n,0),o.lineTo(n,0),o.lineTo(0,2*-n),o.closePath(),o.fill()),t.imgs&&t.imgs.forEach((function(t,r){if(i.btnImgs[e]){var o=i.btnImgs[e][r];if(o){var s=i.computedWidthAndHeight(o,t,2*n,2*n),a=s[0],u=s[1],h=[i.getOffsetX(a),i.getHeight(t.top,n)],c=h[0],l=h[1];i.drawImage(o,c,l,a,u)}}})),t.fonts&&t.fonts.forEach((function(t){var e=t.fontColor||a.fontColor,r=t.fontWeight||a.fontWeight,s=i.getLength(t.fontSize||a.fontSize),u=t.fontStyle||a.fontStyle;o.fillStyle=e,o.font=r+" "+(s>>0)+"px "+u,String(t.text).split("\n").forEach((function(e,i){o.fillText(e,l(e),f(t,n,i))}))}))})),null===(e=r.afterDraw)||void 0===e||e.call(this,o)},e.prototype.play=function(){this.startTime||(this.startTime=Date.now(),this.prizeFlag=void 0,this.run())},e.prototype.stop=function(t){this.prizeFlag=t<0?-1:t%this.prizes.length,-1===this.prizeFlag&&(this.rotateDeg=this.prizeDeg/2-this._defaultConfig.offsetDegree,this.draw())},e.prototype.run=function(t){void 0===t&&(t=0);var e=this,i=e.rAF,n=e.prizeFlag,r=e.prizeDeg,o=e.rotateDeg,s=e._defaultConfig;if(-1!==n){var a=Date.now()-this.startTime;if(a>=s.accelerationTime&&void 0!==n){this.FPS=a/t,this.endTime=Date.now(),this.stopDeg=o;for(var u=(Math.random()*r-r/2)*this.getLength(s.stopRange),h=0;++h;){var c=360*h-n*r-o-s.offsetDegree+u;if(E[s.speedFunction].easeOut(this.FPS,this.stopDeg,c,s.decelerationTime)-this.stopDeg>s.speed){this.endDeg=c;break}}return this.slowDown()}this.rotateDeg=(o+E[s.speedFunction].easeIn(a,0,s.speed,s.accelerationTime))%360,this.draw(),i(this.run.bind(this,t+1))}else this.startTime=0},e.prototype.slowDown=function(){var t,e=this,i=e.rAF,r=e.prizes,o=e.prizeFlag,s=e.stopDeg,a=e.endDeg,u=e._defaultConfig,h=Date.now()-this.endTime;if(-1!==o){if(h>=u.decelerationTime)return this.startTime=0,void(null===(t=this.endCallback)||void 0===t||t.call(this,n({},r.find((function(t,e){return e===o})))));this.rotateDeg=E[u.speedFunction].easeOut(h,s,a,u.decelerationTime)%360,this.draw(),i(this.slowDown.bind(this))}else this.startTime=0},e.prototype.getWidth=function(t,e){return void 0===e&&(e=this.prizeRadian*this.prizeRadius),a(t,"number")?t:a(t,"string")?this.changeUnits(t,e):0},e.prototype.getHeight=function(t,e){return void 0===e&&(e=this.prizeRadius),a(t,"number")?t:a(t,"string")?this.changeUnits(t,e):0},e.prototype.getOffsetX=function(t){return-t/2},e.prototype.conversionAxis=function(t,e){var i=this.config;return[t/i.dpr-this.Radius,e/i.dpr-this.Radius]},e}(w),D=function(t){function e(e,i){var n;void 0===i&&(i={});var r=t.call(this,e)||this;r.rows=3,r.cols=3,r.blocks=[],r.prizes=[],r.buttons=[],r.defaultConfig={},r._defaultConfig={gutter:5,speed:20,accelerationTime:2500,decelerationTime:2500},r.defaultStyle={},r._defaultStyle={borderRadius:20,fontColor:"#000",fontSize:"18px",fontStyle:"sans-serif",fontWeight:"400",lineHeight:"",background:"rgba(0,0,0,0)",shadow:"",wordWrap:!0,lengthLimit:"90%"},r.activeStyle={},r._activeStyle={background:"#ffce98",shadow:"",fontStyle:"",fontWeight:"",fontSize:"",lineHeight:"",fontColor:""},r.cellWidth=0,r.cellHeight=0,r.startTime=0,r.endTime=0,r.currIndex=0,r.stopIndex=0,r.endIndex=0,r.demo=!1,r.timer=0,r.FPS=16.6,r.prizeFlag=-1,r.cells=[],r.blockImgs=[[]],r.btnImgs=[[]],r.prizeImgs=[],e.ob&&(r.initData(i),r.initWatch()),r.initComputed(),null===(n=e.beforeCreate)||void 0===n||n.call(r);var o=r.buttons.map((function(t){return t.imgs}));return r.button&&o.push(r.button.imgs),r.init({blockImgs:r.blocks.map((function(t){return t.imgs})),prizeImgs:r.prizes.map((function(t){return t.imgs})),btnImgs:o}),r}return i(e,t),e.prototype.initData=function(t){this.$set(this,"rows",Number(t.rows)||3),this.$set(this,"cols",Number(t.cols)||3),this.$set(this,"blocks",t.blocks||[]),this.$set(this,"prizes",t.prizes||[]),this.$set(this,"buttons",t.buttons||[]),this.$set(this,"button",t.button),this.$set(this,"defaultConfig",t.defaultConfig||{}),this.$set(this,"defaultStyle",t.defaultStyle||{}),this.$set(this,"activeStyle",t.activeStyle||{}),this.$set(this,"startCallback",t.start),this.$set(this,"endCallback",t.end)},e.prototype.initComputed=function(){var t=this;this.$computed(this,"_defaultConfig",(function(){var e=n({gutter:5,speed:20,accelerationTime:2500,decelerationTime:2500},t.defaultConfig);return e.gutter=t.getLength(e.gutter),e.speed=e.speed/40,e})),this.$computed(this,"_defaultStyle",(function(){return n({borderRadius:20,fontColor:"#000",fontSize:"18px",fontStyle:"sans-serif",fontWeight:"400",background:"rgba(0,0,0,0)",shadow:"",wordWrap:!0,lengthLimit:"90%"},t.defaultStyle)})),this.$computed(this,"_activeStyle",(function(){return n({background:"#ffce98",shadow:""},t.activeStyle)}))},e.prototype.initWatch=function(){var t=this;this.$watch("blocks",(function(e){return t.init({blockImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("prizes",(function(e){return t.init({prizeImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("buttons",(function(e){var i=e.map((function(t){return t.imgs}));return t.button&&i.push(t.button.imgs),t.init({btnImgs:i})}),{deep:!0}),this.$watch("button",(function(){var e=t.buttons.map((function(t){return t.imgs}));return t.button&&e.push(t.button.imgs),t.init({btnImgs:e})}),{deep:!0}),this.$watch("rows",(function(){return t.init({})})),this.$watch("cols",(function(){return t.init({})})),this.$watch("defaultConfig",(function(){return t.draw()}),{deep:!0}),this.$watch("defaultStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("activeStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("startCallback",(function(){return t.init({})})),this.$watch("endCallback",(function(){return t.init({})}))},e.prototype.init=function(e){var i,n,r=this;t.prototype.init.call(this);var o=this,s=o.config;o.ctx,o.button,null===(i=s.beforeInit)||void 0===i||i.call(this),this.draw(),Object.keys(e).forEach((function(t){var i=t,n=e[i],o={blockImgs:"blocks",prizeImgs:"prizes",btnImgs:"buttons"}[i];n&&n.forEach((function(t,e){t&&t.forEach((function(t,n){r.loadAndCacheImg(o,e,i,n,(function(){r.draw()}))}))}))})),null===(n=s.afterInit)||void 0===n||n.call(this)},e.prototype.handleClick=function(t){var e=this,i=this.ctx;s(s([],this.buttons),[this.button]).forEach((function(n){var r;if(n){var o=e.getGeometricProperty([n.x,n.y,n.col||1,n.row||1]),s=o[0],a=o[1],u=o[2],h=o[3];i.beginPath(),i.rect(s,a,u,h),i.isPointInPath(t.offsetX,t.offsetY)&&(e.startTime||("function"==typeof n.callback&&n.callback.call(e,n),null===(r=e.startCallback)||void 0===r||r.call(e,t,n)))}}))},e.prototype.loadAndCacheImg=function(t,e,i,n,s){return r(this,void 0,void 0,(function(){var r,a,u,h=this;return o(this,(function(o){return r=this[t][e],"buttons"===t&&!this.buttons.length&&this.button&&(r=this.button),r&&r.imgs&&(a=r.imgs[n])?(this[i][e]||(this[i][e]=[]),u=[this.loadImg(a.src,a),a.activeSrc&&this.loadImg(a.activeSrc,a,"$activeResolve")],Promise.all(u).then((function(t){var r=t[0],o=t[1];h[i][e][n]={defaultImg:r,activeImg:o},s.call(h)})).catch((function(i){console.error(t+"["+e+"].imgs["+n+"] "+i)})),[2]):[2]}))}))},e.prototype.computedWidthAndHeight=function(t,e,i){if(!e.width&&!e.height)return[t.width,t.height];if(e.width&&!e.height){var n=this.getWidth(e.width,i.col);return[n,t.height*(n/t.width)]}if(!e.width&&e.height){var r=this.getHeight(e.height,i.row);return[t.width*(r/t.height),r]}return[this.getWidth(e.width,i.col),this.getHeight(e.height,i.row)]},e.prototype.draw=function(){var t,e,i=this,n=this,r=n.config,o=n.ctx,c=n._defaultConfig,l=n._defaultStyle,f=n._activeStyle;null===(t=r.beforeDraw)||void 0===t||t.call(this,o),o.clearRect(0,0,this.boxWidth,this.boxHeight),this.cells=s(s([],this.prizes),this.buttons),this.button&&this.cells.push(this.button),this.cells.forEach((function(t){t.col=t.col||1,t.row=t.row||1})),this.prizeArea=this.blocks.reduce((function(t,e){var n=t.x,r=t.y,s=t.w,u=t.h,c=function(t){var e,i=(null===(e=t.padding)||void 0===e?void 0:e.replace(/px/g,"").split(" ").map((function(t){return~~t})))||[0],n=0,r=0,o=0,s=0;switch(i.length){case 1:n=r=o=s=i[0];break;case 2:n=r=i[0],o=s=i[1];break;case 3:n=i[0],o=s=i[1],r=i[2];break;default:n=i[0],r=i[1],o=i[2],s=i[3]}var u={paddingTop:n,paddingBottom:r,paddingLeft:o,paddingRight:s};for(var h in u)u[h]=Object.prototype.hasOwnProperty.call(t,h)&&a(t[h],"string","number")?~~String(t[h]).replace(/px/g,""):u[h];return[n,r,o,s]}(e),f=c[0],d=c[1],p=c[2],g=c[3],m=e.borderRadius?i.getLength(e.borderRadius):0,v=e.background||l.background;return h(v)&&T(o,n,r,s,u,m,i.handleBackground(n,r,s,u,v)),{x:n+p,y:r+f,w:s-p-g,h:u-f-d}}),{x:0,y:0,w:this.boxWidth,h:this.boxHeight}),this.cellWidth=(this.prizeArea.w-c.gutter*(this.cols-1))/this.cols,this.cellHeight=(this.prizeArea.h-c.gutter*(this.rows-1))/this.rows,this.cells.forEach((function(t,e){var n=i.getGeometricProperty([t.x,t.y,t.col,t.row]),s=n[0],a=n[1],c=n[2],d=n[3],p=!1;(void 0===i.prizeFlag||i.prizeFlag>-1)&&(p=e===i.currIndex%i.prizes.length>>0);var g=p?f.background:t.background||l.background;if(h(g)){var m=(p?f.shadow:t.shadow||l.shadow).replace(/px/g,"").split(",")[0].split(" ").map((function(t,e){return e<3?Number(t):t}));4===m.length&&(o.shadowColor=m[3],o.shadowOffsetX=m[0]*r.dpr,o.shadowOffsetY=m[1]*r.dpr,o.shadowBlur=m[2],m[0]>0?c-=m[0]:(c+=m[0],s-=m[0]),m[1]>0?d-=m[1]:(d+=m[1],a-=m[1])),T(o,s,a,c,d,i.getLength(t.borderRadius?t.borderRadius:l.borderRadius),i.handleBackground(s,a,c,d,g)),o.shadowColor="rgba(0, 0, 0, 0)",o.shadowOffsetX=0,o.shadowOffsetY=0,o.shadowBlur=0}var v="prizeImgs";e>=i.prizes.length&&(v="btnImgs",e-=i.prizes.length),t.imgs&&t.imgs.forEach((function(n,r){if(i[v][e]){var o=i[v][e][r];if(o){var u=p&&o.activeImg||o.defaultImg;if(u){var h=i.computedWidthAndHeight(u,n,t),c=h[0],l=h[1],f=[s+i.getOffsetX(c,t.col),a+i.getHeight(n.top,t.row)],d=f[0],g=f[1];i.drawImage(u,d,g,c,l)}}}})),t.fonts&&t.fonts.forEach((function(e){var n=p&&f.fontStyle?f.fontStyle:e.fontStyle||l.fontStyle,r=p&&f.fontWeight?f.fontWeight:e.fontWeight||l.fontWeight,h=p&&f.fontSize?i.getLength(f.fontSize):i.getLength(e.fontSize||l.fontSize),c=p&&f.lineHeight?f.lineHeight:e.lineHeight||l.lineHeight||e.fontSize||l.fontSize;o.font=r+" "+(h>>0)+"px "+n,o.fillStyle=p&&f.fontColor?f.fontColor:e.fontColor||l.fontColor;var d=[],g=String(e.text);if(Object.prototype.hasOwnProperty.call(e,"wordWrap")?e.wordWrap:l.wordWrap){g=u(g);for(var m="",v=0;vi.getWidth(e.lengthLimit||l.lengthLimit,t.col)&&(d.push(m.slice(0,-1)),m=g[v])}m&&d.push(m),d.length||d.push(g)}else d=g.split("\n");d.forEach((function(n,r){o.fillText(n,s+i.getOffsetX(o.measureText(n).width,t.col),a+i.getHeight(e.top,t.row)+(r+1)*i.getLength(c))}))}))})),null===(e=r.afterDraw)||void 0===e||e.call(this,o)},e.prototype.handleBackground=function(t,e,i,n,r){var o=this.ctx;return r.includes("linear-gradient")&&(r=function(t,e,i,n,r,o){var s=/linear-gradient\((.+)\)/.exec(o)[1].split(",").map((function(t){return t.trim()})),a=s.shift(),u=[0,0,0,0];if(a.includes("deg")){var h=function(t){return Math.tan(t/180*Math.PI)};(a=a.slice(0,-3)%360)>=0&&a<45?u=[e,i+r,e+n,i+r-n*h(a-0)]:a>=45&&a<90?u=[e,i+r,e+n-r*h(a-45),i]:a>=90&&a<135?u=[e+n,i+r,e+n-r*h(a-90),i]:a>=135&&a<180?u=[e+n,i+r,e,i+n*h(a-135)]:a>=180&&a<225?u=[e+n,i,e,i+n*h(a-180)]:a>=225&&a<270?u=[e+n,i,e+r*h(a-225),i+r]:a>=270&&a<315?u=[e,i,e+r*h(a-270),i+r]:a>=315&&a<360&&(u=[e,i,e+n,i+r-n*h(a-315)])}else a.includes("top")?u=[e,i+r,e,i]:a.includes("bottom")?u=[e,i,e,i+r]:a.includes("left")?u=[e+n,i,e,i]:a.includes("right")&&(u=[e,i,e+n,i]);var c=t.createLinearGradient.apply(t,u.map((function(t){return t>>0})));return s.reduce((function(t,e,i){var n=e.split(" ");return 1===n.length?t.addColorStop(i,n[0]):2===n.length&&t.addColorStop.apply(t,n),t}),c)}(o,t,e,i,n,r)),r},e.prototype.play=function(){var t=this.config.clearInterval;this.startTime||(t(this.timer),this.startTime=Date.now(),this.prizeFlag=void 0,this.run())},e.prototype.stop=function(t){this.prizeFlag=t<0?-1:t%this.prizes.length,-1===this.prizeFlag&&(this.currIndex=0,this.draw())},e.prototype.run=function(t){void 0===t&&(t=0);var e=this,i=e.rAF,n=e.currIndex,r=e.prizes,o=e.prizeFlag,s=e.startTime,a=e._defaultConfig,u=Date.now()-s;if(u>=a.accelerationTime&&void 0!==o){this.FPS=u/t,this.endTime=Date.now(),this.stopIndex=n;for(var h=0;++h;){var c=r.length*h+o-(n>>0);if(C.easeOut(this.FPS,this.stopIndex,c,a.decelerationTime)-this.stopIndex>a.speed){this.endIndex=c;break}}return this.slowDown()}this.currIndex=(n+C.easeIn(u,.1,a.speed,a.accelerationTime))%r.length,this.draw(),i(this.run.bind(this,t+1))},e.prototype.slowDown=function(){var t,e=this,i=e.rAF,r=e.prizes,o=e.prizeFlag,s=e.stopIndex,a=e.endIndex,u=e._defaultConfig,h=Date.now()-this.endTime;if(-1!==o){if(h>u.decelerationTime)return this.startTime=0,void(null===(t=this.endCallback)||void 0===t||t.call(this,n({},r.find((function(t,e){return e===o})))));this.currIndex=C.easeOut(h,s,a,u.decelerationTime)%r.length,this.draw(),i(this.slowDown.bind(this))}else this.startTime=0},e.prototype.walk=function(){var t=this,e=this.config,i=e.setInterval;(0,e.clearInterval)(this.timer),this.timer=i((function(){t.currIndex+=1,t.draw()}),1300)},e.prototype.getGeometricProperty=function(t){var e=t[0],i=t[1],n=t[2],r=t[3],o=this.cellWidth,s=this.cellHeight,a=this._defaultConfig.gutter,u=[this.prizeArea.x+(o+a)*e,this.prizeArea.y+(s+a)*i];return n&&r&&u.push(o*n+a*(n-1),s*r+a*(r-1)),u},e.prototype.getWidth=function(t,e){return void 0===e&&(e=1),a(t,"number")?t:a(t,"string")?this.changeUnits(t,this.cellWidth*e+this._defaultConfig.gutter*(e-1)):0},e.prototype.getHeight=function(t,e){return void 0===e&&(e=1),a(t,"number")?t:a(t,"string")?this.changeUnits(t,this.cellHeight*e+this._defaultConfig.gutter*(e-1)):0},e.prototype.getOffsetX=function(t,e){return void 0===e&&(e=1),(this.cellWidth*e+this._defaultConfig.gutter*(e-1)-t)/2},e.prototype.conversionAxis=function(t,e){var i=this.config;return[t/i.dpr,e/i.dpr]},e}(w);t.LuckyGrid=D,t.LuckyWheel=P,Object.defineProperty(t,"__esModule",{value:!0})})); 16 | -------------------------------------------------------------------------------- /examples/ggk/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 | 17 | 41 | 42 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 官网文档 10 | 18 | 大转盘示例 19 | 22 | 九宫格示例 23 | 26 | 刮刮乐示例 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/ymc/img/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/0.png -------------------------------------------------------------------------------- /examples/ymc/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/1.png -------------------------------------------------------------------------------- /examples/ymc/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/2.png -------------------------------------------------------------------------------- /examples/ymc/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/3.png -------------------------------------------------------------------------------- /examples/ymc/img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/4.png -------------------------------------------------------------------------------- /examples/ymc/img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/5.png -------------------------------------------------------------------------------- /examples/ymc/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/6.png -------------------------------------------------------------------------------- /examples/ymc/img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/7.png -------------------------------------------------------------------------------- /examples/ymc/img/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/ymc/img/button.png -------------------------------------------------------------------------------- /examples/ymc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /examples/yyjk/img/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/0.png -------------------------------------------------------------------------------- /examples/yyjk/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/1.png -------------------------------------------------------------------------------- /examples/yyjk/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/2.png -------------------------------------------------------------------------------- /examples/yyjk/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/3.png -------------------------------------------------------------------------------- /examples/yyjk/img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/4.png -------------------------------------------------------------------------------- /examples/yyjk/img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/5.png -------------------------------------------------------------------------------- /examples/yyjk/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/6.png -------------------------------------------------------------------------------- /examples/yyjk/img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/7.png -------------------------------------------------------------------------------- /examples/yyjk/img/btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/btn.png -------------------------------------------------------------------------------- /examples/yyjk/img/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/examples/yyjk/img/button.png -------------------------------------------------------------------------------- /examples/yyjk/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/lucky-canvas.cjs.min.js') 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucky-canvas", 3 | "version": "1.5.4", 4 | "description": "一个基于原生 js 的(大转盘抽奖 / 九宫格抽奖)插件", 5 | "main": "dist/lucky-canvas.cjs.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.dev.js -w", 8 | "build": "rollup --config rollup.config.build.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/LuckDraw/lucky-canvas.git" 13 | }, 14 | "keywords": [ 15 | "vue3抽奖", 16 | "大转盘抽奖", 17 | "九宫格抽奖", 18 | "抽奖插件", 19 | "js抽奖", 20 | "移动端抽奖", 21 | "canvas抽奖" 22 | ], 23 | "author": "ldq ", 24 | "license": "BSD-3-Clause", 25 | "bugs": { 26 | "url": "https://github.com/LuckDraw/lucky-canvas/issues" 27 | }, 28 | "homepage": "https://100px.net", 29 | "files": [ 30 | "package.json", 31 | "LICENSE", 32 | "dist", 33 | "index.js", 34 | "umd.min.js" 35 | ], 36 | "devDependencies": { 37 | "@babel/core": "^7.12.3", 38 | "@babel/preset-env": "^7.12.1", 39 | "@rollup/plugin-commonjs": "^16.0.0", 40 | "@rollup/plugin-eslint": "^8.0.1", 41 | "@rollup/plugin-json": "^4.1.0", 42 | "@rollup/plugin-node-resolve": "^10.0.0", 43 | "@rollup/plugin-typescript": "^6.1.0", 44 | "@typescript-eslint/parser": "^4.14.0", 45 | "babel-plugin-external-helpers": "^6.22.0", 46 | "babel-preset-latest": "^6.24.1", 47 | "eslint": "^7.18.0", 48 | "eslint-plugin-prettier": "^3.3.1", 49 | "prettier": "^2.2.1", 50 | "rollup": "^2.33.1", 51 | "rollup-plugin-babel": "^4.4.0", 52 | "rollup-plugin-livereload": "^2.0.0", 53 | "rollup-plugin-serve": "^1.1.0", 54 | "rollup-plugin-terser": "^7.0.2", 55 | "rollup-plugin-typescript2": "^0.29.0", 56 | "tslib": "^2.0.3", 57 | "typescript": "^4.0.5" 58 | }, 59 | "dependencies": {} 60 | } 61 | -------------------------------------------------------------------------------- /rollup.config.build.js: -------------------------------------------------------------------------------- 1 | import { name } from './package.json' 2 | import ts from '@rollup/plugin-typescript' 3 | import json from '@rollup/plugin-json' 4 | import resolve from '@rollup/plugin-node-resolve' 5 | import commonjs from '@rollup/plugin-commonjs' 6 | import babel from 'rollup-plugin-babel' 7 | import { terser } from 'rollup-plugin-terser' 8 | 9 | export default { 10 | input: 'src/index.ts', 11 | output: [ 12 | { 13 | file: `dist/${name}.cjs.js`, 14 | format: 'cjs', 15 | }, 16 | { 17 | file: `dist/${name}.cjs.min.js`, 18 | format: 'cjs', 19 | plugins: [terser()] 20 | }, 21 | { 22 | file: `dist/${name}.umd.js`, 23 | format: 'umd', 24 | name: 'LuckyCanvas', 25 | }, 26 | { 27 | file: `dist/${name}.umd.min.js`, 28 | format: 'umd', 29 | name: 'LuckyCanvas', 30 | plugins: [terser()] 31 | }, 32 | { 33 | file: `umd.min.js`, 34 | format: 'umd', 35 | name: 'LuckyCanvas', 36 | plugins: [terser()] 37 | }, 38 | ], 39 | plugins: [ 40 | ts(), 41 | json(), 42 | resolve(), 43 | commonjs(), 44 | babel({ exclude: 'node_modules/**' }), 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import ts from 'rollup-plugin-typescript2' 3 | import json from '@rollup/plugin-json' 4 | import resolve from '@rollup/plugin-node-resolve' 5 | import commonjs from '@rollup/plugin-commonjs' 6 | import babel from 'rollup-plugin-babel' 7 | import livereload from 'rollup-plugin-livereload' 8 | import serve from 'rollup-plugin-serve' 9 | import eslint from '@rollup/plugin-eslint' 10 | 11 | export default { 12 | input: 'src/index.ts', 13 | output: [ 14 | { 15 | file: `examples/dev.js`, 16 | format: 'umd', 17 | name: 'LuckyCanvas', 18 | sourcemap: true, 19 | }, 20 | ], 21 | plugins: [ 22 | resolve(), 23 | commonjs(), 24 | json(), 25 | // eslint({ 26 | // throwOnError: true, 27 | // throwOnWarning: true, 28 | // include: ['src/**'], 29 | // exclude: ['node_modules/**'] 30 | // }), 31 | ts({ 32 | tsconfig: path.resolve(__dirname, './tsconfig.json'), 33 | extensions: ['.js', '.ts'] 34 | }), 35 | babel({ exclude: 'node_modules/**' }), 36 | livereload(), 37 | serve({ 38 | open: true, 39 | port: 8888, 40 | contentBase: './examples' 41 | }), 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LuckyWheel } from './lib/wheel' 2 | export { default as LuckyGrid } from './lib/grid' 3 | // export { default as LuckyCard } from './lib/card' 4 | -------------------------------------------------------------------------------- /src/lib/card.ts: -------------------------------------------------------------------------------- 1 | import Lucky from './lucky' 2 | import { ConfigType, UniImageType } from '../types/index' 3 | import LuckyCardConfig, { 4 | MaskType, 5 | DefaultConfigType, 6 | StartCallbackType, 7 | EndCallbackType 8 | } from '../types/card' 9 | import { isExpectType, computePadding, hasBackground } from '../utils/index' 10 | import { drawRoundRect } from '../utils/math' 11 | 12 | export default class LuckyCard extends Lucky { 13 | private mask: MaskType = {} 14 | private defaultConfig: DefaultConfigType = {} 15 | private _defaultConfig = { 16 | percent: 0.5, 17 | cleanZone: { x: 0, y: 0, width: 0, height: 0 } 18 | } 19 | private startCallback?: StartCallbackType 20 | private endCallback?: EndCallbackType 21 | // 是否可以开始游戏 22 | private canPlay: boolean = false 23 | // 鼠标是否按下 24 | private isMouseDown: boolean = false 25 | 26 | /** 27 | * 刮刮乐构造器 28 | * @param config 29 | * @param data 30 | */ 31 | constructor (config: ConfigType, data: LuckyCardConfig = {}) { 32 | super(config) 33 | this.initData(data) 34 | this.init() 35 | } 36 | 37 | initData (data: LuckyCardConfig) { 38 | this.$set(this, 'mask', data.mask || []) 39 | this.$set(this, 'startCallback', data.start) 40 | this.$set(this, 'endCallback', data.end) 41 | } 42 | 43 | init () { 44 | super.init() 45 | const { config, ctx } = this 46 | this.canPlay = false 47 | this.draw() 48 | } 49 | 50 | draw () { 51 | const { config, ctx, mask } = this 52 | ctx.globalCompositeOperation = 'source-over' 53 | const background = mask.background 54 | if (hasBackground(background)) { 55 | ctx.fillStyle = background! 56 | ctx.beginPath() 57 | ctx.rect(0, 0, this.boxWidth, this.boxHeight) 58 | ctx.fill() 59 | } 60 | ctx.globalCompositeOperation = 'destination-out' 61 | } 62 | 63 | /** 64 | * 鼠标移动事件 65 | * @param e 事件参数 66 | */ 67 | protected handleMouseMove (e: MouseEvent): void { 68 | if (!this.canPlay || !this.isMouseDown) return 69 | const { config, ctx, _defaultConfig } = this 70 | ctx.beginPath() 71 | const radius = 20 72 | const [x, y] = this.conversionAxis(e.offsetX, e.offsetY) 73 | // ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2) 74 | drawRoundRect(ctx, x - radius, y - radius, radius * 2, radius * 2, 15, '#ccc') 75 | ctx.fill() 76 | const ImageData = ctx.getImageData(0, 0, this.boxWidth * config.dpr, this.boxHeight * config.dpr)?.data 77 | let count = 0, len = ImageData.length / 4 78 | for (let i = 1; i <= len; i++) { 79 | if (ImageData[(i - 1) * 4] < 128) count++ 80 | } 81 | const percent = +(count / len).toFixed(2) 82 | if (percent > this.getLength(_defaultConfig.percent)) { 83 | this.clean() 84 | this.endCallback?.() 85 | } 86 | } 87 | 88 | /** 89 | * 开始游戏 (调用该方法才可以擦除) 90 | */ 91 | public play () { 92 | this.canPlay = true 93 | this.startCallback?.() 94 | } 95 | 96 | /** 97 | * 擦除所有区域 98 | */ 99 | public clean () { 100 | const { config, ctx } = this 101 | ctx.clearRect(0, 0, this.boxWidth, this.boxHeight) 102 | } 103 | 104 | /** 105 | * 鼠标按下事件 106 | * @param e 事件参数 107 | */ 108 | protected handleMouseDown (e: MouseEvent): void { 109 | this.isMouseDown = true 110 | } 111 | 112 | /** 113 | * 鼠标抬起事件 114 | * @param e 事件参数 115 | */ 116 | protected handleMouseUp (e: MouseEvent): void { 117 | this.isMouseDown = false 118 | } 119 | 120 | /** 121 | * 获取相对宽度 122 | */ 123 | private getWidth ( 124 | width: string | number | undefined, 125 | maxWidth: number = this.boxWidth 126 | ): number { 127 | if (isExpectType(width, 'number')) return (width as number) 128 | if (isExpectType(width, 'string')) return this.changeUnits(width as string, maxWidth) 129 | return 0 130 | } 131 | 132 | /** 133 | * 获取相对高度 134 | */ 135 | private getHeight ( 136 | height: string | number | undefined, 137 | maxHeight: number = this.boxHeight 138 | ): number { 139 | if (isExpectType(height, 'number')) return (height as number) 140 | if (isExpectType(height, 'string')) return this.changeUnits(height as string, maxHeight) 141 | return 0 142 | } 143 | 144 | /** 145 | * 换算渲染坐标 146 | * @param x 147 | * @param y 148 | */ 149 | protected conversionAxis (x: number, y: number): [number, number] { 150 | const { config } = this 151 | return [x / config.dpr, y / config.dpr] 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/lib/grid.ts: -------------------------------------------------------------------------------- 1 | import Lucky from './lucky' 2 | import { ConfigType, UniImageType } from '../types/index' 3 | import LuckyGridConfig, { 4 | BlockType, BlockImgType, 5 | PrizeType, PrizeImgType, 6 | ButtonType, ButtonImgType, 7 | CellFontType, CellImgType, 8 | RowsType, ColsType, 9 | CellType, 10 | DefaultConfigType, 11 | DefaultStyleType, 12 | ActiveStyleType, 13 | StartCallbackType, 14 | EndCallbackType, 15 | } from '../types/grid' 16 | import { isExpectType, removeEnter, computePadding, hasBackground } from '../utils/index' 17 | import { drawRoundRect, getLinearGradient } from '../utils/math' 18 | import { quad } from '../utils/tween' 19 | 20 | type ImgObjType = HTMLImageElement | UniImageType 21 | 22 | export default class LuckyGrid extends Lucky { 23 | private rows: RowsType = 3 24 | private cols: ColsType = 3 25 | private blocks: Array = [] 26 | private prizes: Array = [] 27 | private buttons: Array = [] 28 | private button?: ButtonType 29 | private defaultConfig: DefaultConfigType = {} 30 | private _defaultConfig = { // 此处初始化无用, 是为了方便类型推导才加的 31 | gutter: 5, 32 | speed: 20, 33 | accelerationTime: 2500, 34 | decelerationTime: 2500, 35 | } 36 | private defaultStyle: DefaultStyleType = {} 37 | private _defaultStyle = { // 此处初始化无用, 是为了方便类型推导才加的 38 | borderRadius: 20, 39 | fontColor: '#000', 40 | fontSize: '18px', 41 | fontStyle: 'sans-serif', 42 | fontWeight: '400', 43 | lineHeight: '', 44 | background: 'rgba(0,0,0,0)', 45 | shadow: '', 46 | wordWrap: true, 47 | lengthLimit: '90%', 48 | } 49 | private activeStyle: ActiveStyleType = {} 50 | private _activeStyle = { // 此处初始化无用, 是为了方便类型推导才加的 51 | background: '#ffce98', 52 | shadow: '', 53 | fontStyle: '', 54 | fontWeight: '', 55 | fontSize: '', 56 | lineHeight: '', 57 | fontColor: '', 58 | } 59 | private startCallback?: StartCallbackType 60 | private endCallback?: EndCallbackType 61 | private cellWidth = 0 // 格子宽度 62 | private cellHeight = 0 // 格子高度 63 | private startTime = 0 // 开始时间戳 64 | private endTime = 0 // 结束时间戳 65 | private currIndex = 0 // 当前index累加 66 | private stopIndex = 0 // 刻舟求剑 67 | private endIndex = 0 // 停止索引 68 | private demo = false // 是否自动游走 69 | private timer = 0 // 游走定时器 70 | private FPS = 16.6 // 屏幕刷新率 71 | /** 72 | * 中奖索引 73 | * prizeFlag = undefined 时, 处于开始抽奖阶段, 正常旋转 74 | * prizeFlag >= 0 时, 说明stop方法被调用, 并且传入了中奖索引 75 | * prizeFlag === -1 时, 说明stop方法被调用, 并且传入了负值, 本次抽奖无效 76 | */ 77 | private prizeFlag: number | undefined = -1 78 | // 所有格子 79 | private cells: CellType[] = [] 80 | // 奖品区域几何信息 81 | private prizeArea: { x: number, y: number, w: number, h: number } | undefined 82 | // 图片缓存 83 | private blockImgs: Array<{ defaultImg: ImgObjType }[]> = [[]] 84 | private btnImgs: Array<{ defaultImg: ImgObjType }[]> = [[]] 85 | private prizeImgs: Array<{ defaultImg: ImgObjType, activeImg?: ImgObjType }[]> = [] 86 | 87 | /** 88 | * 九宫格构造器 89 | * @param config 元素标识 90 | * @param data 抽奖配置项 91 | */ 92 | constructor (config: ConfigType, data: LuckyGridConfig = {}) { 93 | super(config) 94 | if (config.ob) { 95 | this.initData(data) 96 | this.initWatch() 97 | } 98 | this.initComputed() 99 | // 创建前回调函数 100 | config.beforeCreate?.call(this) 101 | const btnImgs = this.buttons.map(btn => btn.imgs) 102 | if (this.button) btnImgs.push(this.button.imgs) 103 | this.init({ 104 | blockImgs: this.blocks.map(block => block.imgs), 105 | prizeImgs: this.prizes.map(prize => prize.imgs), 106 | btnImgs, 107 | }) 108 | } 109 | 110 | /** 111 | * 初始化数据 112 | * @param data 113 | */ 114 | private initData (data: LuckyGridConfig): void { 115 | this.$set(this, 'rows', Number(data.rows) || 3) 116 | this.$set(this, 'cols', Number(data.cols) || 3) 117 | this.$set(this, 'blocks', data.blocks || []) 118 | this.$set(this, 'prizes', data.prizes || []) 119 | this.$set(this, 'buttons', data.buttons || []) 120 | // 临时过渡代码, 升级到2.x即可删除 121 | this.$set(this, 'button', data.button) 122 | this.$set(this, 'defaultConfig', data.defaultConfig || {}) 123 | this.$set(this, 'defaultStyle', data.defaultStyle || {}) 124 | this.$set(this, 'activeStyle', data.activeStyle || {}) 125 | this.$set(this, 'startCallback', data.start) 126 | this.$set(this, 'endCallback', data.end) 127 | } 128 | 129 | /** 130 | * 初始化属性计算 131 | */ 132 | private initComputed (): void { 133 | // 默认配置 134 | this.$computed(this, '_defaultConfig', () => { 135 | const config = { 136 | gutter: 5, 137 | speed: 20, 138 | accelerationTime: 2500, 139 | decelerationTime: 2500, 140 | ...this.defaultConfig 141 | } 142 | config.gutter = this.getLength(config.gutter) 143 | config.speed = config.speed / 40 144 | return config 145 | }) 146 | // 默认样式 147 | this.$computed(this, '_defaultStyle', () => { 148 | return { 149 | borderRadius: 20, 150 | fontColor: '#000', 151 | fontSize: '18px', 152 | fontStyle: 'sans-serif', 153 | fontWeight: '400', 154 | background: 'rgba(0,0,0,0)', 155 | shadow: '', 156 | wordWrap: true, 157 | lengthLimit: '90%', 158 | ...this.defaultStyle 159 | } 160 | }) 161 | // 中奖样式 162 | this.$computed(this, '_activeStyle', () => { 163 | return { 164 | background: '#ffce98', 165 | shadow: '', 166 | ...this.activeStyle 167 | } 168 | }) 169 | } 170 | 171 | /** 172 | * 初始化观察者 173 | */ 174 | private initWatch (): void { 175 | // 监听 blocks 数据的变化 176 | this.$watch('blocks', (newData: Array) => { 177 | return this.init({ blockImgs: newData.map(block => block.imgs) }) 178 | }, { deep: true }) 179 | // 监听 prizes 数据的变化 180 | this.$watch('prizes', (newData: Array) => { 181 | return this.init({ prizeImgs: newData.map(prize => prize.imgs) }) 182 | }, { deep: true }) 183 | // 监听 button 数据的变化 184 | this.$watch('buttons', (newData: Array) => { 185 | const btnImgs = newData.map(btn => btn.imgs) 186 | if (this.button) btnImgs.push(this.button.imgs) 187 | return this.init({ btnImgs }) 188 | }, { deep: true }) 189 | // 临时过渡代码, 升级到2.x即可删除 190 | this.$watch('button', () => { 191 | const btnImgs = this.buttons.map(btn => btn.imgs) 192 | if (this.button) btnImgs.push(this.button.imgs) 193 | return this.init({ btnImgs }) 194 | }, { deep: true }) 195 | this.$watch('rows', () => this.init({})) 196 | this.$watch('cols', () => this.init({})) 197 | this.$watch('defaultConfig', () => this.draw(), { deep: true }) 198 | this.$watch('defaultStyle', () => this.draw(), { deep: true }) 199 | this.$watch('activeStyle', () => this.draw(), { deep: true }) 200 | this.$watch('startCallback', () => this.init({})) 201 | this.$watch('endCallback', () => this.init({})) 202 | } 203 | 204 | /** 205 | * 初始化 canvas 抽奖 206 | * @param willUpdateImgs 需要更新的图片 207 | */ 208 | public init (willUpdateImgs: { 209 | blockImgs?: Array, 210 | prizeImgs?: Array, 211 | btnImgs?: Array 212 | }): void { 213 | super.init() 214 | const { config, ctx, button } = this 215 | // 初始化前回调函数 216 | config.beforeInit?.call(this) 217 | // 先画一次防止闪烁 218 | this.draw() 219 | // 异步加载图片 220 | Object.keys(willUpdateImgs).forEach(key => { 221 | const imgName = key as 'blockImgs' | 'prizeImgs' | 'btnImgs' 222 | const willUpdate = willUpdateImgs[imgName] 223 | const cellName = { 224 | blockImgs: 'blocks', 225 | prizeImgs: 'prizes', 226 | btnImgs: 'buttons', 227 | }[imgName] as 'blocks' | 'prizes' | 'buttons' 228 | willUpdate && willUpdate.forEach((imgs, cellIndex) => { 229 | imgs && imgs.forEach((imgInfo, imgIndex) => { 230 | this.loadAndCacheImg(cellName, cellIndex, imgName, imgIndex, () => { 231 | this.draw() 232 | }) 233 | }) 234 | }) 235 | }) 236 | // 初始化后回调函数 237 | config.afterInit?.call(this) 238 | } 239 | 240 | /** 241 | * canvas点击事件 242 | * @param e 事件参数 243 | */ 244 | protected handleClick (e: MouseEvent): void { 245 | const { ctx } = this 246 | ;[ 247 | ...this.buttons, 248 | this.button 249 | ].forEach(btn => { 250 | if (!btn) return 251 | const [x, y, width, height] = this.getGeometricProperty([ 252 | btn.x, btn.y, btn.col || 1, btn.row || 1 253 | ]) 254 | ctx.beginPath() 255 | ctx.rect(x, y, width, height) 256 | if (!ctx.isPointInPath(e.offsetX, e.offsetY)) return 257 | if (this.startTime) return 258 | // 如果btn里有单独的回调方法, 优先触发 259 | if (typeof btn.callback === 'function') btn.callback.call(this, btn) 260 | // 最后触发公共回调 261 | this.startCallback?.(e, btn) 262 | }) 263 | } 264 | 265 | /** 266 | * 根据索引单独加载指定图片并缓存 267 | * @param { number } prizeIndex 奖品索引 268 | * @param { number } imgIndex 奖品图片索引 269 | * @param { Function } callBack 图片加载完毕回调 270 | */ 271 | private async loadAndCacheImg ( 272 | cellName: 'blocks' | 'prizes' | 'buttons', 273 | cellIndex: number, 274 | imgName: 'blockImgs' | 'prizeImgs' | 'btnImgs', 275 | imgIndex: number, 276 | callBack: () => void 277 | ) { 278 | let cell: BlockType | PrizeType | ButtonType = this[cellName][cellIndex] 279 | // 临时过渡代码, 升级到2.x即可删除 280 | if (cellName === 'buttons' && !this.buttons.length && this.button) { 281 | cell = this.button 282 | } 283 | if (!cell || !cell.imgs) return 284 | const imgInfo = cell.imgs[imgIndex] 285 | if (!imgInfo) return 286 | if (!this[imgName][cellIndex]) this[imgName][cellIndex] = [] 287 | // 异步加载图片 288 | const request = [ 289 | this.loadImg(imgInfo.src, imgInfo), 290 | imgInfo['activeSrc'] && this.loadImg(imgInfo['activeSrc'], imgInfo, '$activeResolve') 291 | ] 292 | Promise.all(request).then(([defaultImg, activeImg]) => { 293 | this[imgName][cellIndex][imgIndex] = { defaultImg, activeImg } as { 294 | defaultImg: ImgObjType, 295 | activeImg?: ImgObjType 296 | } 297 | callBack.call(this) 298 | }).catch(err => { 299 | console.error(`${cellName}[${cellIndex}].imgs[${imgIndex}] ${err}`) 300 | }) 301 | } 302 | 303 | /** 304 | * 计算图片的渲染宽高 305 | * @param imgObj 图片标签元素 306 | * @param imgInfo 图片信息 307 | * @param cell 格子信息 308 | * @return [渲染宽度, 渲染高度] 309 | */ 310 | private computedWidthAndHeight ( 311 | imgObj: ImgObjType, 312 | imgInfo: CellImgType, 313 | cell: CellType 314 | ): [number, number] { 315 | // 根据配置的样式计算图片的真实宽高 316 | if (!imgInfo.width && !imgInfo.height) { 317 | // 如果没有配置宽高, 则使用图片本身的宽高 318 | return [imgObj.width, imgObj.height] 319 | } else if (imgInfo.width && !imgInfo.height) { 320 | // 如果只填写了宽度, 没填写高度 321 | let trueWidth = this.getWidth(imgInfo.width, cell.col) 322 | // 那高度就随着宽度进行等比缩放 323 | return [trueWidth, imgObj.height * (trueWidth / imgObj.width)] 324 | } else if (!imgInfo.width && imgInfo.height) { 325 | // 如果只填写了宽度, 没填写高度 326 | let trueHeight = this.getHeight(imgInfo.height, cell.row) 327 | // 那宽度就随着高度进行等比缩放 328 | return [imgObj.width * (trueHeight / imgObj.height), trueHeight] 329 | } 330 | // 如果宽度和高度都填写了, 就分别计算 331 | return [ 332 | this.getWidth(imgInfo.width, cell.col), 333 | this.getHeight(imgInfo.height, cell.row) 334 | ] 335 | } 336 | 337 | /** 338 | * 绘制九宫格抽奖 339 | */ 340 | protected draw (): void { 341 | const { config, ctx, _defaultConfig, _defaultStyle, _activeStyle } = this 342 | // 触发绘制前回调 343 | config.beforeDraw?.call(this, ctx) 344 | // 清空画布 345 | ctx.clearRect(0, 0, this.boxWidth, this.boxHeight) 346 | // 合并奖品和按钮 347 | this.cells = [ 348 | ...this.prizes, 349 | ...this.buttons 350 | ] 351 | if (this.button) this.cells.push(this.button) 352 | this.cells.forEach(cell => { 353 | cell.col = cell.col || 1 354 | cell.row = cell.row || 1 355 | }) 356 | // 计算获取奖品区域的几何信息 357 | this.prizeArea = this.blocks.reduce(({x, y, w, h}, block) => { 358 | const [paddingTop, paddingBottom, paddingLeft, paddingRight] = computePadding(block) 359 | const r = block.borderRadius ? this.getLength(block.borderRadius) : 0 360 | // 绘制边框 361 | const background = block.background || _defaultStyle.background 362 | if (hasBackground(background)) { 363 | drawRoundRect(ctx, x, y, w, h, r, this.handleBackground(x, y, w, h, background)) 364 | } 365 | return { 366 | x: x + paddingLeft, 367 | y: y + paddingTop, 368 | w: w - paddingLeft - paddingRight, 369 | h: h - paddingTop - paddingBottom 370 | } 371 | }, { x: 0, y: 0, w: this.boxWidth, h: this.boxHeight }) 372 | // 计算单一奖品格子的宽度和高度 373 | this.cellWidth = (this.prizeArea.w - _defaultConfig.gutter * (this.cols - 1)) / this.cols 374 | this.cellHeight = (this.prizeArea.h - _defaultConfig.gutter * (this.rows - 1)) / this.rows 375 | // 绘制所有格子 376 | this.cells.forEach((cell, cellIndex) => { 377 | let [x, y, width, height] = this.getGeometricProperty([cell.x, cell.y, cell.col, cell.row]) 378 | // 默认不显示中奖标识 379 | let isActive = false 380 | // 只要 prizeFlag 不是负数, 就显示中奖标识 381 | if (this.prizeFlag === void 0 || this.prizeFlag > -1) { 382 | isActive = cellIndex === this.currIndex % this.prizes.length >> 0 383 | } 384 | // 绘制背景色 385 | const background = isActive ? _activeStyle.background : (cell.background || _defaultStyle.background) 386 | if (hasBackground(background)) { 387 | // 处理阴影 (暂时先用any, 这里后续要优化) 388 | const shadow: any = ( 389 | isActive ? _activeStyle.shadow! : (cell.shadow || _defaultStyle.shadow!) 390 | ) 391 | .replace(/px/g, '') // 清空px字符串 392 | .split(',')[0].split(' ') // 防止有人声明多个阴影, 截取第一个阴影 393 | .map((n, i) => i < 3 ? Number(n) : n) // 把数组的前三个值*像素比 394 | // 绘制阴影 395 | if (shadow.length === 4) { 396 | ctx.shadowColor = shadow[3] 397 | ctx.shadowOffsetX = shadow[0] * config.dpr 398 | ctx.shadowOffsetY = shadow[1] * config.dpr 399 | ctx.shadowBlur = shadow[2] 400 | // 修正(格子+阴影)的位置, 这里使用逗号运算符 401 | shadow[0] > 0 ? (width -= shadow[0]) : (width += shadow[0], x -= shadow[0]) 402 | shadow[1] > 0 ? (height -= shadow[1]) : (height += shadow[1], y -= shadow[1]) 403 | } 404 | drawRoundRect( 405 | ctx, x, y, width, height, 406 | this.getLength(cell.borderRadius ? cell.borderRadius : _defaultStyle.borderRadius), 407 | this.handleBackground(x, y, width, height, background) 408 | ) 409 | // 清空阴影 410 | ctx.shadowColor = 'rgba(0, 0, 0, 0)' 411 | ctx.shadowOffsetX = 0 412 | ctx.shadowOffsetY = 0 413 | ctx.shadowBlur = 0 414 | } 415 | // 修正图片缓存 416 | let cellName = 'prizeImgs' 417 | if (cellIndex >= this.prizes.length) { 418 | cellName = 'btnImgs' 419 | cellIndex -= this.prizes.length 420 | } 421 | // 绘制图片 422 | cell.imgs && cell.imgs.forEach((imgInfo, imgIndex) => { 423 | if (!this[cellName][cellIndex]) return 424 | const cellImg = this[cellName][cellIndex][imgIndex] 425 | if (!cellImg) return 426 | const renderImg = (isActive && cellImg['activeImg']) || cellImg.defaultImg 427 | if (!renderImg) return 428 | const [trueWidth, trueHeight] = this.computedWidthAndHeight(renderImg, imgInfo, cell) 429 | const [xAxis, yAxis] = [ 430 | x + this.getOffsetX(trueWidth, cell.col), 431 | y + this.getHeight(imgInfo.top, cell.row) 432 | ] 433 | this.drawImage(renderImg, xAxis, yAxis, trueWidth, trueHeight) 434 | }) 435 | // 绘制文字 436 | cell.fonts && cell.fonts.forEach(font => { 437 | // 字体样式 438 | let style = isActive && _activeStyle.fontStyle 439 | ? _activeStyle.fontStyle 440 | : (font.fontStyle || _defaultStyle.fontStyle) 441 | // 字体加粗 442 | let fontWeight = isActive && _activeStyle.fontWeight 443 | ? _activeStyle.fontWeight 444 | : (font.fontWeight || _defaultStyle.fontWeight) 445 | // 字体大小 446 | let size = isActive && _activeStyle.fontSize 447 | ? this.getLength(_activeStyle.fontSize) 448 | : this.getLength(font.fontSize || _defaultStyle.fontSize) 449 | // 字体行高 450 | const lineHeight = isActive && _activeStyle.lineHeight 451 | ? _activeStyle.lineHeight 452 | : font.lineHeight || _defaultStyle.lineHeight || font.fontSize || _defaultStyle.fontSize 453 | ctx.font = `${fontWeight} ${size >> 0}px ${style}` 454 | ctx.fillStyle = (isActive && _activeStyle.fontColor) ? _activeStyle.fontColor : (font.fontColor || _defaultStyle.fontColor) 455 | let lines = [], text = String(font.text) 456 | // 计算文字换行 457 | if (Object.prototype.hasOwnProperty.call(font, 'wordWrap') ? font.wordWrap : _defaultStyle.wordWrap) { 458 | text = removeEnter(text) 459 | let str = '' 460 | for (let i = 0; i < text.length; i++) { 461 | str += text[i] 462 | let currWidth = ctx.measureText(str).width 463 | let maxWidth = this.getWidth(font.lengthLimit || _defaultStyle.lengthLimit, cell.col) 464 | if (currWidth > maxWidth) { 465 | lines.push(str.slice(0, -1)) 466 | str = text[i] 467 | } 468 | } 469 | if (str) lines.push(str) 470 | if (!lines.length) lines.push(text) 471 | } else { 472 | lines = text.split('\n') 473 | } 474 | lines.forEach((line, lineIndex) => { 475 | ctx.fillText( 476 | line, 477 | x + this.getOffsetX(ctx.measureText(line).width, cell.col), 478 | y + this.getHeight(font.top, cell.row) + (lineIndex + 1) * this.getLength(lineHeight) 479 | ) 480 | }) 481 | }) 482 | }) 483 | // 触发绘制后回调 484 | config.afterDraw?.call(this, ctx) 485 | } 486 | 487 | /** 488 | * 处理背景色 489 | * @param x 490 | * @param y 491 | * @param width 492 | * @param height 493 | * @param background 494 | * @param isActive 495 | */ 496 | private handleBackground ( 497 | x: number, 498 | y: number, 499 | width: number, 500 | height: number, 501 | background: string, 502 | ) { 503 | const { ctx } = this 504 | // 处理线性渐变 505 | if (background.includes('linear-gradient')) { 506 | background = getLinearGradient(ctx, x, y, width, height, background) 507 | } 508 | return background 509 | } 510 | 511 | /** 512 | * 对外暴露: 开始抽奖方法 513 | */ 514 | public play (): void { 515 | const { clearInterval } = this.config 516 | if (this.startTime) return 517 | clearInterval(this.timer) 518 | this.startTime = Date.now() 519 | this.prizeFlag = void 0 520 | this.run() 521 | } 522 | 523 | /** 524 | * 对外暴露: 缓慢停止方法 525 | * @param index 中奖索引 526 | */ 527 | public stop (index: number): void { 528 | // 判断 prizeFlag 是否等于 -1 529 | this.prizeFlag = index < 0 ? -1 : index % this.prizes.length 530 | // 如果是 -1 就初始化状态 531 | if (this.prizeFlag === -1) { 532 | this.currIndex = 0 533 | this.draw() 534 | } 535 | } 536 | 537 | /** 538 | * 实际开始执行方法 539 | * @param num 记录帧动画执行多少次 540 | */ 541 | private run (num: number = 0): void { 542 | const { rAF, currIndex, prizes, prizeFlag, startTime, _defaultConfig } = this 543 | let interval = Date.now() - startTime 544 | // 先完全旋转, 再停止 545 | if (interval >= _defaultConfig.accelerationTime && prizeFlag !== void 0) { 546 | // 记录帧率 547 | this.FPS = interval / num 548 | // 记录开始停止的时间戳 549 | this.endTime = Date.now() 550 | // 记录开始停止的索引 551 | this.stopIndex = currIndex 552 | // 测算最终停止的索引 553 | let i = 0 554 | while (++i) { 555 | const endIndex = prizes.length * i + prizeFlag - (currIndex >> 0) 556 | let currSpeed = quad.easeOut(this.FPS, this.stopIndex, endIndex, _defaultConfig.decelerationTime) - this.stopIndex 557 | if (currSpeed > _defaultConfig.speed) { 558 | this.endIndex = endIndex 559 | break 560 | } 561 | } 562 | return this.slowDown() 563 | } 564 | this.currIndex = (currIndex + quad.easeIn(interval, 0.1, _defaultConfig.speed, _defaultConfig.accelerationTime)) % prizes.length 565 | this.draw() 566 | rAF(this.run.bind(this, num + 1)) 567 | } 568 | 569 | /** 570 | * 缓慢停止的方法 571 | */ 572 | private slowDown (): void { 573 | const { rAF, prizes, prizeFlag, stopIndex, endIndex, _defaultConfig } = this 574 | let interval = Date.now() - this.endTime 575 | // 如果等于 -1 就直接停止游戏 576 | if (prizeFlag === -1) return (this.startTime = 0, void 0) 577 | if (interval > _defaultConfig.decelerationTime) { 578 | this.startTime = 0 579 | this.endCallback?.({...prizes.find((prize, index) => index === prizeFlag)}) 580 | return 581 | } 582 | this.currIndex = quad.easeOut(interval, stopIndex, endIndex, _defaultConfig.decelerationTime) % prizes.length 583 | this.draw() 584 | rAF(this.slowDown.bind(this)) 585 | } 586 | 587 | /** 588 | * 开启中奖标识自动游走 589 | */ 590 | public walk (): void { 591 | const { setInterval, clearInterval } = this.config 592 | clearInterval(this.timer) 593 | this.timer = setInterval(() => { 594 | this.currIndex += 1 595 | this.draw() 596 | }, 1300) 597 | } 598 | 599 | /** 600 | * 计算奖品格子的几何属性 601 | * @param { array } [...矩阵坐标, col, row] 602 | * @return { array } [...真实坐标, width, height] 603 | */ 604 | private getGeometricProperty ([x, y, col, row]: number[]) { 605 | const { cellWidth, cellHeight } = this 606 | const gutter = this._defaultConfig.gutter 607 | let res = [ 608 | this.prizeArea!.x + (cellWidth + gutter) * x, 609 | this.prizeArea!.y + (cellHeight + gutter) * y 610 | ] 611 | col && row && res.push( 612 | cellWidth * col + gutter * (col - 1), 613 | cellHeight * row + gutter * (row - 1), 614 | ) 615 | return res 616 | } 617 | 618 | /** 619 | * 转换并获取宽度 620 | * @param width 将要转换的宽度 621 | * @param col 横向合并的格子 622 | * @return 返回相对宽度 623 | */ 624 | private getWidth ( 625 | width: string | number | undefined, 626 | col: number = 1 627 | ): number { 628 | if (isExpectType(width, 'number')) return (width as number) 629 | if (isExpectType(width, 'string')) return this.changeUnits( 630 | width as string, 631 | this.cellWidth * col + this._defaultConfig.gutter * (col - 1) 632 | ) 633 | return 0 634 | } 635 | 636 | /** 637 | * 转换并获取高度 638 | * @param height 将要转换的高度 639 | * @param row 纵向合并的格子 640 | * @return 返回相对高度 641 | */ 642 | private getHeight ( 643 | height: string | number | undefined, 644 | row: number = 1 645 | ): number { 646 | if (isExpectType(height, 'number')) return (height as number) 647 | if (isExpectType(height, 'string')) return this.changeUnits( 648 | height as string, 649 | this.cellHeight * row + this._defaultConfig.gutter * (row - 1) 650 | ) 651 | return 0 652 | } 653 | 654 | /** 655 | * 获取相对(居中)X坐标 656 | * @param width 657 | * @param col 658 | */ 659 | private getOffsetX (width: number, col = 1): number { 660 | return (this.cellWidth * col + this._defaultConfig.gutter * (col - 1) - width) / 2 661 | } 662 | 663 | /** 664 | * 换算渲染坐标 665 | * @param x 666 | * @param y 667 | */ 668 | protected conversionAxis (x: number, y: number): [number, number] { 669 | const { config } = this 670 | return [x / config.dpr, y / config.dpr] 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /src/lib/lucky.ts: -------------------------------------------------------------------------------- 1 | import '../utils/polyfill' 2 | import { isExpectType } from '../utils/index' 3 | import { name, version } from '../../package.json' 4 | import { ConfigType, ImgType, UniImageType } from '../types/index' 5 | import { defineReactive } from '../observer' 6 | import Watcher, { WatchOptType } from '../observer/watcher' 7 | 8 | export default class Lucky { 9 | protected readonly config: ConfigType 10 | protected readonly ctx: CanvasRenderingContext2D 11 | protected htmlFontSize: number = 16 12 | protected rAF: Function = function () {} 13 | protected boxWidth: number = 0 14 | protected boxHeight: number = 0 15 | 16 | /** 17 | * 公共构造器 18 | * @param config 19 | */ 20 | constructor (config: string | HTMLDivElement | ConfigType) { 21 | // 先初始化 fontSize 以防后面有 rem 单位 22 | this.setHTMLFontSize() 23 | /* eslint-disable */ 24 | // 兼容代码开始: 为了处理 v1.0.6 版本在这里传入了一个 dom 25 | if (typeof config === 'string') config = { el: config } as ConfigType 26 | else if (config.nodeType === 1) config = { el: '', divElement: config } as ConfigType 27 | config = config as ConfigType 28 | /* eslint-enable */ 29 | this.config = config 30 | // 拿到 config 即可设置 dpr 31 | this.setDpr() 32 | // 初始化 window 方法 33 | this.initWindowFunction() 34 | if (!config.flag) config.flag = 'WEB' 35 | if (!Object.prototype.hasOwnProperty.call(config, 'ob')) config.ob = true 36 | if (config.el) config.divElement = document.querySelector(config.el) as HTMLDivElement 37 | let boxWidth = 0, boxHeight = 0 38 | // 如果存在父盒子, 就创建canvas标签 39 | if (config.divElement) { 40 | // 无论盒子内有没有canvas都执行覆盖逻辑 41 | config.canvasElement = document.createElement('canvas') 42 | config.divElement.appendChild(config.canvasElement) 43 | } 44 | // 初始化宽高 45 | this.resetWidthAndHeight() 46 | // 获取 canvas 上下文 47 | if (config.canvasElement) { 48 | config.ctx = config.canvasElement.getContext('2d')! 49 | // 添加版本信息到标签上, 方便定位版本问题 50 | config.canvasElement.setAttribute('package', `${name}@${version}`) 51 | config.canvasElement.addEventListener('click', e => this.handleClick(e)) 52 | config.canvasElement.addEventListener('mousemove', e => this.handleMouseMove(e)) 53 | config.canvasElement.addEventListener('mousedown', e => this.handleMouseDown(e)) 54 | config.canvasElement.addEventListener('mouseup', e => this.handleMouseUp(e)) 55 | } 56 | this.ctx = config.ctx as CanvasRenderingContext2D 57 | // 如果最后得不到 canvas 上下文那就无法进行绘制 58 | if (!config.ctx) { 59 | console.error('无法获取到 CanvasContext2D') 60 | return 61 | } 62 | if (!this.boxWidth || !this.boxHeight) { 63 | console.error('无法获取到宽度或高度') 64 | return 65 | } 66 | } 67 | 68 | /** 69 | * 初始化方法 70 | */ 71 | public init (willUpdateImgs?: object) { 72 | this.setDpr() 73 | this.setHTMLFontSize() 74 | this.resetWidthAndHeight() 75 | this.zoomCanvas() 76 | } 77 | 78 | /** 79 | * 鼠标点击事件 80 | * @param e 事件参数 81 | */ 82 | protected handleClick (e: MouseEvent): void {} 83 | 84 | /** 85 | * 鼠标按下事件 86 | * @param e 事件参数 87 | */ 88 | protected handleMouseDown (e: MouseEvent): void {} 89 | 90 | /** 91 | * 鼠标抬起事件 92 | * @param e 事件参数 93 | */ 94 | protected handleMouseUp (e: MouseEvent): void {} 95 | 96 | /** 97 | * 鼠标移动事件 98 | * @param e 事件参数 99 | */ 100 | protected handleMouseMove (e: MouseEvent): void {} 101 | 102 | /** 103 | * 换算坐标 104 | */ 105 | protected conversionAxis (x: number, y: number): [number, number] { 106 | return [0, 0] 107 | } 108 | 109 | /** 110 | * 设备像素比 111 | * window 环境下自动获取, 其余环境手动传入 112 | */ 113 | protected setDpr (): void { 114 | const { config } = this 115 | if (config.dpr) { 116 | // 优先使用 config 传入的 dpr 117 | } else if (window) { 118 | (window as any).dpr = config.dpr = window.devicePixelRatio || 1 119 | } else if (!config.dpr) { 120 | console.error(config, '未传入 dpr 可能会导致绘制异常') 121 | } 122 | } 123 | 124 | /** 125 | * 根标签的字体大小 126 | */ 127 | protected setHTMLFontSize (): void { 128 | if (!window) return 129 | this.htmlFontSize = +window.getComputedStyle(document.documentElement).fontSize.slice(0, -2) 130 | } 131 | 132 | /** 133 | * 重置盒子和canvas的宽高 134 | */ 135 | private resetWidthAndHeight (): void { 136 | const { config } = this 137 | // 如果是浏览器环境并且存在盒子 138 | let boxWidth = 0, boxHeight = 0 139 | if (config.divElement) { 140 | boxWidth = config.divElement.offsetWidth 141 | boxHeight = config.divElement.offsetHeight 142 | } 143 | // 如果 config 上面没有宽高, 就从 style 上面取 144 | this.boxWidth = this.getLength(config.width) || boxWidth 145 | this.boxHeight = this.getLength(config.height) || boxHeight 146 | // 重新把宽高赋给盒子 147 | if (config.divElement) { 148 | config.divElement.style.overflow = 'hidden' 149 | config.divElement.style.width = this.boxWidth + 'px' 150 | config.divElement.style.height = this.boxHeight + 'px' 151 | } 152 | } 153 | 154 | /** 155 | * 从 window 对象上获取一些方法 156 | */ 157 | private initWindowFunction (): void { 158 | const { config } = this 159 | if (window) { 160 | this.rAF = (function () { 161 | return window.requestAnimationFrame || 162 | window.webkitRequestAnimationFrame || 163 | window['mozRequestAnimationFrame'] || 164 | function (callback) { 165 | window.setTimeout(callback, 1000 / 60) 166 | } 167 | })() 168 | config.setTimeout = window.setTimeout 169 | config.setInterval = window.setInterval 170 | config.clearTimeout = window.clearTimeout 171 | config.clearInterval = window.clearInterval 172 | return 173 | } 174 | if (config.rAF) { 175 | // 优先使用帧动画 176 | this.rAF = config.rAF 177 | } else if (config.setTimeout) { 178 | // 其次使用定时器 179 | const timeout = config.setTimeout 180 | this.rAF = (callback: Function): number => timeout(callback, 16.7) 181 | } else { 182 | // 如果config里面没有提供, 那就假设全局方法存在setTimeout 183 | this.rAF = (callback: Function): number => setTimeout(callback, 16.7) 184 | } 185 | } 186 | 187 | /** 188 | * 根据 dpr 缩放 canvas 并处理位移 189 | */ 190 | protected zoomCanvas (): void { 191 | const { config, ctx } = this 192 | const { canvasElement, dpr } = config 193 | const [width, height] = [this.boxWidth * dpr, this.boxHeight * dpr] 194 | const compute = (len: number) => (len * dpr - len) / (len * dpr) * (dpr / 2) * 100 195 | if (!canvasElement) return 196 | canvasElement.width = width 197 | canvasElement.height = height 198 | canvasElement.style.width = `${width}px` 199 | canvasElement.style.height = `${height}px` 200 | canvasElement.style.transform = `scale(${1 / dpr}) translate(${-compute(width)}%, ${-compute(height)}%)` 201 | ctx.scale(dpr, dpr) 202 | } 203 | 204 | /** 205 | * 异步加载图片并返回图片的几何信息 206 | * @param src 图片路径 207 | * @param info 图片信息 208 | */ 209 | protected loadImg ( 210 | src: string, 211 | info: ImgType, 212 | resolveName = '$resolve' 213 | ): Promise { 214 | return new Promise((resolve, reject) => { 215 | if (!src) reject(`=> '${info.src}' 不能为空或不合法`) 216 | if (this.config.flag === 'WEB') { 217 | let imgObj = new Image() 218 | imgObj.src = src 219 | imgObj.onload = () => resolve(imgObj) 220 | imgObj.onerror = () => reject(`=> '${info.src}' 图片加载失败`) 221 | } else { 222 | // 其余平台向外暴露, 交给外部自行处理 223 | info[resolveName] = resolve 224 | return 225 | } 226 | }) 227 | } 228 | 229 | /** 230 | * 公共绘制图片的方法 231 | * @param imgObj 图片对象 232 | * @param xAxis x轴位置 233 | * @param yAxis y轴位置 234 | * @param width 渲染宽度 235 | * @param height 渲染高度 236 | */ 237 | protected drawImage ( 238 | imgObj: HTMLImageElement | UniImageType, 239 | xAxis: number, 240 | yAxis: number, 241 | width: number, 242 | height: number 243 | ): void { 244 | let drawImg, { config, ctx } = this 245 | if (['WEB', 'MP-WX'].includes(config.flag)) { 246 | // 浏览器中直接绘制标签即可 247 | drawImg = imgObj 248 | } else if (['UNI-H5', 'UNI-MP', 'TARO-H5', 'TARO-MP'].includes(config.flag)) { 249 | // 小程序中直接绘制一个路径 250 | drawImg = (imgObj as UniImageType).path 251 | } 252 | return ctx.drawImage((drawImg as CanvasImageSource), xAxis, yAxis, width, height) 253 | } 254 | 255 | /** 256 | * 获取长度 257 | * @param length 将要转换的长度 258 | * @return 返回长度 259 | */ 260 | protected getLength (length: string | number | undefined): number { 261 | if (isExpectType(length, 'number')) return length as number 262 | if (isExpectType(length, 'string')) return this.changeUnits(length as string) 263 | return 0 264 | } 265 | 266 | /** 267 | * 转换单位 268 | * @param { string } value 将要转换的值 269 | * @param { number } denominator 分子 270 | * @return { number } 返回新的字符串 271 | */ 272 | protected changeUnits (value: string, denominator = 1): number { 273 | return Number(value.replace(/^([-]*[0-9.]*)([a-z%]*)$/, (value, num, unit) => { 274 | const unitFunc = { 275 | '%': (n: number) => n * (denominator / 100), 276 | 'px': (n: number) => n * 1, 277 | 'rem': (n: number) => n * this.htmlFontSize, 278 | }[unit] 279 | if (unitFunc) return unitFunc(num) 280 | // 如果找不到默认单位, 就交给外面处理 281 | const otherUnitFunc = this.config.unitFunc 282 | return otherUnitFunc ? otherUnitFunc(num, unit) : num 283 | })) 284 | } 285 | 286 | /** 287 | * 添加一个新的响应式数据 (临时) 288 | * @param data 数据 289 | * @param key 属性 290 | * @param value 新值 291 | */ 292 | public $set (data: object, key: string | number, value: any) { 293 | if (!data || typeof data !== 'object') return 294 | defineReactive(data, key, value) 295 | } 296 | 297 | /** 298 | * 添加一个属性计算 (临时) 299 | * @param data 源数据 300 | * @param key 属性名 301 | * @param callback 回调函数 302 | */ 303 | protected $computed (data: object, key: string, callback: Function) { 304 | Object.defineProperty(data, key, { 305 | get: () => { 306 | return callback.call(this) 307 | } 308 | }) 309 | } 310 | 311 | /** 312 | * 添加一个观察者 create user watcher 313 | * @param expr 表达式 314 | * @param handler 回调函数 315 | * @param watchOpt 配置参数 316 | * @return 卸载当前观察者的函数 (暂未返回) 317 | */ 318 | protected $watch ( 319 | expr: string | Function, 320 | handler: Function | WatchOptType, 321 | watchOpt: WatchOptType = {} 322 | ): Function { 323 | if (typeof handler === 'object') { 324 | watchOpt = handler 325 | handler = watchOpt.handler! 326 | } 327 | // 创建 user watcher 328 | const watcher = new Watcher(this, expr, handler, watchOpt) 329 | // 判断是否需要初始化时触发回调 330 | if (watchOpt.immediate) { 331 | handler.call(this, watcher.value) 332 | } 333 | // 返回一个卸载当前观察者的函数 334 | return function unWatchFn () {} 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/lib/wheel.ts: -------------------------------------------------------------------------------- 1 | import Lucky from './lucky' 2 | import { ConfigType } from '../types/index' 3 | import LuckyWheelConfig, { 4 | BlockType, BlockImgType, 5 | PrizeType, PrizeImgType, 6 | ButtonType, ButtonImgType, 7 | DefaultConfigType, 8 | DefaultStyleType, 9 | StartCallbackType, 10 | EndCallbackType 11 | } from '../types/wheel' 12 | import { FontType, ImgType, UniImageType } from '../types/index' 13 | import { isExpectType, removeEnter, hasBackground } from '../utils/index' 14 | import { getAngle, drawSector } from '../utils/math' 15 | import * as Tween from '../utils/tween' 16 | 17 | export default class LuckyWheel extends Lucky { 18 | private blocks: Array = [] 19 | private prizes: Array = [] 20 | private buttons: Array = [] 21 | private defaultConfig: DefaultConfigType = {} 22 | private _defaultConfig = { 23 | gutter: '0px', 24 | offsetDegree: 0, 25 | speed: 20, 26 | speedFunction: 'quad', 27 | accelerationTime: 2500, 28 | decelerationTime: 2500, 29 | stopRange: 0.8, 30 | } 31 | private defaultStyle: DefaultStyleType = {} 32 | private _defaultStyle = { 33 | fontSize: '18px', 34 | fontColor: '#000', 35 | fontStyle: 'sans-serif', 36 | fontWeight: '400', 37 | lineHeight: '', 38 | background: 'rgba(0,0,0,0)', 39 | wordWrap: true, 40 | lengthLimit: '90%', 41 | } 42 | private startCallback?: StartCallbackType 43 | private endCallback?: EndCallbackType 44 | private Radius = 0 // 大转盘半径 45 | private prizeRadius = 0 // 奖品区域半径 46 | private prizeDeg = 0 // 奖品数学角度 47 | private prizeRadian = 0 // 奖品运算角度 48 | private rotateDeg = 0 // 转盘旋转角度 49 | private maxBtnRadius = 0 // 最大按钮半径 50 | private startTime = 0 // 开始时间戳 51 | private endTime = 0 // 停止时间戳 52 | private stopDeg = 0 // 刻舟求剑 53 | private endDeg = 0 // 停止角度 54 | private FPS = 16.6 // 屏幕刷新率 55 | /** 56 | * 中奖索引 57 | * prizeFlag = undefined 时, 处于开始抽奖阶段, 正常旋转 58 | * prizeFlag >= 0 时, 说明stop方法被调用, 并且传入了中奖索引 59 | * prizeFlag === -1 时, 说明stop方法被调用, 并且传入了负值, 本次抽奖无效 60 | */ 61 | private prizeFlag: number | undefined 62 | private blockImgs: Array = [[]] 63 | private prizeImgs: Array = [[]] 64 | private btnImgs: Array = [[]] 65 | 66 | /** 67 | * 大转盘构造器 68 | * @param config 元素标识 69 | * @param data 抽奖配置项 70 | */ 71 | constructor (config: ConfigType, data: LuckyWheelConfig = {}) { 72 | super(config) 73 | if (config.ob) { 74 | this.initData(data) 75 | this.initWatch() 76 | } 77 | this.initComputed() 78 | // 创建前回调函数 79 | config.beforeCreate?.call(this) 80 | // 收集首次渲染的图片 81 | this.init({ 82 | blockImgs: this.blocks.map(block => block.imgs), 83 | prizeImgs: this.prizes.map(prize => prize.imgs), 84 | btnImgs: this.buttons.map(btn => btn.imgs), 85 | }) 86 | } 87 | 88 | /** 89 | * 初始化数据 90 | * @param data 91 | */ 92 | private initData (data: LuckyWheelConfig): void { 93 | this.$set(this, 'blocks', data.blocks || []) 94 | this.$set(this, 'prizes', data.prizes || []) 95 | this.$set(this, 'buttons', data.buttons || []) 96 | this.$set(this, 'defaultConfig', data.defaultConfig || {}) 97 | this.$set(this, 'defaultStyle', data.defaultStyle || {}) 98 | this.$set(this, 'startCallback', data.start) 99 | this.$set(this, 'endCallback', data.end) 100 | } 101 | 102 | /** 103 | * 初始化属性计算 104 | */ 105 | private initComputed () { 106 | // 默认配置 107 | this.$computed(this, '_defaultConfig', () => { 108 | const config = { 109 | gutter: '0px', 110 | offsetDegree: 0, 111 | speed: 20, 112 | speedFunction: 'quad', 113 | accelerationTime: 2500, 114 | decelerationTime: 2500, 115 | stopRange: 0.8, 116 | ...this.defaultConfig 117 | } 118 | return config 119 | }) 120 | // 默认样式 121 | this.$computed(this, '_defaultStyle', () => { 122 | const style = { 123 | fontSize: '18px', 124 | fontColor: '#000', 125 | fontStyle: 'sans-serif', 126 | fontWeight: '400', 127 | background: 'rgba(0,0,0,0)', 128 | wordWrap: true, 129 | lengthLimit: '90%', 130 | ...this.defaultStyle 131 | } 132 | return style 133 | }) 134 | } 135 | 136 | /** 137 | * 初始化观察者 138 | */ 139 | private initWatch () { 140 | // 观察 blocks 变化收集图片 141 | this.$watch('blocks', (newData: Array) => { 142 | return this.init({ blockImgs: newData.map(cell => cell.imgs) }) 143 | }, { deep: true }) 144 | // 观察 prizes 变化收集图片 145 | this.$watch('prizes', (newData: Array) => { 146 | return this.init({ prizeImgs: newData.map(cell => cell.imgs) }) 147 | }, { deep: true }) 148 | // 观察 buttons 变化收集图片 149 | this.$watch('buttons', (newData: Array) => { 150 | return this.init({ btnImgs: newData.map(cell => cell.imgs) }) 151 | }, { deep: true }) 152 | this.$watch('defaultConfig', () => this.draw(), { deep: true }) 153 | this.$watch('defaultStyle', () => this.draw(), { deep: true }) 154 | this.$watch('startCallback', () => this.init({})) 155 | this.$watch('endCallback', () => this.init({})) 156 | } 157 | 158 | /** 159 | * 初始化 canvas 抽奖 160 | * @param { willUpdateImgs } willUpdateImgs 需要更新的图片 161 | */ 162 | public init (willUpdateImgs: { 163 | blockImgs?: Array 164 | prizeImgs?: Array 165 | btnImgs?: Array 166 | }): void { 167 | super.init() 168 | const { config, ctx } = this 169 | this.Radius = Math.min(this.boxWidth, this.boxHeight) / 2 170 | // 初始化前回调函数 171 | config.beforeInit?.call(this) 172 | ctx.translate(this.Radius, this.Radius) 173 | this.draw() // 先画一次, 防止闪烁 174 | this.draw() // 再画一次, 拿到正确的按钮轮廓 175 | // 异步加载图片 176 | Object.keys(willUpdateImgs).forEach(key => { 177 | const imgName = key as 'blockImgs' | 'prizeImgs' | 'btnImgs' 178 | const cellName = { 179 | blockImgs: 'blocks', 180 | prizeImgs: 'prizes', 181 | btnImgs: 'buttons', 182 | }[imgName] as 'blocks' | 'prizes' | 'buttons' 183 | const willUpdate = willUpdateImgs[imgName] 184 | willUpdate && willUpdate.forEach((imgs, cellIndex) => { 185 | imgs && imgs.forEach((imgInfo, imgIndex) => { 186 | this.loadAndCacheImg(cellName, cellIndex, imgName, imgIndex, () => { 187 | this.draw() 188 | }) 189 | }) 190 | }) 191 | }) 192 | // 初始化后回调函数 193 | config.afterInit?.call(this) 194 | } 195 | 196 | /** 197 | * canvas点击事件 198 | * @param e 事件参数 199 | */ 200 | protected handleClick (e: MouseEvent): void { 201 | const { ctx } = this 202 | ctx.beginPath() 203 | ctx.arc(0, 0, this.maxBtnRadius, 0, Math.PI * 2, false) 204 | if (!ctx.isPointInPath(e.offsetX, e.offsetY)) return 205 | if (this.startTime) return 206 | this.startCallback?.(e) 207 | } 208 | 209 | /** 210 | * 单独加载某一张图片并计算其实际渲染宽高 211 | * @param { number } cellIndex 奖品索引 212 | * @param { number } imgIndex 奖品图片索引 213 | * @param { Function } callBack 图片加载完毕回调 214 | */ 215 | private async loadAndCacheImg ( 216 | cellName: 'blocks' | 'prizes' | 'buttons', 217 | cellIndex: number, 218 | imgName: 'blockImgs' | 'prizeImgs' | 'btnImgs', 219 | imgIndex: number, 220 | callBack: () => void 221 | ) { 222 | // 获取图片信息 223 | const cell: BlockType | PrizeType | ButtonType = this[cellName][cellIndex] 224 | if (!cell || !cell.imgs) return 225 | const imgInfo = cell.imgs[imgIndex] 226 | if (!imgInfo) return 227 | if (!this[imgName][cellIndex]) this[imgName][cellIndex] = [] 228 | // 异步加载图片 229 | this.loadImg(imgInfo.src, imgInfo).then(res => { 230 | this[imgName][cellIndex][imgIndex] = res 231 | callBack.call(this) 232 | }).catch(err => { 233 | console.error(`${cellName}[${cellIndex}].imgs[${imgIndex}] ${err}`) 234 | }) 235 | } 236 | 237 | /** 238 | * 计算图片的渲染宽高 239 | * @param imgObj 图片标签元素 240 | * @param imgInfo 图片信息 241 | * @param maxWidth 最大宽度 242 | * @param maxHeight 最大高度 243 | * @return [渲染宽度, 渲染高度] 244 | */ 245 | private computedWidthAndHeight ( 246 | imgObj: HTMLImageElement | UniImageType, 247 | imgInfo: ImgType, 248 | maxWidth: number, 249 | maxHeight: number 250 | ): [number, number] { 251 | // 根据配置的样式计算图片的真实宽高 252 | if (!imgInfo.width && !imgInfo.height) { 253 | // 如果没有配置宽高, 则使用图片本身的宽高 254 | return [imgObj.width, imgObj.height] 255 | } else if (imgInfo.width && !imgInfo.height) { 256 | // 如果只填写了宽度, 没填写高度 257 | let trueWidth = this.getWidth(imgInfo.width, maxWidth) 258 | // 那高度就随着宽度进行等比缩放 259 | return [trueWidth, imgObj.height * (trueWidth / imgObj.width)] 260 | } else if (!imgInfo.width && imgInfo.height) { 261 | // 如果只填写了宽度, 没填写高度 262 | let trueHeight = this.getHeight(imgInfo.height, maxHeight) 263 | // 那宽度就随着高度进行等比缩放 264 | return [imgObj.width * (trueHeight / imgObj.height), trueHeight] 265 | } 266 | // 如果宽度和高度都填写了, 就如实计算 267 | return [ 268 | this.getWidth(imgInfo.width, maxWidth), 269 | this.getHeight(imgInfo.height, maxHeight) 270 | ] 271 | } 272 | 273 | /** 274 | * 开始绘制 275 | */ 276 | protected draw (): void { 277 | const { config, ctx, _defaultConfig, _defaultStyle } = this 278 | // 触发绘制前回调 279 | config.beforeDraw?.call(this, ctx) 280 | // 清空画布 281 | ctx.clearRect(-this.Radius, -this.Radius, this.Radius * 2, this.Radius * 2) 282 | // 绘制blocks边框 283 | this.prizeRadius = this.blocks.reduce((radius, block, blockIndex) => { 284 | if (hasBackground(block.background)) { 285 | ctx.beginPath() 286 | ctx.fillStyle = block.background! 287 | ctx.arc(0, 0, radius, 0, Math.PI * 2, false) 288 | ctx.fill() 289 | } 290 | block.imgs && block.imgs.forEach((imgInfo, imgIndex) => { 291 | if (!this.blockImgs[blockIndex]) return 292 | const blockImg = this.blockImgs[blockIndex][imgIndex] 293 | if (!blockImg) return 294 | // 绘制图片 295 | const [trueWidth, trueHeight] = this.computedWidthAndHeight(blockImg, imgInfo, radius * 2, radius * 2) 296 | const [xAxis, yAxis] = [this.getOffsetX(trueWidth), this.getHeight(imgInfo.top, radius * 2) - radius] 297 | ctx.save() 298 | imgInfo.rotate && ctx.rotate(getAngle(this.rotateDeg)) 299 | this.drawImage(blockImg, xAxis, yAxis, trueWidth, trueHeight) 300 | ctx.restore() 301 | }) 302 | return radius - this.getLength(block.padding && block.padding.split(' ')[0]) 303 | }, this.Radius) 304 | // 计算起始弧度 305 | this.prizeDeg = 360 / this.prizes.length 306 | this.prizeRadian = getAngle(this.prizeDeg) 307 | let start = getAngle(-90 + this.rotateDeg + _defaultConfig.offsetDegree) 308 | // 计算文字横坐标 309 | const getFontX = (line: string) => { 310 | return this.getOffsetX(ctx.measureText(line).width) 311 | } 312 | // 计算文字纵坐标 313 | const getFontY = (font: FontType, height: number, lineIndex: number) => { 314 | // 优先使用字体行高, 要么使用默认行高, 其次使用字体大小, 否则使用默认字体大小 315 | const lineHeight = font.lineHeight || _defaultStyle.lineHeight || font.fontSize || _defaultStyle.fontSize 316 | return this.getHeight(font.top, height) + (lineIndex + 1) * this.getLength(lineHeight) 317 | } 318 | ctx.save() 319 | // 绘制prizes奖品区域 320 | this.prizes.forEach((prize, prizeIndex) => { 321 | // 计算当前奖品区域中间坐标点 322 | let currMiddleDeg = start + prizeIndex * this.prizeRadian 323 | // 奖品区域可见高度 324 | let prizeHeight = this.prizeRadius - this.maxBtnRadius 325 | // 绘制背景 326 | const background = prize.background || _defaultStyle.background 327 | hasBackground(background) && drawSector( 328 | config.flag, ctx, 329 | this.maxBtnRadius, this.prizeRadius, 330 | currMiddleDeg - this.prizeRadian / 2, 331 | currMiddleDeg + this.prizeRadian / 2, 332 | this.getLength(_defaultConfig.gutter), 333 | background 334 | ) 335 | // 计算临时坐标并旋转文字 336 | let x = Math.cos(currMiddleDeg) * this.prizeRadius 337 | let y = Math.sin(currMiddleDeg) * this.prizeRadius 338 | ctx.translate(x, y) 339 | ctx.rotate(currMiddleDeg + getAngle(90)) 340 | // 绘制图片 341 | prize.imgs && prize.imgs.forEach((imgInfo, imgIndex) => { 342 | if (!this.prizeImgs[prizeIndex]) return 343 | const prizeImg = this.prizeImgs[prizeIndex][imgIndex] 344 | if (!prizeImg) return 345 | const [trueWidth, trueHeight] = this.computedWidthAndHeight( 346 | prizeImg, 347 | imgInfo, 348 | this.prizeRadian * this.prizeRadius, 349 | prizeHeight 350 | ) 351 | const [xAxis, yAxis] = [this.getOffsetX(trueWidth), this.getHeight(imgInfo.top, prizeHeight)] 352 | this.drawImage(prizeImg, xAxis, yAxis, trueWidth, trueHeight) 353 | }) 354 | // 逐行绘制文字 355 | prize.fonts && prize.fonts.forEach(font => { 356 | let fontColor = font.fontColor || _defaultStyle.fontColor 357 | let fontWeight = font.fontWeight || _defaultStyle.fontWeight 358 | let fontSize = this.getLength(font.fontSize || _defaultStyle.fontSize) 359 | let fontStyle = font.fontStyle || _defaultStyle.fontStyle 360 | ctx.fillStyle = fontColor! 361 | ctx.font = `${fontWeight} ${fontSize >> 0}px ${fontStyle}` 362 | let lines = [], text = String(font.text) 363 | if (Object.prototype.hasOwnProperty.call(font, 'wordWrap') ? font.wordWrap : _defaultStyle.wordWrap) { 364 | text = removeEnter(text) 365 | let str = '' 366 | for (let i = 0; i < text.length; i++) { 367 | str += text[i] 368 | let currWidth = ctx.measureText(str).width 369 | let maxWidth = (this.prizeRadius - getFontY(font, prizeHeight, lines.length)) 370 | * Math.tan(this.prizeRadian / 2) * 2 - this.getLength(_defaultConfig.gutter) 371 | if (currWidth > this.getWidth(font.lengthLimit || _defaultStyle.lengthLimit, maxWidth)) { 372 | lines.push(str.slice(0, -1)) 373 | str = text[i] 374 | } 375 | } 376 | if (str) lines.push(str) 377 | if (!lines.length) lines.push(text) 378 | } else { 379 | lines = text.split('\n') 380 | } 381 | lines.filter(line => !!line).forEach((line, lineIndex) => { 382 | ctx.fillText(line, getFontX(line), getFontY(font, prizeHeight, lineIndex)) 383 | }) 384 | }) 385 | // 修正旋转角度和原点坐标 386 | ctx.rotate(getAngle(360) - currMiddleDeg - getAngle(90)) 387 | ctx.translate(-x, -y) 388 | }) 389 | ctx.restore() 390 | // 绘制按钮 391 | this.buttons.forEach((btn, btnIndex) => { 392 | let radius = this.getHeight(btn.radius) 393 | // 绘制背景颜色 394 | this.maxBtnRadius = Math.max(this.maxBtnRadius, radius) 395 | if (hasBackground(btn.background)) { 396 | ctx.beginPath() 397 | ctx.fillStyle = btn.background as string 398 | ctx.arc(0, 0, radius, 0, Math.PI * 2, false) 399 | ctx.fill() 400 | } 401 | // 绘制指针 402 | if (btn.pointer && hasBackground(btn.background)) { 403 | ctx.beginPath() 404 | ctx.fillStyle = btn.background as string 405 | ctx.moveTo(-radius, 0) 406 | ctx.lineTo(radius, 0) 407 | ctx.lineTo(0, -radius * 2) 408 | ctx.closePath() 409 | ctx.fill() 410 | } 411 | // 绘制按钮图片 412 | btn.imgs && btn.imgs.forEach((imgInfo, imgIndex) => { 413 | if (!this.btnImgs[btnIndex]) return 414 | const btnImg = this.btnImgs[btnIndex][imgIndex] 415 | if (!btnImg) return 416 | const [trueWidth, trueHeight] = this.computedWidthAndHeight(btnImg, imgInfo, radius * 2, radius * 2) 417 | const [xAxis, yAxis] = [this.getOffsetX(trueWidth), this.getHeight(imgInfo.top, radius)] 418 | this.drawImage(btnImg, xAxis, yAxis, trueWidth, trueHeight) 419 | }) 420 | // 绘制按钮文字 421 | btn.fonts && btn.fonts.forEach(font => { 422 | let fontColor = font.fontColor || _defaultStyle.fontColor 423 | let fontWeight = font.fontWeight || _defaultStyle.fontWeight 424 | let fontSize = this.getLength(font.fontSize || _defaultStyle.fontSize) 425 | let fontStyle = font.fontStyle || _defaultStyle.fontStyle 426 | ctx.fillStyle = fontColor! 427 | ctx.font = `${fontWeight} ${fontSize >> 0}px ${fontStyle}` 428 | String(font.text).split('\n').forEach((line, lineIndex) => { 429 | ctx.fillText(line, getFontX(line), getFontY(font, radius, lineIndex)) 430 | }) 431 | }) 432 | }) 433 | // 触发绘制后回调 434 | config.afterDraw?.call(this, ctx) 435 | } 436 | 437 | /** 438 | * 对外暴露: 开始抽奖方法 439 | */ 440 | public play (): void { 441 | // 再次拦截, 因为play是可以异步调用的 442 | if (this.startTime) return 443 | this.startTime = Date.now() 444 | this.prizeFlag = void 0 445 | this.run() 446 | } 447 | 448 | /** 449 | * 对外暴露: 缓慢停止方法 450 | * @param index 中奖索引 451 | */ 452 | public stop (index: number): void { 453 | // 判断 prizeFlag 是否等于 -1 454 | this.prizeFlag = index < 0 ? -1 : index % this.prizes.length 455 | // 如果是 -1 就初始化状态 456 | if (this.prizeFlag === -1) { 457 | this.rotateDeg = this.prizeDeg / 2 - this._defaultConfig.offsetDegree 458 | this.draw() 459 | } 460 | } 461 | 462 | /** 463 | * 实际开始执行方法 464 | * @param num 记录帧动画执行多少次 465 | */ 466 | private run (num: number = 0): void { 467 | const { rAF, prizeFlag, prizeDeg, rotateDeg, _defaultConfig } = this 468 | if (prizeFlag === -1) return (this.startTime = 0, void 0) 469 | let interval = Date.now() - this.startTime 470 | // 先完全旋转, 再停止 471 | if (interval >= _defaultConfig.accelerationTime && prizeFlag !== void 0) { 472 | // 记录帧率 473 | this.FPS = interval / num 474 | // 记录开始停止的时间戳 475 | this.endTime = Date.now() 476 | // 记录开始停止的位置 477 | this.stopDeg = rotateDeg 478 | // 停止范围 479 | const stopRange = (Math.random() * prizeDeg - prizeDeg / 2) * this.getLength(_defaultConfig.stopRange) 480 | // 测算最终停止的角度 481 | let i = 0 482 | while (++i) { 483 | const endDeg = 360 * i - prizeFlag * prizeDeg - rotateDeg - _defaultConfig.offsetDegree + stopRange 484 | let currSpeed = Tween[_defaultConfig.speedFunction].easeOut(this.FPS, this.stopDeg, endDeg, _defaultConfig.decelerationTime) - this.stopDeg 485 | if (currSpeed > _defaultConfig.speed) { 486 | this.endDeg = endDeg 487 | break 488 | } 489 | } 490 | return this.slowDown() 491 | } 492 | this.rotateDeg = (rotateDeg + Tween[_defaultConfig.speedFunction].easeIn(interval, 0, _defaultConfig.speed, _defaultConfig.accelerationTime)) % 360 493 | this.draw() 494 | rAF(this.run.bind(this, num + 1)) 495 | } 496 | 497 | /** 498 | * 缓慢停止的方法 499 | */ 500 | private slowDown (): void { 501 | const { rAF, prizes, prizeFlag, stopDeg, endDeg, _defaultConfig } = this 502 | let interval = Date.now() - this.endTime 503 | if (prizeFlag === -1) return (this.startTime = 0, void 0) 504 | if (interval >= _defaultConfig.decelerationTime) { 505 | this.startTime = 0 506 | this.endCallback?.({...prizes.find((prize, index) => index === prizeFlag)}) 507 | return 508 | } 509 | this.rotateDeg = Tween[_defaultConfig.speedFunction].easeOut(interval, stopDeg, endDeg, _defaultConfig.decelerationTime) % 360 510 | this.draw() 511 | rAF(this.slowDown.bind(this)) 512 | } 513 | 514 | /** 515 | * 获取相对宽度 516 | * @param length 将要转换的宽度 517 | * @param width 宽度计算百分比 518 | * @return 返回相对宽度 519 | */ 520 | private getWidth ( 521 | length: string | number | undefined, 522 | width = this.prizeRadian * this.prizeRadius 523 | ): number { 524 | if (isExpectType(length, 'number')) return (length as number) 525 | if (isExpectType(length, 'string')) return this.changeUnits(length as string, width) 526 | return 0 527 | } 528 | 529 | /** 530 | * 获取相对高度 531 | * @param length 将要转换的高度 532 | * @param height 高度计算百分比 533 | * @return 返回相对高度 534 | */ 535 | private getHeight ( 536 | length: string | number | undefined, 537 | height: number = this.prizeRadius 538 | ): number { 539 | if (isExpectType(length, 'number')) return (length as number) 540 | if (isExpectType(length, 'string')) return this.changeUnits(length as string, height) 541 | return 0 542 | } 543 | 544 | /** 545 | * 获取相对(居中)X坐标 546 | * @param width 547 | * @return 返回x坐标 548 | */ 549 | private getOffsetX (width: number): number { 550 | return -width / 2 551 | } 552 | 553 | /** 554 | * 换算渲染坐标 555 | * @param x 556 | * @param y 557 | */ 558 | protected conversionAxis (x: number, y: number): [number, number] { 559 | const { config } = this 560 | return [x / config.dpr - this.Radius, y / config.dpr - this.Radius] 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /src/observer/array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 重写数组的原型方法 3 | */ 4 | const oldArrayProto = Array.prototype 5 | const newArrayProto = Object.create(oldArrayProto) 6 | const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'] 7 | methods.forEach(method => { 8 | newArrayProto[method] = function (...args: any[]) { 9 | const res = oldArrayProto[method].apply(this, args) 10 | const luckyOb = this['__luckyOb__'] 11 | if (['push', 'unshift', 'splice'].includes(method)) luckyOb.walk(this) 12 | luckyOb.dep.notify() 13 | return res 14 | } 15 | }) 16 | 17 | export { newArrayProto } 18 | -------------------------------------------------------------------------------- /src/observer/dep.ts: -------------------------------------------------------------------------------- 1 | import Watcher from './watcher' 2 | 3 | export default class Dep { 4 | static target: Watcher | null 5 | private subs: Array 6 | 7 | /** 8 | * 订阅中心构造器 9 | */ 10 | constructor () { 11 | this.subs = [] 12 | } 13 | 14 | /** 15 | * 收集依赖 16 | * @param {*} sub 17 | */ 18 | public addSub (sub: Watcher) { 19 | // 此处临时使用includes防重复添加 20 | if (!this.subs.includes(sub)) { 21 | this.subs.push(sub) 22 | } 23 | } 24 | 25 | /** 26 | * 派发更新 27 | */ 28 | public notify () { 29 | this.subs.forEach(sub => { 30 | sub.update() 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/observer/index.ts: -------------------------------------------------------------------------------- 1 | import Dep from './dep' 2 | import { hasProto, def } from './utils' 3 | import { newArrayProto } from './array' 4 | 5 | export default class Observer { 6 | value: any 7 | dep: Dep 8 | 9 | /** 10 | * 观察者构造器 11 | * @param value 12 | */ 13 | constructor (value: any) { 14 | // this.value = value 15 | this.dep = new Dep() 16 | // 将响应式对象代理到当前value上面, 并且将当前的enumerable设置为false 17 | def(value, '__luckyOb__', this) 18 | if (Array.isArray(value)) { // 如果是数组, 则重写原型方法 19 | if (hasProto) { 20 | value['__proto__'] = newArrayProto 21 | } else { 22 | Object.getOwnPropertyNames(newArrayProto).forEach(key => { 23 | def(value, key, newArrayProto[key]) 24 | }) 25 | } 26 | } 27 | this.walk(value) 28 | } 29 | 30 | walk (data: object | any[]) { 31 | Object.keys(data).forEach(key => { 32 | defineReactive(data, key, data[key]) 33 | }) 34 | } 35 | } 36 | 37 | /** 38 | * 处理响应式 39 | * @param { Object | Array } data 40 | */ 41 | export function observe (data: any): Observer | void { 42 | if (!data || typeof data !== 'object') return 43 | let luckyOb: Observer | void 44 | if ('__luckyOb__' in data) { 45 | luckyOb = data['__luckyOb__'] 46 | } else { 47 | luckyOb = new Observer(data) 48 | } 49 | return luckyOb 50 | } 51 | 52 | /** 53 | * 重写 setter / getter 54 | * @param {*} data 55 | * @param {*} key 56 | * @param {*} val 57 | */ 58 | export function defineReactive (data: any, key: string | number, val: any) { 59 | const dep = new Dep() 60 | const property = Object.getOwnPropertyDescriptor(data, key) 61 | if (property && property.configurable === false) { 62 | return 63 | } 64 | const getter = property && property.get 65 | const setter = property && property.set 66 | if ((!getter || setter) && arguments.length === 2) { 67 | val = data[key] 68 | } 69 | let childOb = observe(val) 70 | Object.defineProperty(data, key, { 71 | get: () => { 72 | const value = getter ? getter.call(data) : val 73 | if (Dep.target) { 74 | dep.addSub(Dep.target) 75 | if (childOb) { 76 | childOb.dep.addSub(Dep.target) 77 | } 78 | } 79 | return value 80 | }, 81 | set: (newVal) => { 82 | if (newVal === val) return 83 | val = newVal 84 | if (getter && !setter) return 85 | if (setter) { 86 | setter.call(data, newVal) 87 | } else { 88 | val = newVal 89 | } 90 | childOb = observe(newVal) 91 | dep.notify() 92 | } 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/observer/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { isExpectType } from '../utils' 3 | 4 | export const hasProto = '__proto__' in {} 5 | 6 | export function def (obj: object, key: string | number, val: any, enumerable?: boolean) { 7 | Object.defineProperty(obj, key, { 8 | value: val, 9 | enumerable: !!enumerable, 10 | writable: true, 11 | configurable: true 12 | }) 13 | } 14 | 15 | export function parsePath (path: string) { 16 | path += '.' 17 | let segments: string[] = [], segment = '' 18 | for (let i = 0; i < path.length; i++) { 19 | let curr = path[i] 20 | if (/\[|\./.test(curr)) { 21 | segments.push(segment) 22 | segment = '' 23 | } else if (/\W/.test(curr)) { 24 | continue 25 | } else { 26 | segment += curr 27 | } 28 | } 29 | return function (data: object | any[]) { 30 | return segments.reduce((data, key) => { 31 | return data[key] 32 | }, data) 33 | } 34 | } 35 | 36 | export function traverse (value: any) { 37 | // const seenObjects = new Set() 38 | const dfs = (data: any) => { 39 | if (!isExpectType(data, 'array', 'object')) return 40 | Object.keys(data).forEach(key => { 41 | const value = data[key] 42 | dfs(value) 43 | }) 44 | } 45 | dfs(value) 46 | // seenObjects.clear() 47 | } -------------------------------------------------------------------------------- /src/observer/watcher.ts: -------------------------------------------------------------------------------- 1 | import Lucky from '../lib/lucky' 2 | import Dep from './dep' 3 | import { parsePath, traverse } from './utils' 4 | 5 | export interface WatchOptType { 6 | handler?: () => Function 7 | immediate?: boolean 8 | deep?: boolean 9 | } 10 | 11 | let uid = 0 12 | export default class Watcher { 13 | id: number 14 | $lucky: Lucky 15 | expr: string | Function 16 | cb: Function 17 | deep: boolean 18 | getter: Function 19 | value: any 20 | 21 | /** 22 | * 观察者构造器 23 | * @param {*} $lucky 24 | * @param {*} expr 25 | * @param {*} cb 26 | */ 27 | constructor ($lucky: Lucky, expr: string | Function, cb: Function, options: WatchOptType = {}) { 28 | this.id = uid++ 29 | this.$lucky = $lucky 30 | this.expr = expr 31 | this.deep = !!options.deep 32 | if (typeof expr === 'function') { 33 | this.getter = expr 34 | } else { 35 | this.getter = parsePath(expr) 36 | } 37 | this.cb = cb 38 | this.value = this.get() 39 | } 40 | 41 | /** 42 | * 根据表达式获取新值 43 | */ 44 | get () { 45 | Dep.target = this 46 | const value = this.getter.call(this.$lucky, this.$lucky) 47 | // 处理深度监听 48 | if (this.deep) { 49 | traverse(value) 50 | } 51 | Dep.target = null 52 | return value 53 | } 54 | 55 | /** 56 | * 触发 watcher 更新 57 | */ 58 | update () { 59 | // get获取新值 60 | const newVal = this.get() 61 | // 读取之前存储的旧值 62 | const oldVal = this.value 63 | this.value = newVal 64 | // 触发 watch 回调 65 | this.cb.call(this.$lucky, newVal, oldVal) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/types/card.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FontType, 3 | ImgType, 4 | BackgroundType 5 | } from './index' 6 | 7 | export interface MaskType { 8 | background?: BackgroundType 9 | fonts?: Array 10 | imgs?: Array 11 | } 12 | 13 | export interface WatermarkType { 14 | margin?: string 15 | rotate?: number 16 | opacity?: number 17 | text?: FontType['text'] 18 | fontColor?: FontType['fontColor'] 19 | fontSize?: FontType['fontSize'] 20 | fontStyle?: FontType['fontStyle'] 21 | fontWeight?: FontType['fontWeight'] 22 | } 23 | 24 | export interface DefaultConfigType { 25 | percent?: string | number 26 | cleanZone?: { 27 | x: string | number 28 | y: string | number 29 | width: string | number 30 | height: string | number 31 | } 32 | } 33 | 34 | export type StartCallbackType = () => void 35 | export type EndCallbackType = () => void 36 | 37 | export default interface LuckyCardConfig { 38 | mask?: MaskType 39 | watermark?: WatermarkType 40 | start?: StartCallbackType 41 | end?: EndCallbackType 42 | } 43 | -------------------------------------------------------------------------------- /src/types/grid.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FontType, 3 | ImgType, 4 | BorderRadiusType, 5 | BackgroundType, 6 | ShadowType 7 | } from './index' 8 | 9 | export interface PrizeFontType extends FontType { 10 | wordWrap?: boolean 11 | lengthLimit?: string | number 12 | } 13 | 14 | export interface ButtonFontType extends FontType { 15 | wordWrap?: boolean 16 | lengthLimit?: string | number 17 | } 18 | 19 | export type CellFontType = PrizeFontType | ButtonFontType 20 | 21 | export interface BlockImgType extends ImgType {} 22 | 23 | export interface PrizeImgType extends ImgType { 24 | activeSrc?: string 25 | } 26 | 27 | export interface ButtonImgType extends ImgType {} 28 | 29 | export type CellImgType = PrizeImgType | ButtonImgType 30 | 31 | export interface BlockType { 32 | borderRadius?: BorderRadiusType 33 | background?: BackgroundType 34 | padding?: string 35 | paddingTop?: string | number 36 | paddingRight?: string | number 37 | paddingBottom?: string | number 38 | paddingLeft?: string | number 39 | imgs?: Array 40 | } 41 | 42 | export interface CellType { 43 | x: number 44 | y: number 45 | col: number 46 | row: number 47 | borderRadius?: BorderRadiusType 48 | background: BackgroundType 49 | shadow?: ShadowType 50 | fonts?: Array 51 | imgs?: Array 52 | } 53 | 54 | export type PrizeType = CellType 55 | 56 | export type ButtonType = CellType & { 57 | callback?: Function 58 | } 59 | 60 | export interface DefaultConfigType { 61 | gutter?: number 62 | speed?: number 63 | accelerationTime?: number 64 | decelerationTime?: number 65 | } 66 | 67 | export interface DefaultStyleType { 68 | borderRadius?: BorderRadiusType 69 | background?: BackgroundType 70 | shadow?: ShadowType 71 | fontColor?: PrizeFontType['fontColor'] 72 | fontSize?: PrizeFontType['fontSize'] 73 | fontStyle?: PrizeFontType['fontStyle'] 74 | fontWeight?: PrizeFontType['fontWeight'] 75 | lineHeight?: PrizeFontType['lineHeight'] 76 | wordWrap?: PrizeFontType['wordWrap'] 77 | lengthLimit?: PrizeFontType['lengthLimit'] 78 | } 79 | 80 | export interface ActiveStyleType { 81 | background?: BackgroundType 82 | shadow?: ShadowType 83 | fontColor?: PrizeFontType['fontColor'] 84 | fontSize?: PrizeFontType['fontSize'] 85 | fontStyle?: PrizeFontType['fontStyle'] 86 | fontWeight?: PrizeFontType['fontWeight'] 87 | lineHeight?: PrizeFontType['lineHeight'] 88 | } 89 | 90 | export type RowsType = number 91 | export type ColsType = number 92 | export type StartCallbackType = (e: MouseEvent, button?: ButtonType) => void 93 | export type EndCallbackType = (prize: object) => void 94 | 95 | export default interface LuckyGridConfig { 96 | rows?: RowsType 97 | cols?: ColsType 98 | blocks?: Array 99 | prizes?: Array 100 | buttons?: Array 101 | button?: ButtonType 102 | defaultConfig?: DefaultConfigType 103 | defaultStyle?: DefaultStyleType 104 | activeStyle?: ActiveStyleType 105 | start?: StartCallbackType 106 | end?: EndCallbackType 107 | } 108 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // 字体类型 2 | export interface FontType { 3 | text: string 4 | top?: string | number 5 | fontColor?: string 6 | fontSize?: string 7 | fontStyle?: string 8 | fontWeight?: string 9 | lineHeight?: string 10 | } 11 | 12 | // 图片类型 13 | export interface ImgType { 14 | src: string 15 | top?: string | number 16 | width?: string 17 | height?: string 18 | $resolve?: Function 19 | } 20 | 21 | export type BorderRadiusType = string | number 22 | export type BackgroundType = string 23 | export type ShadowType = string 24 | 25 | export interface ConfigType { 26 | // 临时处理元素类型, 当版本升到4.x之后就可以删掉了 27 | nodeType: number 28 | // 配置 29 | ob?: boolean 30 | // flag: 'WEB' | 'MINI-WX' | 'UNI-H5' | 'UNI-MINI-WX' 31 | flag: 'WEB' | 'MP-WX' | 'UNI-H5' | 'UNI-MP' | 'TARO-H5' | 'TARO-MP' 32 | el?: string 33 | divElement?: HTMLDivElement 34 | canvasElement?: HTMLCanvasElement 35 | ctx: CanvasRenderingContext2D 36 | dpr: number 37 | width: string 38 | height: string 39 | unitFunc?: (num: number, unit: string) => number 40 | // 覆盖方法 41 | rAF?: Function 42 | setTimeout: Function 43 | setInterval: Function 44 | clearTimeout: Function 45 | clearInterval: Function 46 | // 组件生命周期 47 | beforeCreate?: Function 48 | beforeInit?: Function 49 | afterInit?: Function 50 | beforeDraw?: Function 51 | afterDraw?: Function 52 | } 53 | 54 | export interface UniImageType { 55 | path: string 56 | width: number 57 | height: number 58 | } 59 | -------------------------------------------------------------------------------- /src/types/wheel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FontType, 3 | ImgType, 4 | BackgroundType 5 | } from './index' 6 | 7 | export interface PrizeFontType extends FontType { 8 | wordWrap?: boolean 9 | lengthLimit?: string | number 10 | } 11 | 12 | export interface ButtonFontType extends FontType {} 13 | 14 | export interface BlockImgType extends ImgType { 15 | rotate?: boolean 16 | } 17 | 18 | export interface PrizeImgType extends ImgType {} 19 | 20 | export interface ButtonImgType extends ImgType {} 21 | 22 | export interface BlockType { 23 | padding?: string 24 | background?: BackgroundType 25 | imgs?: Array 26 | } 27 | 28 | export interface PrizeType { 29 | background?: BackgroundType 30 | fonts?: Array 31 | imgs?: Array 32 | } 33 | 34 | export interface ButtonType { 35 | radius?: string 36 | pointer?: boolean 37 | background?: BackgroundType 38 | fonts?: Array 39 | imgs?: Array 40 | } 41 | 42 | export interface DefaultConfigType { 43 | gutter?: string | number 44 | offsetDegree?: number 45 | speed?: number 46 | speedFunction?: string 47 | accelerationTime?: number 48 | decelerationTime?: number 49 | stopRange?: number 50 | } 51 | 52 | export interface DefaultStyleType { 53 | background?: BackgroundType 54 | fontColor?: PrizeFontType['fontColor'] 55 | fontSize?: PrizeFontType['fontSize'] 56 | fontStyle?: PrizeFontType['fontStyle'] 57 | fontWeight?: PrizeFontType['fontWeight'] 58 | lineHeight?: PrizeFontType['lineHeight'] 59 | wordWrap?: PrizeFontType['wordWrap'] 60 | lengthLimit?: PrizeFontType['lengthLimit'] 61 | } 62 | 63 | export type StartCallbackType = (e: MouseEvent) => void 64 | export type EndCallbackType = (prize: object) => void 65 | 66 | export default interface LuckyWheelConfig { 67 | blocks?: Array 68 | prizes?: Array 69 | buttons?: Array 70 | defaultConfig?: DefaultConfigType 71 | defaultStyle?: DefaultStyleType 72 | start?: StartCallbackType 73 | end?: EndCallbackType 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断是否是期望的类型 3 | * @param { any } param 将要判断的变量 4 | * @param { ...string } types 期望的类型 5 | * @return { boolean } 返回期望是否正确 6 | */ 7 | export const isExpectType = (param: any, ...types: string[]): boolean => { 8 | return types.some(type => Object.prototype.toString.call(param).slice(8, -1).toLowerCase() === type) 9 | } 10 | 11 | /** 12 | * 移除\n 13 | * @param { string } str 将要处理的字符串 14 | * @return { string } 返回新的字符串 15 | */ 16 | export const removeEnter = (str: string): string => { 17 | return [].filter.call(str, s => s !== '\n').join('') 18 | } 19 | 20 | /** 21 | * 把任何数据类型转成数字 22 | * @param num 23 | */ 24 | export const getNumber = (num: any): number => { 25 | if (num === null) return 0 26 | if (typeof num === 'object') return NaN 27 | if (typeof num === 'number') return num 28 | if (typeof num === 'string') { 29 | if (num[num.length - 1] === '%') { 30 | return Number(num.slice(0, -1)) / 100 31 | } 32 | return Number(num) 33 | } 34 | return NaN 35 | } 36 | 37 | /** 38 | * 判断颜色是否有效 (透明色 === 无效) 39 | * @param color 颜色 40 | */ 41 | export const hasBackground = (color: string | undefined | null): boolean => { 42 | if (typeof color !== 'string') return false 43 | color = color.toLocaleLowerCase().trim() 44 | if (color === 'transparent') return false 45 | if (/^rgba/.test(color)) { 46 | const alpha = /([^\s,]+)\)$/.exec(color) 47 | if (getNumber(alpha) === 0) return false 48 | } 49 | return true 50 | } 51 | 52 | /** 53 | * 通过padding计算 54 | * @return { object } block 边框信息 55 | */ 56 | export const computePadding = ( 57 | block: { padding?: string } 58 | ): [number, number, number, number] => { 59 | let padding = block.padding?.replace(/px/g, '').split(' ').map(n => ~~n) || [0], 60 | paddingTop = 0, 61 | paddingBottom = 0, 62 | paddingLeft = 0, 63 | paddingRight = 0 64 | switch (padding.length) { 65 | case 1: 66 | paddingTop = paddingBottom = paddingLeft = paddingRight = padding[0] 67 | break 68 | case 2: 69 | paddingTop = paddingBottom = padding[0] 70 | paddingLeft = paddingRight = padding[1] 71 | break 72 | case 3: 73 | paddingTop = padding[0] 74 | paddingLeft = paddingRight = padding[1] 75 | paddingBottom = padding[2] 76 | break 77 | default: 78 | paddingTop = padding[0] 79 | paddingBottom = padding[1] 80 | paddingLeft = padding[2] 81 | paddingRight = padding[3] 82 | } 83 | // 检查是否单独传入值, 并且不是0 84 | const res = { paddingTop, paddingBottom, paddingLeft, paddingRight } 85 | for (let key in res) { 86 | // 是否含有这个属性, 并且是数字或字符串 87 | res[key] = Object.prototype.hasOwnProperty.call(block, key) && isExpectType(block[key], 'string', 'number') 88 | ? ~~String(block[key]).replace(/px/g, '') 89 | : res[key] 90 | } 91 | return [paddingTop, paddingBottom, paddingLeft, paddingRight] 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 转换为运算角度 3 | * @param { number } deg 数学角度 4 | * @return { number } 运算角度 5 | */ 6 | export const getAngle = (deg: number): number => { 7 | return Math.PI / 180 * deg 8 | } 9 | 10 | /** 11 | * 根据角度计算圆上的点 12 | * @param { number } deg 运算角度 13 | * @param { number } r 半径 14 | * @return { Array } 坐标[x, y] 15 | */ 16 | export const getArcPointerByDeg = (deg: number, r: number): [number, number] => { 17 | return [+(Math.cos(deg) * r).toFixed(8), +(Math.sin(deg) * r).toFixed(8)] 18 | } 19 | 20 | /** 21 | * 根据点计算切线方程 22 | * @param { number } x 横坐标 23 | * @param { number } y 纵坐标 24 | * @return { Array } [斜率, 常数] 25 | */ 26 | export const getTangentByPointer = (x: number, y: number): Array => { 27 | let k = - x / y 28 | let b = -k * x + y 29 | return [k, b] 30 | } 31 | 32 | // 根据三点画圆弧 33 | export const drawRadian = ( 34 | flag: string, 35 | ctx: CanvasRenderingContext2D, 36 | r: number, 37 | start: number, 38 | end: number, 39 | direction: boolean = true 40 | ) => { 41 | // 如果角度大于等于180度, 则分两次绘制, 因为 arcTo 无法绘制180度的圆弧 42 | if (Math.abs(end - start).toFixed(8) >= getAngle(180).toFixed(8)) { 43 | let middle = (end + start) / 2 44 | if (direction) { 45 | drawRadian(flag, ctx, r, start, middle, direction) 46 | drawRadian(flag, ctx, r, middle, end, direction) 47 | } else { 48 | drawRadian(flag, ctx, r, middle, end, direction) 49 | drawRadian(flag, ctx, r, start, middle, direction) 50 | } 51 | return false 52 | } 53 | // 如果方法相反, 则交换起点和终点 54 | if (!direction) [start, end] = [end, start] 55 | const [x1, y1] = getArcPointerByDeg(start, r) 56 | const [x2, y2] = getArcPointerByDeg(end, r) 57 | const [k1, b1] = getTangentByPointer(x1, y1) 58 | const [k2, b2] = getTangentByPointer(x2, y2) 59 | // 计算两条切线的交点 60 | let x0 = (b2 - b1) / (k1 - k2) 61 | let y0 = (k2 * b1 - k1 * b2) / (k2 - k1) 62 | // 如果有任何一条切线垂直于x轴, 则斜率不存在 63 | if (isNaN(x0)) { 64 | Math.abs(x1) === +r.toFixed(8) && (x0 = x1) 65 | Math.abs(x2) === +r.toFixed(8) && (x0 = x2) 66 | } 67 | if (k1 === Infinity || k1 === -Infinity) { 68 | y0 = k2 * x0 + b2 69 | } 70 | else if (k2 === Infinity || k2 === -Infinity) { 71 | y0 = k1 * x0 + b1 72 | } 73 | ctx.lineTo(x1, y1) 74 | // 微信小程序下 arcTo 在安卓真机下绘制有 bug 75 | if (flag.indexOf('MP') > 0) { 76 | ctx.quadraticCurveTo(x0, y0, x2, y2) 77 | } else { 78 | ctx.arcTo(x0, y0, x2, y2, r) 79 | } 80 | } 81 | 82 | // 绘制扇形 83 | export const drawSector = ( 84 | flag: string, 85 | ctx: CanvasRenderingContext2D, 86 | minRadius: number, 87 | maxRadius: number, 88 | start: number, 89 | end: number, 90 | gutter: number, 91 | background: string 92 | ) => { 93 | // 如果不存在 getter, 则直接使用 arc 绘制扇形 94 | if (!gutter) { 95 | ctx.beginPath() 96 | ctx.fillStyle = background 97 | ctx.moveTo(0, 0) 98 | ctx.arc(0, 0, maxRadius, start, end, false) 99 | ctx.closePath() 100 | ctx.fill() 101 | } else { 102 | drawSectorByArcTo(flag, ctx, minRadius, maxRadius, start, end, gutter, background) 103 | } 104 | } 105 | 106 | // 根据arcTo绘制扇形 107 | export const drawSectorByArcTo = ( 108 | flag: string, 109 | ctx: CanvasRenderingContext2D, 110 | minRadius: number, 111 | maxRadius: number, 112 | start: number, 113 | end: number, 114 | gutter: number, 115 | background: string 116 | ) => { 117 | if (!minRadius) minRadius = gutter 118 | // 内外圆弧分别进行等边缩放 119 | let maxGutter = getAngle(90 / Math.PI / maxRadius * gutter) 120 | let minGutter = getAngle(90 / Math.PI / minRadius * gutter) 121 | let maxStart = start + maxGutter 122 | let maxEnd = end - maxGutter 123 | let minStart = start + minGutter 124 | let minEnd = end - minGutter 125 | ctx.beginPath() 126 | ctx.fillStyle = background 127 | ctx.moveTo(...getArcPointerByDeg(maxStart, maxRadius)) 128 | drawRadian(flag, ctx, maxRadius, maxStart, maxEnd, true) 129 | // 如果 getter 比按钮短就绘制圆弧, 反之计算新的坐标点 130 | if (minEnd > minStart) { 131 | drawRadian(flag, ctx, minRadius, minStart, minEnd, false) 132 | } else { 133 | ctx.lineTo( 134 | ...getArcPointerByDeg( 135 | (start + end) / 2, 136 | gutter / 2 / Math.abs(Math.sin((start - end) / 2)) 137 | ) 138 | ) 139 | } 140 | ctx.closePath() 141 | ctx.fill() 142 | } 143 | 144 | // 绘制圆角矩形 (由于微信小程序的 arcTo 有 bug, 下面的圆弧使用二次贝塞尔曲线代替) 145 | export const drawRoundRect = ( 146 | ctx: CanvasRenderingContext2D, 147 | x: number, 148 | y: number, 149 | w: number, 150 | h: number, 151 | r: number, 152 | color: string 153 | ) => { 154 | let min = Math.min(w, h) 155 | if (r > min / 2) r = min / 2 156 | ctx.beginPath() 157 | ctx.fillStyle = color 158 | ctx.moveTo(x + r, y) 159 | ctx.lineTo(x + r, y) 160 | ctx.lineTo(x + w - r, y) 161 | // ctx.arcTo(x + w, y, x + w, y + r, r) 162 | ctx.quadraticCurveTo(x + w, y, x + w, y + r) 163 | ctx.lineTo(x + w, y + h - r) 164 | // ctx.arcTo(x + w, y + h, x + w - r, y + h, r) 165 | ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h) 166 | ctx.lineTo(x + r, y + h) 167 | // ctx.arcTo(x, y + h, x, y + h - r, r) 168 | ctx.quadraticCurveTo(x, y + h, x, y + h - r) 169 | ctx.lineTo(x, y + r) 170 | // ctx.arcTo(x, y, x + r, y, r) 171 | ctx.quadraticCurveTo(x, y, x + r, y) 172 | ctx.closePath() 173 | ctx.fill() 174 | } 175 | 176 | /** 177 | * 创建线性渐变色 178 | */ 179 | export const getLinearGradient = ( 180 | ctx: CanvasRenderingContext2D, 181 | x: number, 182 | y: number, 183 | w: number, 184 | h: number, 185 | background: string 186 | ) => { 187 | const context = (/linear-gradient\((.+)\)/.exec(background) as Array)[1] 188 | .split(',') // 根据逗号分割 189 | .map((text: string) => text.trim()) // 去除两边空格 190 | let deg = context.shift(), direction: [number, number, number, number] = [0, 0, 0, 0] 191 | // 通过起始点和角度计算渐变终点的坐标点, 这里感谢泽宇大神提醒我使用勾股定理.... 192 | if (deg.includes('deg')) { 193 | deg = deg.slice(0, -3) % 360 194 | // 根据4个象限定义起点坐标, 根据45度划分8个区域计算终点坐标 195 | const getLenOfTanDeg = (deg: number) => Math.tan(deg / 180 * Math.PI) 196 | if (deg >= 0 && deg < 45) direction = [x, y + h, x + w, y + h - w * getLenOfTanDeg(deg - 0)] 197 | else if (deg >= 45 && deg < 90) direction = [x, y + h, (x + w) - h * getLenOfTanDeg(deg - 45), y] 198 | else if (deg >= 90 && deg < 135) direction = [x + w, y + h, (x + w) - h * getLenOfTanDeg(deg - 90), y] 199 | else if (deg >= 135 && deg < 180) direction = [x + w, y + h, x, y + w * getLenOfTanDeg(deg - 135)] 200 | else if (deg >= 180 && deg < 225) direction = [x + w, y, x, y + w * getLenOfTanDeg(deg - 180)] 201 | else if (deg >= 225 && deg < 270) direction = [x + w, y, x + h * getLenOfTanDeg(deg - 225), y + h] 202 | else if (deg >= 270 && deg < 315) direction = [x, y, x + h * getLenOfTanDeg(deg - 270), y + h] 203 | else if (deg >= 315 && deg < 360) direction = [x, y, x + w, y + h - w * getLenOfTanDeg(deg - 315)] 204 | } 205 | // 创建四个简单的方向坐标 206 | else if (deg.includes('top')) direction = [x, y + h, x, y] 207 | else if (deg.includes('bottom')) direction = [x, y, x, y + h] 208 | else if (deg.includes('left')) direction = [x + w, y, x, y] 209 | else if (deg.includes('right')) direction = [x, y, x + w, y] 210 | // 创建线性渐变必须使用整数坐标 211 | const gradient = ctx.createLinearGradient(...(direction.map(n => n >> 0) as [number, number, number, number])) 212 | // 这里后期重构, 先用any代替 213 | return context.reduce((gradient: any, item: any, index: any) => { 214 | const info = item.split(' ') 215 | if (info.length === 1) gradient.addColorStop(index, info[0]) 216 | else if (info.length === 2) gradient.addColorStop(...info) 217 | return gradient 218 | }, gradient) 219 | } 220 | -------------------------------------------------------------------------------- /src/utils/polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 由于部分低版本下的某些 app 可能会缺少某些原型方法, 这里增加兼容 3 | */ 4 | 5 | // vivo x7 下网易云游戏 app 缺少 includes 方法 6 | if (!String.prototype.includes) { 7 | String.prototype.includes = function(search, start) { 8 | 'use strict'; 9 | if (typeof start !== 'number') { 10 | start = 0; 11 | } 12 | if (start + search.length > this.length) { 13 | return false; 14 | } else { 15 | return this.indexOf(search, start) !== -1; 16 | } 17 | }; 18 | } 19 | 20 | // vivo x7 下网易云游戏 app 缺少 find 方法 21 | if (!Array.prototype.find) { 22 | Object.defineProperty(Array.prototype, 'find', { 23 | value: function(predicate) { 24 | // 1. Let O be ? ToObject(this value). 25 | if (this == null) { 26 | throw new TypeError('"this" is null or not defined'); 27 | } 28 | var o = Object(this); 29 | // 2. Let len be ? ToLength(? Get(O, "length")). 30 | var len = o.length >>> 0; 31 | // 3. If IsCallable(predicate) is false, throw a TypeError exception. 32 | if (typeof predicate !== 'function') { 33 | throw new TypeError('predicate must be a function'); 34 | } 35 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 36 | var thisArg = arguments[1]; 37 | // 5. Let k be 0. 38 | var k = 0; 39 | // 6. Repeat, while k < len 40 | while (k < len) { 41 | // a. Let Pk be ! ToString(k). 42 | // b. Let kValue be ? Get(O, Pk). 43 | // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). 44 | // d. If testResult is true, return kValue. 45 | var kValue = o[k]; 46 | if (predicate.call(thisArg, kValue, k, o)) { 47 | return kValue; 48 | } 49 | // e. Increase k by 1. 50 | k++; 51 | } 52 | // 7. Return undefined. 53 | return void 0; 54 | } 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/tween.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 缓动函数 3 | * t: current time(当前时间) 4 | * b: beginning value(初始值) 5 | * c: change in value(变化量) 6 | * d: duration(持续时间) 7 | * 8 | * 感谢张鑫旭大佬 https://github.com/zhangxinxu/Tween 9 | */ 10 | 11 | interface SpeedType { 12 | easeIn: (t: number, b: number, c: number, d: number) => number 13 | easeOut: (t: number, b: number, c: number, d: number) => number 14 | } 15 | 16 | // 二次方的缓动 17 | export const quad: SpeedType = { 18 | easeIn: function (t, b, c, d) { 19 | if (t >= d) t = d 20 | return c * (t /= d) * t + b 21 | }, 22 | easeOut: function (t, b, c, d) { 23 | if (t >= d) t = d 24 | return -c * (t /= d) * (t - 2) + b 25 | } 26 | } 27 | 28 | // 三次方的缓动 29 | export const cubic: SpeedType = { 30 | easeIn: function (t, b, c, d) { 31 | if (t >= d) t = d 32 | return c * (t /= d) * t * t + b 33 | }, 34 | easeOut: function (t, b, c, d) { 35 | if (t >= d) t = d 36 | return c * ((t = t / d - 1) * t * t + 1) + b 37 | } 38 | } 39 | 40 | // 四次方的缓动 41 | export const quart: SpeedType = { 42 | easeIn: function (t, b, c, d) { 43 | if (t >= d) t = d 44 | return c * (t /= d) * t * t * t + b 45 | }, 46 | easeOut: function (t, b, c, d) { 47 | if (t >= d) t = d 48 | return -c * ((t = t / d - 1) * t * t * t - 1) + b 49 | } 50 | } 51 | 52 | // 五次方的缓动 53 | export const quint: SpeedType = { 54 | easeIn: function (t, b, c, d) { 55 | if (t >= d) t = d 56 | return c * (t /= d) * t * t * t * t + b 57 | }, 58 | easeOut: function (t, b, c, d) { 59 | if (t >= d) t = d 60 | return c * ((t = t / d - 1) * t * t * t * t + 1) + b 61 | } 62 | } 63 | 64 | // 正弦曲线的缓动 65 | export const sine: SpeedType = { 66 | easeIn: function (t, b, c, d) { 67 | if (t >= d) t = d 68 | return -c * Math.cos(t / d * (Math.PI / 2)) + c + b 69 | }, 70 | easeOut: function (t, b, c, d) { 71 | if (t >= d) t = d 72 | return c * Math.sin(t / d * (Math.PI / 2)) + b 73 | } 74 | } 75 | 76 | // 指数曲线的缓动 77 | export const expo: SpeedType = { 78 | easeIn: function (t, b, c, d) { 79 | if (t >= d) t = d 80 | return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b 81 | }, 82 | easeOut: function (t, b, c, d) { 83 | if (t >= d) t = d 84 | return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b 85 | } 86 | } 87 | 88 | // 圆形曲线的缓动 89 | export const circ: SpeedType = { 90 | easeIn: function (t, b, c, d) { 91 | if (t >= d) t = d 92 | return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b 93 | }, 94 | easeOut: function (t, b, c, d) { 95 | if (t >= d) t = d 96 | return c * Math.sqrt(1 - (t = t / d - 1) * t) + b 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "allowJs": true, 6 | "strict": true, 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "resolveJsonModule": true, 14 | "sourceMap": true, 15 | "lib": [ 16 | "es2015", 17 | "es2016", 18 | "es2017", 19 | "esnext", 20 | "dom", 21 | "dom.iterable", 22 | "scripthost" 23 | ] 24 | }, 25 | "exclude": [ 26 | "node_modules/**" 27 | ], 28 | "include": [ 29 | "src/**/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /umd.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).LuckyCanvas={})}(this,(function(t){"use strict"; 2 | /*! ***************************************************************************** 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | ***************************************************************************** */var e=function(t,i){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i])})(t,i)};function i(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var n=function(){return(n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i0&&r[r.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!r||o[1]>r[0]&&o[1]this.length)&&-1!==this.indexOf(t,e)}),Array.prototype.find||Object.defineProperty(Array.prototype,"find",{value:function(t){if(null==this)throw new TypeError('"this" is null or not defined');var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError("predicate must be a function");for(var n=arguments[1],r=0;r '"+e.src+"' 不能为空或不合法"),"WEB"===n.config.flag){var s=new Image;s.src=t,s.onload=function(){return r(s)},s.onerror=function(){return o("=> '"+e.src+"' 图片加载失败")}}else e[i]=r}))},t.prototype.drawImage=function(t,e,i,n,r){var o,s=this.config,a=this.ctx;return["WEB","MP-WX"].includes(s.flag)?o=t:["UNI-H5","UNI-MP","TARO-H5","TARO-MP"].includes(s.flag)&&(o=t.path),a.drawImage(o,e,i,n,r)},t.prototype.getLength=function(t){return a(t,"number")?t:a(t,"string")?this.changeUnits(t):0},t.prototype.changeUnits=function(t,e){var i=this;return void 0===e&&(e=1),Number(t.replace(/^([-]*[0-9.]*)([a-z%]*)$/,(function(t,n,r){var o={"%":function(t){return t*(e/100)},px:function(t){return 1*t},rem:function(t){return t*i.htmlFontSize}}[r];if(o)return o(n);var s=i.config.unitFunc;return s?s(n,r):n})))},t.prototype.$set=function(t,e,i){t&&"object"==typeof t&&v(t,e,i)},t.prototype.$computed=function(t,e,i){var n=this;Object.defineProperty(t,e,{get:function(){return i.call(n)}})},t.prototype.$watch=function(t,e,i){void 0===i&&(i={}),"object"==typeof e&&(e=(i=e).handler);var n=new y(this,t,e,i);return i.immediate&&e.call(this,n.value),function(){}},t}(),x=function(t){return Math.PI/180*t},z=function(t,e){return[+(Math.cos(t)*e).toFixed(8),+(Math.sin(t)*e).toFixed(8)]},I=function(t,e){var i=-t/e;return[i,-i*t+e]},k=function(t,e,i,n,r,o){var s;if(void 0===o&&(o=!0),Math.abs(r-n).toFixed(8)>=x(180).toFixed(8)){var a=(r+n)/2;return o?(k(t,e,i,n,a,o),k(t,e,i,a,r,o)):(k(t,e,i,a,r,o),k(t,e,i,n,a,o)),!1}o||(n=(s=[r,n])[0],r=s[1]);var u=z(n,i),h=u[0],c=u[1],l=z(r,i),f=l[0],d=l[1],p=I(h,c),g=p[0],m=p[1],v=I(f,d),b=v[0],y=v[1],w=(y-m)/(g-b),S=(b*m-g*y)/(b-g);isNaN(w)&&(Math.abs(h)===+i.toFixed(8)&&(w=h),Math.abs(f)===+i.toFixed(8)&&(w=f)),g===1/0||g===-1/0?S=b*w+y:b!==1/0&&b!==-1/0||(S=g*w+m),e.lineTo(h,c),t.indexOf("MP")>0?e.quadraticCurveTo(w,S,f,d):e.arcTo(w,S,f,d,i)},S=function(t,e,i,n,r,o,s,a){i||(i=s);var u=x(90/Math.PI/n*s),h=x(90/Math.PI/i*s),c=r+u,l=o-u,f=r+h,d=o-h;e.beginPath(),e.fillStyle=a,e.moveTo.apply(e,z(c,n)),k(t,e,n,c,l,!0),d>f?k(t,e,i,f,d,!1):e.lineTo.apply(e,z((r+o)/2,s/2/Math.abs(Math.sin((r-o)/2)))),e.closePath(),e.fill()},T=function(t,e,i,n,r,o,s){var a=Math.min(n,r);o>a/2&&(o=a/2),t.beginPath(),t.fillStyle=s,t.moveTo(e+o,i),t.lineTo(e+o,i),t.lineTo(e+n-o,i),t.quadraticCurveTo(e+n,i,e+n,i+o),t.lineTo(e+n,i+r-o),t.quadraticCurveTo(e+n,i+r,e+n-o,i+r),t.lineTo(e+o,i+r),t.quadraticCurveTo(e,i+r,e,i+r-o),t.lineTo(e,i+o),t.quadraticCurveTo(e,i,e+o,i),t.closePath(),t.fill()},C={easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),-i*(t/=n)*(t-2)+e}},W={easeIn:function(t,e,i,n){return t>=n&&(t=n),-i*Math.cos(t/n*(Math.PI/2))+i+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*Math.sin(t/n*(Math.PI/2))+e}},O={easeIn:function(t,e,i,n){return t>=n&&(t=n),0==t?e:i*Math.pow(2,10*(t/n-1))+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),t==n?e+i:i*(1-Math.pow(2,-10*t/n))+e}},_={easeIn:function(t,e,i,n){return t>=n&&(t=n),-i*(Math.sqrt(1-(t/=n)*t)-1)+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*Math.sqrt(1-(t=t/n-1)*t)+e}},E=Object.freeze({__proto__:null,quad:C,cubic:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*((t=t/n-1)*t*t+1)+e}},quart:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),-i*((t=t/n-1)*t*t*t-1)+e}},quint:{easeIn:function(t,e,i,n){return t>=n&&(t=n),i*(t/=n)*t*t*t*t+e},easeOut:function(t,e,i,n){return t>=n&&(t=n),i*((t=t/n-1)*t*t*t*t+1)+e}},sine:W,expo:O,circ:_}),P=function(t){function e(e,i){var n;void 0===i&&(i={});var r=t.call(this,e)||this;return r.blocks=[],r.prizes=[],r.buttons=[],r.defaultConfig={},r._defaultConfig={gutter:"0px",offsetDegree:0,speed:20,speedFunction:"quad",accelerationTime:2500,decelerationTime:2500,stopRange:.8},r.defaultStyle={},r._defaultStyle={fontSize:"18px",fontColor:"#000",fontStyle:"sans-serif",fontWeight:"400",lineHeight:"",background:"rgba(0,0,0,0)",wordWrap:!0,lengthLimit:"90%"},r.Radius=0,r.prizeRadius=0,r.prizeDeg=0,r.prizeRadian=0,r.rotateDeg=0,r.maxBtnRadius=0,r.startTime=0,r.endTime=0,r.stopDeg=0,r.endDeg=0,r.FPS=16.6,r.blockImgs=[[]],r.prizeImgs=[[]],r.btnImgs=[[]],e.ob&&(r.initData(i),r.initWatch()),r.initComputed(),null===(n=e.beforeCreate)||void 0===n||n.call(r),r.init({blockImgs:r.blocks.map((function(t){return t.imgs})),prizeImgs:r.prizes.map((function(t){return t.imgs})),btnImgs:r.buttons.map((function(t){return t.imgs}))}),r}return i(e,t),e.prototype.initData=function(t){this.$set(this,"blocks",t.blocks||[]),this.$set(this,"prizes",t.prizes||[]),this.$set(this,"buttons",t.buttons||[]),this.$set(this,"defaultConfig",t.defaultConfig||{}),this.$set(this,"defaultStyle",t.defaultStyle||{}),this.$set(this,"startCallback",t.start),this.$set(this,"endCallback",t.end)},e.prototype.initComputed=function(){var t=this;this.$computed(this,"_defaultConfig",(function(){return n({gutter:"0px",offsetDegree:0,speed:20,speedFunction:"quad",accelerationTime:2500,decelerationTime:2500,stopRange:.8},t.defaultConfig)})),this.$computed(this,"_defaultStyle",(function(){return n({fontSize:"18px",fontColor:"#000",fontStyle:"sans-serif",fontWeight:"400",background:"rgba(0,0,0,0)",wordWrap:!0,lengthLimit:"90%"},t.defaultStyle)}))},e.prototype.initWatch=function(){var t=this;this.$watch("blocks",(function(e){return t.init({blockImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("prizes",(function(e){return t.init({prizeImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("buttons",(function(e){return t.init({btnImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("defaultConfig",(function(){return t.draw()}),{deep:!0}),this.$watch("defaultStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("startCallback",(function(){return t.init({})})),this.$watch("endCallback",(function(){return t.init({})}))},e.prototype.init=function(e){var i,n,r=this;t.prototype.init.call(this);var o=this.config,s=this.ctx;this.Radius=Math.min(this.boxWidth,this.boxHeight)/2,null===(i=o.beforeInit)||void 0===i||i.call(this),s.translate(this.Radius,this.Radius),this.draw(),this.draw(),Object.keys(e).forEach((function(t){var i=t,n={blockImgs:"blocks",prizeImgs:"prizes",btnImgs:"buttons"}[i],o=e[i];o&&o.forEach((function(t,e){t&&t.forEach((function(t,o){r.loadAndCacheImg(n,e,i,o,(function(){r.draw()}))}))}))})),null===(n=o.afterInit)||void 0===n||n.call(this)},e.prototype.handleClick=function(t){var e,i=this.ctx;i.beginPath(),i.arc(0,0,this.maxBtnRadius,0,2*Math.PI,!1),i.isPointInPath(t.offsetX,t.offsetY)&&(this.startTime||null===(e=this.startCallback)||void 0===e||e.call(this,t))},e.prototype.loadAndCacheImg=function(t,e,i,n,s){return r(this,void 0,void 0,(function(){var r,a,u=this;return o(this,(function(o){return(r=this[t][e])&&r.imgs&&(a=r.imgs[n])?(this[i][e]||(this[i][e]=[]),this.loadImg(a.src,a).then((function(t){u[i][e][n]=t,s.call(u)})).catch((function(i){console.error(t+"["+e+"].imgs["+n+"] "+i)})),[2]):[2]}))}))},e.prototype.computedWidthAndHeight=function(t,e,i,n){if(!e.width&&!e.height)return[t.width,t.height];if(e.width&&!e.height){var r=this.getWidth(e.width,i);return[r,t.height*(r/t.width)]}if(!e.width&&e.height){var o=this.getHeight(e.height,n);return[t.width*(o/t.height),o]}return[this.getWidth(e.width,i),this.getHeight(e.height,n)]},e.prototype.draw=function(){var t,e,i=this,n=this,r=n.config,o=n.ctx,s=n._defaultConfig,a=n._defaultStyle;null===(t=r.beforeDraw)||void 0===t||t.call(this,o),o.clearRect(-this.Radius,-this.Radius,2*this.Radius,2*this.Radius),this.prizeRadius=this.blocks.reduce((function(t,e,n){return h(e.background)&&(o.beginPath(),o.fillStyle=e.background,o.arc(0,0,t,0,2*Math.PI,!1),o.fill()),e.imgs&&e.imgs.forEach((function(e,r){if(i.blockImgs[n]){var s=i.blockImgs[n][r];if(s){var a=i.computedWidthAndHeight(s,e,2*t,2*t),u=a[0],h=a[1],c=[i.getOffsetX(u),i.getHeight(e.top,2*t)-t],l=c[0],f=c[1];o.save(),e.rotate&&o.rotate(x(i.rotateDeg)),i.drawImage(s,l,f,u,h),o.restore()}}})),t-i.getLength(e.padding&&e.padding.split(" ")[0])}),this.Radius),this.prizeDeg=360/this.prizes.length,this.prizeRadian=x(this.prizeDeg);var c=x(-90+this.rotateDeg+s.offsetDegree),l=function(t){return i.getOffsetX(o.measureText(t).width)},f=function(t,e,n){var r=t.lineHeight||a.lineHeight||t.fontSize||a.fontSize;return i.getHeight(t.top,e)+(n+1)*i.getLength(r)};o.save(),this.prizes.forEach((function(t,e){var n=c+e*i.prizeRadian,d=i.prizeRadius-i.maxBtnRadius,p=t.background||a.background;h(p)&&function(t,e,i,n,r,o,s,a){s?S(t,e,i,n,r,o,s,a):(e.beginPath(),e.fillStyle=a,e.moveTo(0,0),e.arc(0,0,n,r,o,!1),e.closePath(),e.fill())}(r.flag,o,i.maxBtnRadius,i.prizeRadius,n-i.prizeRadian/2,n+i.prizeRadian/2,i.getLength(s.gutter),p);var g=Math.cos(n)*i.prizeRadius,m=Math.sin(n)*i.prizeRadius;o.translate(g,m),o.rotate(n+x(90)),t.imgs&&t.imgs.forEach((function(t,n){if(i.prizeImgs[e]){var r=i.prizeImgs[e][n];if(r){var o=i.computedWidthAndHeight(r,t,i.prizeRadian*i.prizeRadius,d),s=o[0],a=o[1],u=[i.getOffsetX(s),i.getHeight(t.top,d)],h=u[0],c=u[1];i.drawImage(r,h,c,s,a)}}})),t.fonts&&t.fonts.forEach((function(t){var e=t.fontColor||a.fontColor,n=t.fontWeight||a.fontWeight,r=i.getLength(t.fontSize||a.fontSize),h=t.fontStyle||a.fontStyle;o.fillStyle=e,o.font=n+" "+(r>>0)+"px "+h;var c=[],p=String(t.text);if(Object.prototype.hasOwnProperty.call(t,"wordWrap")?t.wordWrap:a.wordWrap){p=u(p);for(var g="",m=0;mi.getWidth(t.lengthLimit||a.lengthLimit,b)&&(c.push(g.slice(0,-1)),g=p[m])}g&&c.push(g),c.length||c.push(p)}else c=p.split("\n");c.filter((function(t){return!!t})).forEach((function(e,i){o.fillText(e,l(e),f(t,d,i))}))})),o.rotate(x(360)-n-x(90)),o.translate(-g,-m)})),o.restore(),this.buttons.forEach((function(t,e){var n=i.getHeight(t.radius);i.maxBtnRadius=Math.max(i.maxBtnRadius,n),h(t.background)&&(o.beginPath(),o.fillStyle=t.background,o.arc(0,0,n,0,2*Math.PI,!1),o.fill()),t.pointer&&h(t.background)&&(o.beginPath(),o.fillStyle=t.background,o.moveTo(-n,0),o.lineTo(n,0),o.lineTo(0,2*-n),o.closePath(),o.fill()),t.imgs&&t.imgs.forEach((function(t,r){if(i.btnImgs[e]){var o=i.btnImgs[e][r];if(o){var s=i.computedWidthAndHeight(o,t,2*n,2*n),a=s[0],u=s[1],h=[i.getOffsetX(a),i.getHeight(t.top,n)],c=h[0],l=h[1];i.drawImage(o,c,l,a,u)}}})),t.fonts&&t.fonts.forEach((function(t){var e=t.fontColor||a.fontColor,r=t.fontWeight||a.fontWeight,s=i.getLength(t.fontSize||a.fontSize),u=t.fontStyle||a.fontStyle;o.fillStyle=e,o.font=r+" "+(s>>0)+"px "+u,String(t.text).split("\n").forEach((function(e,i){o.fillText(e,l(e),f(t,n,i))}))}))})),null===(e=r.afterDraw)||void 0===e||e.call(this,o)},e.prototype.play=function(){this.startTime||(this.startTime=Date.now(),this.prizeFlag=void 0,this.run())},e.prototype.stop=function(t){this.prizeFlag=t<0?-1:t%this.prizes.length,-1===this.prizeFlag&&(this.rotateDeg=this.prizeDeg/2-this._defaultConfig.offsetDegree,this.draw())},e.prototype.run=function(t){void 0===t&&(t=0);var e=this,i=e.rAF,n=e.prizeFlag,r=e.prizeDeg,o=e.rotateDeg,s=e._defaultConfig;if(-1!==n){var a=Date.now()-this.startTime;if(a>=s.accelerationTime&&void 0!==n){this.FPS=a/t,this.endTime=Date.now(),this.stopDeg=o;for(var u=(Math.random()*r-r/2)*this.getLength(s.stopRange),h=0;++h;){var c=360*h-n*r-o-s.offsetDegree+u;if(E[s.speedFunction].easeOut(this.FPS,this.stopDeg,c,s.decelerationTime)-this.stopDeg>s.speed){this.endDeg=c;break}}return this.slowDown()}this.rotateDeg=(o+E[s.speedFunction].easeIn(a,0,s.speed,s.accelerationTime))%360,this.draw(),i(this.run.bind(this,t+1))}else this.startTime=0},e.prototype.slowDown=function(){var t,e=this,i=e.rAF,r=e.prizes,o=e.prizeFlag,s=e.stopDeg,a=e.endDeg,u=e._defaultConfig,h=Date.now()-this.endTime;if(-1!==o){if(h>=u.decelerationTime)return this.startTime=0,void(null===(t=this.endCallback)||void 0===t||t.call(this,n({},r.find((function(t,e){return e===o})))));this.rotateDeg=E[u.speedFunction].easeOut(h,s,a,u.decelerationTime)%360,this.draw(),i(this.slowDown.bind(this))}else this.startTime=0},e.prototype.getWidth=function(t,e){return void 0===e&&(e=this.prizeRadian*this.prizeRadius),a(t,"number")?t:a(t,"string")?this.changeUnits(t,e):0},e.prototype.getHeight=function(t,e){return void 0===e&&(e=this.prizeRadius),a(t,"number")?t:a(t,"string")?this.changeUnits(t,e):0},e.prototype.getOffsetX=function(t){return-t/2},e.prototype.conversionAxis=function(t,e){var i=this.config;return[t/i.dpr-this.Radius,e/i.dpr-this.Radius]},e}(w),D=function(t){function e(e,i){var n;void 0===i&&(i={});var r=t.call(this,e)||this;r.rows=3,r.cols=3,r.blocks=[],r.prizes=[],r.buttons=[],r.defaultConfig={},r._defaultConfig={gutter:5,speed:20,accelerationTime:2500,decelerationTime:2500},r.defaultStyle={},r._defaultStyle={borderRadius:20,fontColor:"#000",fontSize:"18px",fontStyle:"sans-serif",fontWeight:"400",lineHeight:"",background:"rgba(0,0,0,0)",shadow:"",wordWrap:!0,lengthLimit:"90%"},r.activeStyle={},r._activeStyle={background:"#ffce98",shadow:"",fontStyle:"",fontWeight:"",fontSize:"",lineHeight:"",fontColor:""},r.cellWidth=0,r.cellHeight=0,r.startTime=0,r.endTime=0,r.currIndex=0,r.stopIndex=0,r.endIndex=0,r.demo=!1,r.timer=0,r.FPS=16.6,r.prizeFlag=-1,r.cells=[],r.blockImgs=[[]],r.btnImgs=[[]],r.prizeImgs=[],e.ob&&(r.initData(i),r.initWatch()),r.initComputed(),null===(n=e.beforeCreate)||void 0===n||n.call(r);var o=r.buttons.map((function(t){return t.imgs}));return r.button&&o.push(r.button.imgs),r.init({blockImgs:r.blocks.map((function(t){return t.imgs})),prizeImgs:r.prizes.map((function(t){return t.imgs})),btnImgs:o}),r}return i(e,t),e.prototype.initData=function(t){this.$set(this,"rows",Number(t.rows)||3),this.$set(this,"cols",Number(t.cols)||3),this.$set(this,"blocks",t.blocks||[]),this.$set(this,"prizes",t.prizes||[]),this.$set(this,"buttons",t.buttons||[]),this.$set(this,"button",t.button),this.$set(this,"defaultConfig",t.defaultConfig||{}),this.$set(this,"defaultStyle",t.defaultStyle||{}),this.$set(this,"activeStyle",t.activeStyle||{}),this.$set(this,"startCallback",t.start),this.$set(this,"endCallback",t.end)},e.prototype.initComputed=function(){var t=this;this.$computed(this,"_defaultConfig",(function(){var e=n({gutter:5,speed:20,accelerationTime:2500,decelerationTime:2500},t.defaultConfig);return e.gutter=t.getLength(e.gutter),e.speed=e.speed/40,e})),this.$computed(this,"_defaultStyle",(function(){return n({borderRadius:20,fontColor:"#000",fontSize:"18px",fontStyle:"sans-serif",fontWeight:"400",background:"rgba(0,0,0,0)",shadow:"",wordWrap:!0,lengthLimit:"90%"},t.defaultStyle)})),this.$computed(this,"_activeStyle",(function(){return n({background:"#ffce98",shadow:""},t.activeStyle)}))},e.prototype.initWatch=function(){var t=this;this.$watch("blocks",(function(e){return t.init({blockImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("prizes",(function(e){return t.init({prizeImgs:e.map((function(t){return t.imgs}))})}),{deep:!0}),this.$watch("buttons",(function(e){var i=e.map((function(t){return t.imgs}));return t.button&&i.push(t.button.imgs),t.init({btnImgs:i})}),{deep:!0}),this.$watch("button",(function(){var e=t.buttons.map((function(t){return t.imgs}));return t.button&&e.push(t.button.imgs),t.init({btnImgs:e})}),{deep:!0}),this.$watch("rows",(function(){return t.init({})})),this.$watch("cols",(function(){return t.init({})})),this.$watch("defaultConfig",(function(){return t.draw()}),{deep:!0}),this.$watch("defaultStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("activeStyle",(function(){return t.draw()}),{deep:!0}),this.$watch("startCallback",(function(){return t.init({})})),this.$watch("endCallback",(function(){return t.init({})}))},e.prototype.init=function(e){var i,n,r=this;t.prototype.init.call(this);var o=this,s=o.config;o.ctx,o.button,null===(i=s.beforeInit)||void 0===i||i.call(this),this.draw(),Object.keys(e).forEach((function(t){var i=t,n=e[i],o={blockImgs:"blocks",prizeImgs:"prizes",btnImgs:"buttons"}[i];n&&n.forEach((function(t,e){t&&t.forEach((function(t,n){r.loadAndCacheImg(o,e,i,n,(function(){r.draw()}))}))}))})),null===(n=s.afterInit)||void 0===n||n.call(this)},e.prototype.handleClick=function(t){var e=this,i=this.ctx;s(s([],this.buttons),[this.button]).forEach((function(n){var r;if(n){var o=e.getGeometricProperty([n.x,n.y,n.col||1,n.row||1]),s=o[0],a=o[1],u=o[2],h=o[3];i.beginPath(),i.rect(s,a,u,h),i.isPointInPath(t.offsetX,t.offsetY)&&(e.startTime||("function"==typeof n.callback&&n.callback.call(e,n),null===(r=e.startCallback)||void 0===r||r.call(e,t,n)))}}))},e.prototype.loadAndCacheImg=function(t,e,i,n,s){return r(this,void 0,void 0,(function(){var r,a,u,h=this;return o(this,(function(o){return r=this[t][e],"buttons"===t&&!this.buttons.length&&this.button&&(r=this.button),r&&r.imgs&&(a=r.imgs[n])?(this[i][e]||(this[i][e]=[]),u=[this.loadImg(a.src,a),a.activeSrc&&this.loadImg(a.activeSrc,a,"$activeResolve")],Promise.all(u).then((function(t){var r=t[0],o=t[1];h[i][e][n]={defaultImg:r,activeImg:o},s.call(h)})).catch((function(i){console.error(t+"["+e+"].imgs["+n+"] "+i)})),[2]):[2]}))}))},e.prototype.computedWidthAndHeight=function(t,e,i){if(!e.width&&!e.height)return[t.width,t.height];if(e.width&&!e.height){var n=this.getWidth(e.width,i.col);return[n,t.height*(n/t.width)]}if(!e.width&&e.height){var r=this.getHeight(e.height,i.row);return[t.width*(r/t.height),r]}return[this.getWidth(e.width,i.col),this.getHeight(e.height,i.row)]},e.prototype.draw=function(){var t,e,i=this,n=this,r=n.config,o=n.ctx,c=n._defaultConfig,l=n._defaultStyle,f=n._activeStyle;null===(t=r.beforeDraw)||void 0===t||t.call(this,o),o.clearRect(0,0,this.boxWidth,this.boxHeight),this.cells=s(s([],this.prizes),this.buttons),this.button&&this.cells.push(this.button),this.cells.forEach((function(t){t.col=t.col||1,t.row=t.row||1})),this.prizeArea=this.blocks.reduce((function(t,e){var n=t.x,r=t.y,s=t.w,u=t.h,c=function(t){var e,i=(null===(e=t.padding)||void 0===e?void 0:e.replace(/px/g,"").split(" ").map((function(t){return~~t})))||[0],n=0,r=0,o=0,s=0;switch(i.length){case 1:n=r=o=s=i[0];break;case 2:n=r=i[0],o=s=i[1];break;case 3:n=i[0],o=s=i[1],r=i[2];break;default:n=i[0],r=i[1],o=i[2],s=i[3]}var u={paddingTop:n,paddingBottom:r,paddingLeft:o,paddingRight:s};for(var h in u)u[h]=Object.prototype.hasOwnProperty.call(t,h)&&a(t[h],"string","number")?~~String(t[h]).replace(/px/g,""):u[h];return[n,r,o,s]}(e),f=c[0],d=c[1],p=c[2],g=c[3],m=e.borderRadius?i.getLength(e.borderRadius):0,v=e.background||l.background;return h(v)&&T(o,n,r,s,u,m,i.handleBackground(n,r,s,u,v)),{x:n+p,y:r+f,w:s-p-g,h:u-f-d}}),{x:0,y:0,w:this.boxWidth,h:this.boxHeight}),this.cellWidth=(this.prizeArea.w-c.gutter*(this.cols-1))/this.cols,this.cellHeight=(this.prizeArea.h-c.gutter*(this.rows-1))/this.rows,this.cells.forEach((function(t,e){var n=i.getGeometricProperty([t.x,t.y,t.col,t.row]),s=n[0],a=n[1],c=n[2],d=n[3],p=!1;(void 0===i.prizeFlag||i.prizeFlag>-1)&&(p=e===i.currIndex%i.prizes.length>>0);var g=p?f.background:t.background||l.background;if(h(g)){var m=(p?f.shadow:t.shadow||l.shadow).replace(/px/g,"").split(",")[0].split(" ").map((function(t,e){return e<3?Number(t):t}));4===m.length&&(o.shadowColor=m[3],o.shadowOffsetX=m[0]*r.dpr,o.shadowOffsetY=m[1]*r.dpr,o.shadowBlur=m[2],m[0]>0?c-=m[0]:(c+=m[0],s-=m[0]),m[1]>0?d-=m[1]:(d+=m[1],a-=m[1])),T(o,s,a,c,d,i.getLength(t.borderRadius?t.borderRadius:l.borderRadius),i.handleBackground(s,a,c,d,g)),o.shadowColor="rgba(0, 0, 0, 0)",o.shadowOffsetX=0,o.shadowOffsetY=0,o.shadowBlur=0}var v="prizeImgs";e>=i.prizes.length&&(v="btnImgs",e-=i.prizes.length),t.imgs&&t.imgs.forEach((function(n,r){if(i[v][e]){var o=i[v][e][r];if(o){var u=p&&o.activeImg||o.defaultImg;if(u){var h=i.computedWidthAndHeight(u,n,t),c=h[0],l=h[1],f=[s+i.getOffsetX(c,t.col),a+i.getHeight(n.top,t.row)],d=f[0],g=f[1];i.drawImage(u,d,g,c,l)}}}})),t.fonts&&t.fonts.forEach((function(e){var n=p&&f.fontStyle?f.fontStyle:e.fontStyle||l.fontStyle,r=p&&f.fontWeight?f.fontWeight:e.fontWeight||l.fontWeight,h=p&&f.fontSize?i.getLength(f.fontSize):i.getLength(e.fontSize||l.fontSize),c=p&&f.lineHeight?f.lineHeight:e.lineHeight||l.lineHeight||e.fontSize||l.fontSize;o.font=r+" "+(h>>0)+"px "+n,o.fillStyle=p&&f.fontColor?f.fontColor:e.fontColor||l.fontColor;var d=[],g=String(e.text);if(Object.prototype.hasOwnProperty.call(e,"wordWrap")?e.wordWrap:l.wordWrap){g=u(g);for(var m="",v=0;vi.getWidth(e.lengthLimit||l.lengthLimit,t.col)&&(d.push(m.slice(0,-1)),m=g[v])}m&&d.push(m),d.length||d.push(g)}else d=g.split("\n");d.forEach((function(n,r){o.fillText(n,s+i.getOffsetX(o.measureText(n).width,t.col),a+i.getHeight(e.top,t.row)+(r+1)*i.getLength(c))}))}))})),null===(e=r.afterDraw)||void 0===e||e.call(this,o)},e.prototype.handleBackground=function(t,e,i,n,r){var o=this.ctx;return r.includes("linear-gradient")&&(r=function(t,e,i,n,r,o){var s=/linear-gradient\((.+)\)/.exec(o)[1].split(",").map((function(t){return t.trim()})),a=s.shift(),u=[0,0,0,0];if(a.includes("deg")){var h=function(t){return Math.tan(t/180*Math.PI)};(a=a.slice(0,-3)%360)>=0&&a<45?u=[e,i+r,e+n,i+r-n*h(a-0)]:a>=45&&a<90?u=[e,i+r,e+n-r*h(a-45),i]:a>=90&&a<135?u=[e+n,i+r,e+n-r*h(a-90),i]:a>=135&&a<180?u=[e+n,i+r,e,i+n*h(a-135)]:a>=180&&a<225?u=[e+n,i,e,i+n*h(a-180)]:a>=225&&a<270?u=[e+n,i,e+r*h(a-225),i+r]:a>=270&&a<315?u=[e,i,e+r*h(a-270),i+r]:a>=315&&a<360&&(u=[e,i,e+n,i+r-n*h(a-315)])}else a.includes("top")?u=[e,i+r,e,i]:a.includes("bottom")?u=[e,i,e,i+r]:a.includes("left")?u=[e+n,i,e,i]:a.includes("right")&&(u=[e,i,e+n,i]);var c=t.createLinearGradient.apply(t,u.map((function(t){return t>>0})));return s.reduce((function(t,e,i){var n=e.split(" ");return 1===n.length?t.addColorStop(i,n[0]):2===n.length&&t.addColorStop.apply(t,n),t}),c)}(o,t,e,i,n,r)),r},e.prototype.play=function(){var t=this.config.clearInterval;this.startTime||(t(this.timer),this.startTime=Date.now(),this.prizeFlag=void 0,this.run())},e.prototype.stop=function(t){this.prizeFlag=t<0?-1:t%this.prizes.length,-1===this.prizeFlag&&(this.currIndex=0,this.draw())},e.prototype.run=function(t){void 0===t&&(t=0);var e=this,i=e.rAF,n=e.currIndex,r=e.prizes,o=e.prizeFlag,s=e.startTime,a=e._defaultConfig,u=Date.now()-s;if(u>=a.accelerationTime&&void 0!==o){this.FPS=u/t,this.endTime=Date.now(),this.stopIndex=n;for(var h=0;++h;){var c=r.length*h+o-(n>>0);if(C.easeOut(this.FPS,this.stopIndex,c,a.decelerationTime)-this.stopIndex>a.speed){this.endIndex=c;break}}return this.slowDown()}this.currIndex=(n+C.easeIn(u,.1,a.speed,a.accelerationTime))%r.length,this.draw(),i(this.run.bind(this,t+1))},e.prototype.slowDown=function(){var t,e=this,i=e.rAF,r=e.prizes,o=e.prizeFlag,s=e.stopIndex,a=e.endIndex,u=e._defaultConfig,h=Date.now()-this.endTime;if(-1!==o){if(h>u.decelerationTime)return this.startTime=0,void(null===(t=this.endCallback)||void 0===t||t.call(this,n({},r.find((function(t,e){return e===o})))));this.currIndex=C.easeOut(h,s,a,u.decelerationTime)%r.length,this.draw(),i(this.slowDown.bind(this))}else this.startTime=0},e.prototype.walk=function(){var t=this,e=this.config,i=e.setInterval;(0,e.clearInterval)(this.timer),this.timer=i((function(){t.currIndex+=1,t.draw()}),1300)},e.prototype.getGeometricProperty=function(t){var e=t[0],i=t[1],n=t[2],r=t[3],o=this.cellWidth,s=this.cellHeight,a=this._defaultConfig.gutter,u=[this.prizeArea.x+(o+a)*e,this.prizeArea.y+(s+a)*i];return n&&r&&u.push(o*n+a*(n-1),s*r+a*(r-1)),u},e.prototype.getWidth=function(t,e){return void 0===e&&(e=1),a(t,"number")?t:a(t,"string")?this.changeUnits(t,this.cellWidth*e+this._defaultConfig.gutter*(e-1)):0},e.prototype.getHeight=function(t,e){return void 0===e&&(e=1),a(t,"number")?t:a(t,"string")?this.changeUnits(t,this.cellHeight*e+this._defaultConfig.gutter*(e-1)):0},e.prototype.getOffsetX=function(t,e){return void 0===e&&(e=1),(this.cellWidth*e+this._defaultConfig.gutter*(e-1)-t)/2},e.prototype.conversionAxis=function(t,e){var i=this.config;return[t/i.dpr,e/i.dpr]},e}(w);t.LuckyGrid=D,t.LuckyWheel=P,Object.defineProperty(t,"__esModule",{value:!0})})); 16 | -------------------------------------------------------------------------------- /wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuckDraw/lucky-canvas-invalid/950ff673375bd399871a2183a34f4c9c5b37ad65/wx.jpg --------------------------------------------------------------------------------