diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40c872ff478d6ed71c65dd068a6b8452acb58269 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release +dist/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# lock file +package-lock.json +yarn.lock diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..cdf8c21921f341830971717ab9792144200d43f5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Zoron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a4ac9659b8b05e477e1a1f8c3c1ea591147365ac..1e193ad7614a0e6bc4676b8278cf1ae31d9fc27e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,297 @@ -# lottery +[![Build Status](https://travis-ci.com/fralonra/lottery-wheel.svg?branch=master)](https://travis-ci.com/fralonra/lottery-wheel) +[![npm version](https://img.shields.io/npm/v/lottery-wheel.svg)](https://www.npmjs.com/package/lottery-wheel) +[![Greenkeeper badge](https://badges.greenkeeper.io/fralonra/lottery-wheel.svg)](https://greenkeeper.io/) -H2U-lottery \ No newline at end of file +# lottery-wheel + +A library helps you performing a wheel for lottery game. Using [Snap.svg](https://github.com/adobe-webplatform/Snap.svg) and [anime.js](https://github.com/juliangarnier/anime/). + +[demo](https://fralonra.github.io/lottery-wheel/demo/) + +# Usage + +```bash +npm install lottery-wheel +``` +Or download the latest [release](https://github.com/fralonra/lottery-wheel/releases). + +Then link `lottery-wheel.min.js` or `lottery-wheel.js` in your HTML. +```html + +``` + +Supposed you have an element whose id is 'wheel' in your html file. +```html + +``` + +Then you can do the following to create a wheel: +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: [{ + text: 'apple', + chance: 20 + }, { + text: 'banana' + }, { + text: 'orange' + }, { + text: 'peach' + }], + onSuccess(data) { + console.log(data.text); + } +}); +``` + +# API + +## Methods + +### constructor(option) + +More for `option`, see [below](#options). + +### draw() +To manually render the wheel when the `draw` property is set to false. +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: ['Beijing', 'London', 'New York', 'Tokyo'], + draw: false +}); +setTimeout(() => { + wheel.draw(); +}, 2000); +``` + +## Options + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| el | The element where the wheel mounted. [Details](#el). | Object | - | +| data | An array of prizes. [Details](#data). | Array | - | +| pos | The top-left corner of the wheel related to its parent element (the `el` element). | Array | [0, 0] +| radius | The radius of the wheel in `px`. | Number | 100 | +| buttonText | The text on the button. | String | 'Draw' | +| fontSize | The size of text for prizes. | Number | (auto generate) | +| buttonWidth | The width of the button in `px`. | Number | 50 | +| buttonFontSize | The size of text on the button. | Number | (auto generate) | +| limit | The maxium times the wheel can be run. | Number | 0 (unlimited) | +| duration | How long will the animation last in millseconds. | Number | 5000 | +| turn | The minimum amount of circles the wheel will turn during the animation. | Number | 4 | +| draw | If true, the wheel will be rendered immediately the instance created. Otherwise, you should call [draw](#draw) to manually render it. | Boolean | true | +| clockwise | If true, the rotation movement will be clockwise. Otherwise, it will be counter-clockwise. | Boolean | true | +| theme | The color preset to be used. [Details](#themes). | String | 'default' | +| image | Allow you to render the wheel using image resources. See [image](#image). | Object | - | +| color | An object used to override the color in the current theme. See [themes](#themes) | Object | - | +| onSuccess | The callback function called when a prize is drawn successfully. [Details](#onsuccess). | Function | - | +| onFail | The callback function called when trying to draw prize while has already drawn `limit` times. [Details](#onfail). | Function | - | +| onButtonHover | The function called when the mouse moves over the button. [Details](#onbuttonhover) | Function | - | + +### el +The `el` property defines the element where to render the wheel. You should pass a +DOM Element to it: +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: [] +}); +``` + +### data +The `data` property use an array to define the things relating to the lottery game itself. The length of the array must between 3 and 12. + +The simplest way is to put the name of each prize in an array: +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: ['Beijing', 'London', 'New York', 'Tokyo'] +}); +``` +It will generate the following wheel with default [options](#options). Every prizes take the same chance to be drawn, as the program will create four 'prize' objects with their `text` property set to the string in `data` array and `chance` property to `1` automatically. + +![](/doc/images/data.png) + +You can also custom each prize by making it an object. The properties for the 'prize' object are listed [here](#prize-object). +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: [{ + text: 'Beijing', + chance: 5 + }, { + text: 'London', + chance: 4 + }, 'New York', 'Tokyo'] +}); +``` + +### onSuccess +The callback function called when a prize is drawn successfully. + +| Parameter | Description | Type | +| --- | --- | --- | +| data | The drawn '[prize](#prize-object)' object. | Object | + +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: ['prize A', 'prize B', 'prize C', 'prize D'], + onSuccess(data) { + alert(`Congratulations! You picked up ${data.text}`); + } +}); +``` + +### onFail +The callback function called when trying to draw prize while has already drawn the maximum times (defined in `limit`). Notice that by the default options, one can draw unlimited times. + +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: ['prize A', 'prize B', 'prize C', 'prize D'], + limit: 1, + onFail() { + alert('You have no more chance to draw'); + } +}); +``` +In this case, if one has already drawn a prize, the next time he clicks the button the alert dialog will be shown. + +### onButtonHover +Called when the mouse is moving over the button. + +| Parameter | Description | Type | +| --- | --- | --- | +| anime | Refer to animejs. See the [doc](https://github.com/juliangarnier/anime) for usage.| | +| button | Refer to the Snap [Element](http://snapsvg.io/docs/#Element) where the button lies. | Object | + +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: ['prize A', 'prize B', 'prize C', 'prize D'], + onButtonHover(anime, button) { + anime({ + targets: button.node, + scale: 1.2, + duration: 500 + }); + } +}); +``` + +## Prize Object + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| text | The name for the prize | String | '' | +| chance | The probability the prize to be drawn. The higher the value, the more chances the prize to be picked up. The probability is actually calculated by the formula `probability = 1 * chance / (sum of every prize's chance)` | Number | 1 | +| color | The background color for the prize (will override `color.prize` of Wheel). | String | - | +| fontColor | The color of the text (will override `color.fontColor` of Wheel). | String | - | +| fontSize | The size of the text (will override `fontSize` of Wheel). | Number | - | + +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: [{ + text: 'Beijing', + color: 'silver', + fontSize: 24 + }, { + text: 'London', + fontColor: '#008000' + }, 'New York', 'Tokyo'] +}); +``` +The above code will result the following wheel: + +![](/doc/images/prize.png) + +## Themes + +A theme is an object where stores the colors used in the wheel. It has following properties: +* border: background color for the wheel's border. +* prize: background color for the prize part. +* button: background color for the button. +* line: color for the line between prize parts. +* prizeFont: color for prize text. +* buttonFont: color for button text. + +There are three themes preseted: + +* default +```javascript +default: { + border: 'red', + prize: 'gold', + button: 'darkorange', + line: 'red', + prizeFont: 'red', + buttonFont: 'white' +} +``` + +* light +```javascript +light: { + border: 'orange', + prize: 'lightyellow', + button: 'tomato', + line: 'orange', + prizeFont: 'orange', + buttonFont: 'white' +} +``` +![theme light](/doc/images/theme-light.png) + +* dark +```javascript +dark: { + border: 'silver', + prize: 'dimgray', + button: 'darkslategray', + line: 'silver', + prizeFont: 'silver', + buttonFont: 'lightyellow' +} + ``` +![theme dark](/doc/images/theme-dark.png) + +You can also change the color by setting `color` property. +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: ['Beijing', 'London', 'New York', 'Tokyo'], + theme: 'dark', + color: { + button: '#fef5e7', + buttonFont: '#34495e' + } +}); +``` +![setting color](/doc/images/color.png) + +## Image +The `image` property lets you render the wheel using the existing resources by setting an object. It will make an `image` SVG element and it supports jpeg, png and svg formats. + +| Property | Description | Type | +| --- | --- | --- | +| turntable | The image for the turntable. | String | +| button | The image for the button. It's width is controled by `buttonWidth` property and the aspect ratio will be preserved. Centered in the turntable by default. | String | +| offset | The y-axis offsets for the button. If negative, the button moves up. | Number | + +Here's an example of how it looks like when using the images in [/doc/images](https://github.com/fralonra/lottery-wheel/tree/master/doc/images) folder in this repo. +```javascript +const wheel = new Wheel({ + el: document.getElementById('wheel'), + data: ['Prize A', 'Prize B', 'Prize C', 'Prize D', 'Prize E', 'Prize F'], + image: { + turntable: 'turntable.png', + button: 'button.png', + offset: -10 + }, +}); +``` +![image example](/doc/images/image.png) diff --git a/demo/index.css b/demo/index.css new file mode 100644 index 0000000000000000000000000000000000000000..71411c480d404ef189e521b5345c91370839e504 --- /dev/null +++ b/demo/index.css @@ -0,0 +1,84 @@ +* { + box-sizing: border-box; + outline: 0; + text-decoration: none; + word-wrap: break-word; +} +html { + font-size: 100%; + line-height: 1.5; +} +@media (max-width: 960px) { + html { + font-size: 80%; + } +} +body { + margin: 0; + padding: 0; + overflow-x: hidden; +} +a { + color: #1e90ff; +} +header { + width: 100%; + padding-bottom: 2rem; + background: #24292e; + color: #f5f5f5; + text-align: center; +} +section { + width: 100%; + margin-bottom: 0.5rem; +} +.wrapper { + display: flex; + flex-flow: column; + align-items: center; + width: 100%; + height: 100%; +} +.main { + display: flex; + flex-flow: column; + align-items: center; + width: 66%; + padding: 1rem; + background: #f5f5f5; +} +.section-content { + +} +.section-demo { + display: flex; + flex-flow: row; + align-items: flex-start; + width: 100%; +} +.code { + flex: 0 0 60%; + width: 60%; + overflow-x: auto; +} +.code code { + width: 100%; +} +.showcase { + flex: 0 0 40%; + width: 40%; + padding: 0.75rem; +} +@media (max-width: 960px) { + .main { + width: 100%; + } + .section-demo { + flex-flow: column; + align-items: center; + } + .code, .showcase { + flex: 0 0 100%; + width: 100%; + } +} diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000000000000000000000000000000000000..ec414a97b3c214a10ef188aa55be47d3540cc896 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,193 @@ + + + + + + Lottery Wheel + + + + + + + +
+
+

Lottery Wheel

+

A library helps you performing a wheel for lottery game.

+ REPO & DOC +
+ + Fork me on GitHub + +
+
+

Usage

+
+ Install the package via npm: +
npm install lottery-wheel
+ Or download the latest Release. +
+
+
+

API

+
+ Please visit repo + to see guides. +
+
+
+

Basic Examples

+
+ Show the drawn prize: +
+
+
+

+  new Wheel({
+    el: document.getElementById('wheel1'),
+    data: ['prize A', 'prize B', 'prize C', 'prize D'],
+    onSuccess(data) {
+      alert(data.text);
+    }
+  });
+              
+
+
+ +
+ +
+
+
+
+ Prizes with different probability to be drawn: +
+
+
+

+  new Wheel({
+    el: document.getElementById('wheel2'),
+    data: [{
+      text: '50%',
+      chance: 5
+    }, {
+      text: '20%',
+      chance: 2
+    }, '10%', '10%', '10%']
+  });
+              
+
+
+ +
+ +
+
+
+
+ Can only draw one time: +
+
+
+

+  new Wheel({
+    el: document.getElementById('wheel3'),
+    data: ['prize A', 'prize B', 'prize C', 'prize D'],
+    limit: 1,
+    onFail() {
+      alert('You have no more chance to draw');
+    }
+  });
+              
+
+
+ +
+ +
+
+
+
+ Custom styles: +
+
+
+

+  new Wheel({
+    el: document.getElementById('wheel4'),
+    data: [{
+      text: 'Beijing',
+      color: 'silver',
+      fontSize: 24
+    }, {
+      text: 'London',
+      fontColor: '#008000'
+    }, 'New York', 'Tokyo'],
+    theme: 'light',
+    radius: 150,
+    buttonWidth: 75,
+    color: {
+      button: '#fef5e7',
+      buttonFont: '#34495e'
+    }
+  });
+              
+
+
+ +
+ +
+
+
+
+ + diff --git a/doc/images/button.png b/doc/images/button.png new file mode 100644 index 0000000000000000000000000000000000000000..b69b4ebd5864f2f36e5462a3e4a214826a70d6c4 Binary files /dev/null and b/doc/images/button.png differ diff --git a/doc/images/color.png b/doc/images/color.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc96562516601b6d0c4b882d545f4f100120e05 Binary files /dev/null and b/doc/images/color.png differ diff --git a/doc/images/data.png b/doc/images/data.png new file mode 100644 index 0000000000000000000000000000000000000000..ab16e0143628ac713589eed54b4579212c8cdf4c Binary files /dev/null and b/doc/images/data.png differ diff --git a/doc/images/image.png b/doc/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..12376a8a5bbf9591c33ed3eae2e06e455148fe1e Binary files /dev/null and b/doc/images/image.png differ diff --git a/doc/images/prize.png b/doc/images/prize.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e3372f882881b5cc9fe9a0fc01b7eeb96cea0d Binary files /dev/null and b/doc/images/prize.png differ diff --git a/doc/images/theme-dark.png b/doc/images/theme-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..dfd8b60e5502d72def5ff2a1309ae2b4b6a0a59c Binary files /dev/null and b/doc/images/theme-dark.png differ diff --git a/doc/images/theme-light.png b/doc/images/theme-light.png new file mode 100644 index 0000000000000000000000000000000000000000..92fead89b3b467764970eaeffaca0668917a2bca Binary files /dev/null and b/doc/images/theme-light.png differ diff --git a/doc/images/turntable.png b/doc/images/turntable.png new file mode 100644 index 0000000000000000000000000000000000000000..c00e63c01eed68b14b365f87577f506c46a95fc6 Binary files /dev/null and b/doc/images/turntable.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..b4cd9c267d384436818bd6eac769e1af0f2f1ecd --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "lottery-wheel", + "version": "2.0.1", + "description": "A library helps you performing a wheel for lottery game.", + "main": "index.js", + "scripts": { + "build": "rollup -c rollup.config.js", + "lint": "standard --fix src/*.js", + "prepare": "npm run build", + "test": "npm run lint", + "start": "node src/index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fralonra/lottery-wheel.git" + }, + "keywords": [ + "browser", + "javascript", + "turntable", + "lottery" + ], + "author": "zoron (https://github.com/fralonra/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/fralonra/lottery-wheel/issues" + }, + "homepage": "https://github.com/fralonra/lottery-wheel#readme", + "dependencies": {}, + "devDependencies": { + "animejs": "^3.0.0", + "rollup": "^1.1.0", + "rollup-plugin-commonjs": "^10.0.0", + "rollup-plugin-filesize": "^6.0.0", + "rollup-plugin-node-resolve": "^5.0.0", + "rollup-plugin-terser": "^5.0.0", + "snapsvg": "^0.5.1", + "snazzy": "^8.0.0", + "standard": "^14.0.0" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000000000000000000000000000000000000..2ad1145de96e90e23c2c9ec8098ef2d874f00e99 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,31 @@ +import { terser } from 'rollup-plugin-terser' +import filesize from 'rollup-plugin-filesize' +import resolve from 'rollup-plugin-node-resolve' +import commonjs from 'rollup-plugin-commonjs' + +export default [{ + input: 'src/index.js', + plugins: [ + commonjs(), + resolve(), + filesize() + ], + output: { + file: 'dist/lottery-wheel.js', + format: 'umd', + name: 'Wheel' + } +}, { + input: 'src/index.js', + plugins: [ + commonjs(), + resolve(), + terser(), + filesize() + ], + output: { + file: 'dist/lottery-wheel.min.js', + format: 'umd', + name: 'Wheel' + } +}] \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4964284c8dc4383d59325a238f3c032a45522888 --- /dev/null +++ b/src/index.js @@ -0,0 +1,407 @@ +import anime from 'animejs' +import Snap from 'snapsvg' + +const count = Symbol('count') // 已抽奖次数 +const deg = Symbol('deg') // pie 夹角 +const rotation = Symbol('rotation') // 当前转动角度 +const weight = Symbol('weight') // 权重 +const weightSum = Symbol('weight-sum') // 权重总和 +const turntable = Symbol('turntable') // 转盘元素 +const button = Symbol('button') // 按钮元素 +const checkPrize = Symbol('check-prize') // 检查数据函数 +const drawDefault = Symbol('draw-default') // 默认绘制函数 +const drawResource = Symbol('draw-resource') // 素材绘制函数 +const drawTurntable = Symbol('draw-turntable') // 绘制转盘函数 +const drawButton = Symbol('draw-button') // 绘制按钮函数 +const animeFunc = Symbol('anime-func') // 动画函数 +const run = Symbol('run') // 启动转盘函数 +const running = Symbol('running') // 转盘正在转动 +const baseFontSize = 16 + +const themes = { + default: { + border: 'red', + prize: 'gold', + button: 'darkorange', + line: 'red', + prizeFont: 'red', + buttonFont: 'white' + }, + light: { + border: 'orange', + prize: 'lightyellow', + button: 'tomato', + line: 'orange', + prizeFont: 'orange', + buttonFont: 'white' + }, + dark: { + border: 'silver', + prize: 'dimgray', + button: 'darkslategray', + line: 'silver', + prizeFont: 'silver', + buttonFont: 'lightyellow' + } +} + +class Wheel { + constructor (option = {}) { + const self = this + self.option = { + pos: [0, 0], // 左上角坐标 + radius: 100, // 半径 + buttonWidth: 50, // 按钮宽度 + buttonDeg: 80, // 顶针夹角 + buttonText: 'Draw', // 按钮文字 + textBottomPercentage: 0.6, // 文字底部对于圆半径的百分比 + limit: 0, // 抽奖限定次数 + duration: 5000, // 转动持续时间 + turn: 4, // 最小转动圈数 + clockwise: true, // 顺时针旋转 + draw: true, // 立刻绘制 + theme: 'default' // 主题 + } + Object.keys(option).forEach(function (k) { + self.option[k] = option[k] + }) + + if (!self.option.el) throw new Error('el is undefined in Wheel') + if (!self.option.data) throw new Error('data is undefined in Wheel') + + self.doc = self.option.el.ownerDocument + self[count] = 0 + self[rotation] = 0 + self[weight] = [] + self[weightSum] = 0 + self[running] = false + + self[checkPrize]() + + if (self.option.draw) self.draw() + } + + [checkPrize] () { + const self = this + const opt = self.option + for (let i in opt.data) { + let d = opt.data[i] + if (typeof d === 'string') { + opt.data[i] = { + text: d, + chance: 1 + } + } + if (!opt.data[i].text) opt.data[i].text = i + if (!opt.data[i].chance) opt.data[i].chance = 1 + + self[weight].push(Number(opt.data[i].chance)) + self[weightSum] += Number(opt.data[i].chance) + } + } + + draw () { + const self = this + const opt = self.option + if (!opt.el) throw new Error('el is undefined in Wheel') + if (!opt.data) throw new Error('data is undefined in Wheel') + + const center = opt.pos.map(p => p + opt.radius) + opt.center = center + + const svg = Snap(opt.el) + svg.node.style.width = String(opt.radius * 2) + 'px' + svg.node.style.height = String(opt.radius * 2) + 'px' + + self[deg] = 360 / opt.data.length + + // image resource provided? + if (opt.image) self[drawResource](svg) + else self[drawDefault](svg) + + self[animeFunc]() + } + + [drawDefault] (svg) { + const self = this + if (self[turntable] && self[button]) return + + const opt = self.option + + // theme + const theme = themes[opt.theme] ? opt.theme : 'default' + if (!opt.color) opt.color = {} + Object.keys(themes[theme]).forEach(k => { + if (!opt.color[k]) opt.color[k] = themes[theme][k] + }) + + // params caculate + if (!opt.inRadius) { + opt.inRadius = getInRadius(opt.radius) + } else if (opt.inRadius > opt.radius) { + opt.inRadius = opt.radius + } + + // draw turntable + self[drawTurntable](svg) + + // draw button + self[drawButton](svg) + } + + [drawResource] (svg) { + const self = this + const opt = self.option + + const res = opt.image + if (typeof res === 'object' && Object.keys(res).length > 0) { + if (res.turntable && typeof res.turntable === 'string') { + self[turntable] = svg.image(res.turntable, opt.pos[0], opt.pos[1], opt.radius * 2, opt.radius * 2) + } + if (res.button && typeof res.button === 'string') { + if (!res.offset || typeof res.offset !== 'number') res.offset = 0 + const size = getImageSize(res.button, svg, self.doc) + const buttonHeight = size[1] * opt.buttonWidth / size[0] + + self[button] = svg.image(res.button, opt.center[0] - opt.buttonWidth / 2, + opt.center[1] + res.offset - Math.round(buttonHeight / 2), opt.buttonWidth, buttonHeight) + } + } + + return self[drawDefault](svg) + } + + [drawTurntable] (svg) { + const self = this + if (self[turntable]) return + + const opt = self.option + + // draw circle + let obj = svg.circle(opt.center[0], opt.center[1], opt.radius) + obj.attr({ + fill: opt.color.border + }) + + obj = svg.circle(opt.center[0], opt.center[1], opt.inRadius) + obj.attr({ + fill: opt.color.prize + }) + + // draw pie + const len = opt.data.length + self[turntable] = svg.g() + if (len < 2 || len > 12) throw new Error('data.length must between 3 and 12') + for (let i in opt.data) { + const d = opt.data[i] + const r = opt.inRadius + + const [pathD, dLen] = describeArc(opt.center[0], opt.center[1], r, -self[deg] / 2, self[deg] / 2) + const pie = svg.path(pathD) + pie.attr({ + fill: d.color ? d.color : opt.color.prize, + stroke: opt.color.line, + strokeWidth: 2 + }) + + let fontSize = d.fontSize ? d.fontSize : (opt.fontSize ? opt.fontSize : baseFontSize) + let textSum = 0 // a-z0-9 为 1,其他为 2 + for (let i = 0; i < d.text.length; ++i) { + if (d.text[i].match(/\w/)) { + textSum += 1 + } else textSum += 2 + } + if (!opt.fontSize && !d.fontSize) { + fontSize = fontSize * textSum / 2 > dLen * opt.textBottomPercentage ? dLen * opt.textBottomPercentage / textSum * 2 : fontSize + } + const text = svg.text(opt.center[0], opt.pos[1] + opt.radius - (r * opt.textBottomPercentage * Snap.cos(self[deg] / 2)) - fontSize, d.text) + text.attr({ + fill: d.fontColor ? d.fontColor : opt.color.prizeFont, + fontSize: opt.fontSize ? opt.fontSize : fontSize + }) + const box = text.node.getBoundingClientRect() + text.transform(new Snap.Matrix().translate(-Math.round(box.width / 2), 2)) + + const g = svg.g(pie, text).transform(new Snap.Matrix().rotate(self[deg] * Number(i), opt.center[0], opt.center[1])) + self[turntable].add(g) + } + } + + [drawButton] (svg) { + const self = this + if (self[button]) return + + const opt = self.option + + if (opt.button && typeof opt.button === 'string') { + return + } + + const r = opt.buttonWidth / 2 + const center = opt.center + const deg = (180 - opt.buttonDeg) / 2 + const [pathArc, , , end] = describeArc(center[0], center[1], r, deg - 360, 360 - deg) + const top = [center[0], center[1] - r / Snap.cos(deg)] + const pathD = `${pathArc}L${top[0]},${top[1]}L${end.x},${end.y}L${center[0]},${center[1]}` + const b = svg.path(pathD) + b.attr({ + fill: opt.color.button, + filter: svg.filter(Snap.filter.shadow(0, 3, 3, 'black', 0.5)) + }) + + let text = null + if (opt.buttonText !== '') { + const maxLen = r * 2 * 0.8 + let fontSize = opt.buttonFontSize ? opt.buttonFontSize : baseFontSize + let textSum = 0 + for (let i = 0; i < opt.buttonText.length; ++i) { + if (opt.buttonText[i].match(/\w/)) { + textSum += 1 + } else textSum += 2 + } + if (!opt.buttonFontSize) { + fontSize = fontSize * textSum / 2 > maxLen ? maxLen / textSum * 2 : fontSize + } + text = svg.text(center[0], center[1], opt.buttonText) + text.attr({ + fill: opt.color.buttonFont, + fontSize: opt.buttonFontSize ? opt.buttonFontSize : fontSize + }) + const box = text.node.getBoundingClientRect() + text.transform(new Snap.Matrix().translate(-Math.round(box.width / 2), 2)) + } + + self[button] = svg.g(b, text) + } + + [animeFunc] () { + const self = this + const opt = self.option + + self[turntable].node.style['transform-origin'] = 'center' + + self[button].node.style.cursor = 'pointer' + self[button].node.style['transform-origin'] = 'center' + self[button].hover(() => { + if (opt.onButtonHover && typeof opt.onButtonHover === 'function') { + return opt.onButtonHover(anime, self[button]) + } + anime({ + targets: self[button].node, + scale: 1.2, + duration: 500 + }) + }, () => { + anime({ + targets: self[button].node, + scale: 1, + duration: 500 + }) + }) + self[button].click(() => { + self[run]() + }) + } + + [run] () { + const self = this + if (self[running]) return + const opt = self.option + if (!opt.el) throw new Error('el is undefined in Wheel') + if (!opt.data) throw new Error('data is undefined in Wheel') + + // 抽奖次数超过 limit + if (opt.limit > 0 && self[count] >= opt.limit) { + return (opt.onFail && typeof opt.onFail === 'function') ? opt.onFail() : null + } + + // rotate animation + const runAnime = (pie) => { + if (self[rotation] > 0) { + const revision = 360 - (self[rotation] % 360) + self[rotation] += revision + } + self[rotation] += getRotation(pie, self[deg], opt.turn) + anime({ + targets: self[turntable].node, + rotate: opt.clockwise ? String(self[rotation]) + 'deg' : '-' + String(self[rotation]) + 'deg', + duration: opt.duration, + begin () { + self[running] = true + }, + complete () { + self[running] = false + ++self[count] + if (opt.onSuccess && typeof opt.onSuccess === 'function') { + const d = opt.clockwise ? opt.data[(opt.data.length - pie) % opt.data.length] : opt.data[pie] + opt.onSuccess(d) + } + } + }) + } + + const random = Math.random() * self[weightSum] + let randomWeight = 0; let pie = 0 + for (let i in self[weight]) { + randomWeight += self[weight][i] + if (randomWeight > random) { + pie = Number(i) + runAnime(pie) + break + } + } + } +} + +// 获取内圈半径 +function getInRadius (radius) { + if (radius < 50) return radius + if (radius < 100) return radius - 10 + return Math.round(radius / 10) * 9 +} + +function polarToCartesian (centerX, centerY, radius, angleInDegrees) { + const angleInRadians = (angleInDegrees - 90) * Math.PI / 180 + return { + x: centerX + (radius * Math.cos(angleInRadians)), + y: centerY + (radius * Math.sin(angleInRadians)) + } +} + +function describeArc (x, y, radius, startAngle, endAngle) { + const start = polarToCartesian(x, y, radius, endAngle) + const end = polarToCartesian(x, y, radius, startAngle) + + const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1 + + const d = [ + 'M', start.x, start.y, + 'A', radius, radius, 0, largeArcFlag, 0, end.x, end.y, + 'L', x, y, + 'L', start.x, start.y + ].join(' ') + const l = start.x - end.x // 扇形最大宽度 + return [d, l, start, end] +} + +// 获取旋转角度 +function getRotation (i, deg, minTurn) { + return minTurn * 360 + i * deg +} + +// 获取图片尺寸 +function getImageSize (src, svg, doc) { + const img = doc.createElement('img') + const body = doc.body + body.appendChild(img) + img.src = src + + const size = [ + img.width || img.naturalWidth || img.getBoundingClientRect().width || 50, + img.height || img.naturalHeight || img.getBoundingClientRect().height || 50 + ] + doc.body.removeChild(img) + return size +} + +export default Wheel