├── README.md ├── app.js ├── app.json ├── app.wxss ├── pages └── index │ ├── assets │ ├── gift.png │ └── logo.png │ ├── index.js │ ├── index.wxml │ └── index.wxss ├── project.config.json └── utils ├── Animation.js ├── Pointer.js ├── Wheel.js └── utils.js /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 大转盘营销活动大家应该都不陌生那如何用小程序实现呢? 3 | 第一个版本的大转盘都是用图片做的,奖品等信息都是不无法修改的,说白了就是没啥实际用途,作者我就直接用canvas撸了一个全手工绘制的大转盘分享给大家。此处应有掌声 : ) 4 | 5 | ### 效果图 6 | > 因为gif动态效果图实在是太大了,就截取了两张图片给大家看 7 | 8 | ![](https://upload-images.jianshu.io/upload_images/9260441-fa18737c987a30ee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 9 | 10 | ![](https://upload-images.jianshu.io/upload_images/9260441-7aa220d8ccdfb2de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 11 | 12 | ### 项目结构 13 | ![](https://upload-images.jianshu.io/upload_images/9260441-54271569eae57ede.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 15 | ### 运行流程 16 | * 获取所有奖品列表 17 | * 绘制指针和转盘 18 | * 绑定指针区域点击事件 19 | * 点击指针开始转动 20 | * 请求后端获取抽中的奖品 21 | * 调用大转盘停止方法把奖品传进去 22 | * 大转盘停止转动 23 | * 展示奖品和信息 24 | 25 | ### 关键代码 26 | 绘制大转盘和绑定指针点击事件 27 | ```javascript 28 | // 启用事件 29 | point.inputEvent = true; 30 | point.onInputDown = run; 31 | // 更新动画 32 | var update = function () { 33 | // 清空 34 | context.clearRect(0, 0, w, h); 35 | // 画转盘 36 | wheel.draw(context); 37 | // 画指针 38 | point.draw(context); 39 | 40 | // 更新数据 41 | animation.draw(context); 42 | // 更新数据 43 | animation.update(); 44 | 45 | // 获取手指点击 46 | var touch = that.data.touch; 47 | if (point.inputEvent && touch.isPressed && point.onInputDown) { 48 | // 如果点击到了指针 49 | if (point.contains(touch)) { 50 | // 调用点击回调方法 51 | point.onInputDown(); 52 | } 53 | } 54 | 55 | // 绘图 56 | context.draw() 57 | 58 | }; 59 | setInterval(update, 1000 / fps, 1000 / fps); 60 | ``` 61 | 62 | 点击指针之后的回调处理 63 | 使用setTimeout 模拟后端返回结果 64 | 使用stopTo 方法来让大转盘停止到某一个奖品 65 | ```javascript 66 | // 开始转 67 | function run() { 68 | // 避免重复调用 69 | if (animation.isRun) return; 70 | // 当动画完成时 71 | animation.onComplete = function (prize) { 72 | wx.showToast({ 73 | image: prize.img, 74 | title: prize.text, 75 | duration: 3000, 76 | mask: true, 77 | }) 78 | }; 79 | 80 | // 开始转 81 | animation.run(); 82 | 83 | // 模拟后台返回数据 84 | setTimeout(function () { 85 | // 随机一个奖品 86 | var prizeIndex = utils.getRandom(slicePrizes.length - 1); 87 | // 计算奖品角度 88 | animation.stopTo(prizeIndex); 89 | }, 3000); 90 | } 91 | ``` 92 | 转动处理的构造方法 93 | ```javascript 94 | function Animation(circle, screen) { 95 | this.circle = circle; 96 | this.screen = screen; 97 | // 角速度 98 | this.speed = 0; 99 | // 最大速度 100 | this.maxSpeed = 10; 101 | // 摩擦力 102 | this.friction = 0.98; 103 | // 加速度 104 | this.acceleration = 0.5; 105 | // 是否开始运行 106 | this.isRun = false; 107 | // 圈数 108 | this.rounds = 5; 109 | // 角度 110 | this.degrees = 0; 111 | // 当前角度 112 | this.angle = -90; 113 | // 开始减速 114 | this.speedDown = false; 115 | // 开始加速 116 | this.speedUp = true; 117 | // 顺时针还是逆时针 118 | this.anticlockwise = false; 119 | // 完成 120 | this.onComplete = null; 121 | // debug 122 | this.isDebug = false; 123 | } 124 | ``` 125 | 转动效果,先加速》匀速一段时间》调用stopTo方法后》减速》停止 126 | ```javascript 127 | 128 | Animation.prototype.update = function () { 129 | if (this.isRun) { 130 | // 到达奖品区 131 | var isJoin = this.prize && this.angle > this.minAngle && this.angle < this.maxAngle; 132 | // 是否要减速 133 | if (isJoin) { 134 | this.speedDown = true; 135 | } 136 | 137 | // 是否要停止加速 138 | if (this.speed >= this.maxSpeed) { 139 | this.speedUp = false; 140 | } 141 | 142 | // 加速 143 | if (this.speedUp) { 144 | this.speed += this.acceleration; 145 | } 146 | 147 | // 减速 148 | if (this.speedDown) { 149 | if (this.speed <= 2 && isJoin) { 150 | this.isRun = false; 151 | this.speed = 0; 152 | if (this.onComplete) this.onComplete(this.prize); 153 | } else if (this.speed <= 1) { 154 | this.speed = 1; 155 | } else { 156 | this.speed *= this.friction; 157 | } 158 | } 159 | 160 | this.angle += this.speed; 161 | // 转动角度 162 | if (Math.abs(this.angle) > 360) { 163 | this.angle -= 360; 164 | } 165 | // 旋转方向 166 | if (this.anticlockwise) { 167 | this.circle.rotation += (Math.PI / 180) * this.speed; 168 | } else { 169 | this.circle.rotation -= (Math.PI / 180) * this.speed; 170 | } 171 | } 172 | }; 173 | ``` 174 | 175 | 大转盘的绘制 176 | ```javascript 177 | 178 | Circle.prototype.draw = function (context) { 179 | // 保存 180 | context.save(); 181 | // 移动到圆心 182 | context.translate(this.x, this.y); 183 | // 旋转 184 | context.rotate(this.rotation); 185 | // 缩放 186 | // context.scale(this.scaleX, this.scaleY); 187 | 188 | if (this.img) { 189 | var imgX = -this.width / 2; 190 | var imgY = -this.height / 2; 191 | context.drawImage(this.img, imgX, imgY); 192 | } 193 | var colors = this.colors; 194 | var size = this.size; 195 | var angle = this.angle; 196 | 197 | var r = this.radius; 198 | 199 | // 画扇形 200 | for (var i = 0; i < size; i++) { 201 | context.beginPath() 202 | context.moveTo(0, 0) 203 | context.arc(0, 0, r, 0, angle * Math.PI / 180) 204 | context.setFillStyle(colors[i % colors.length]) 205 | context.fill() 206 | context.rotate(angle * Math.PI / 180); 207 | } 208 | 209 | // 画图案 210 | // 计算圆上任意一点 211 | var offset = this.radius / 2; 212 | var offsetA = angle / 2; 213 | var offsetX = Math.cos(offsetA * Math.PI / 180) * offset; 214 | var offsetY = Math.sin(offsetA * Math.PI / 180) * offset; 215 | for (var i = 0; i < size; i++) { 216 | var prize = this.slicePrizes[i] 217 | context.save(); 218 | 219 | context.translate(offsetX, offsetY); 220 | 221 | context.rotate((180 - ((180 - angle) / 2)) * Math.PI / 180); 222 | context.drawImage(prize.img, -10, -30, this.prizeWidth, this.prizeHeight); 223 | 224 | // 写文字 225 | context.setFontSize(this.prizeFontSize) 226 | context.setFillStyle(this.prizeFontStyle) 227 | context.textAlign = "center"; 228 | context.fillText(prize.text, 0, (offset - 25) * -1) 229 | 230 | // 话圆点参考 231 | // context.beginPath() 232 | // context.arc(0, 0, 2, 0, 2 * Math.PI) 233 | // context.setFillStyle('red') 234 | // context.fill() 235 | 236 | context.restore() 237 | context.rotate(angle * Math.PI / 180); 238 | } 239 | 240 | // 还原 241 | context.restore(); 242 | }; 243 | ``` 244 | 指针和跑马灯的绘制 245 | ```javascript 246 | 247 | Circle.prototype.draw = function (context) { 248 | // 保存 249 | context.save(); 250 | // 移动到圆心 251 | context.translate(this.x, this.y); 252 | // 旋转 253 | context.rotate(this.rotation); 254 | // 缩放 255 | // context.scale(this.scaleX, this.scaleY); 256 | 257 | if (this.img) { 258 | var imgX = -this.width / 2; 259 | var imgY = -this.height / 2; 260 | context.drawImage(this.img, imgX, imgY); 261 | } 262 | 263 | // 指针 264 | context.beginPath() 265 | context.setFillStyle('#ffffff') 266 | context.moveTo(0, (this.radius + 50) * -1); 267 | context.lineTo(-10, 0); 268 | context.lineTo(10, 0); 269 | context.fill() 270 | 271 | // 圆盘 272 | context.beginPath() 273 | context.setFillStyle('#ffffff') 274 | context.arc(0, 0, this.radius, 0, 2 * Math.PI) 275 | context.fill() 276 | 277 | // 文字 278 | context.setFontSize(30) 279 | context.setFillStyle('#ff2244') 280 | context.setTextAlign("center"); 281 | context.fillText('抽奖', 0, 10) 282 | 283 | var r = this.wheel.radius; 284 | // 跑马灯外框 F6D000 FFA200 285 | context.beginPath() 286 | context.setLineWidth(15); 287 | context.setStrokeStyle("#FFA200") 288 | context.arc(0, 0, r + 20, 0, 2 * Math.PI) 289 | context.stroke() 290 | 291 | // 跑马灯内框 F6D000 292 | context.beginPath() 293 | context.setLineWidth(10); 294 | context.setStrokeStyle("#F6D000") 295 | context.arc(0, 0, r + 8, 0, 2 * Math.PI) 296 | context.stroke() 297 | 298 | // 跑马灯灯 299 | for (var i = 0; i < 32; i++) { 300 | context.beginPath() 301 | context.setFillStyle(i % 2 == 0 ? "#ffffff" : "#FF403A") 302 | context.arc(r + 20, 0, 4, 0, 2 * Math.PI) 303 | context.fill() 304 | context.rotate((360 / 32) * Math.PI / 180); 305 | } 306 | // 还原 307 | context.restore(); 308 | }; 309 | ``` 310 | 311 | ### 其他推荐 312 | 微信小程序-欢乐夹娃娃 https://blog.100boot.cn/post/3067 313 | 314 | [![微信小程序-欢乐夹娃娃](https://blog.100boot.cn/storage/thumbnails/_signature/2MN5CRHT0NNDA9QRAFIBS6G7HS.jpg)](https://blog.100boot.cn/post/3067) 315 | 316 | ### 完整代码 317 | GitHub 地址: https://github.com/qiaohhgz/bigwheel-miniapp 318 | 319 | ### 关注我们 320 | 321 | ![](http://upload-images.jianshu.io/upload_images/9260441-fbb877c1ace32df7.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 322 | 323 | > IT实战联盟是集产品、UI设计、前后端、架构、大数据和AI人工智能等为一体的实战交流服务平台!联盟嘉宾都为各互联网公司项目的核心成员,联盟主旨是“让实战更简单”,欢迎来撩~~~ 324 | 325 | > 我们的网站: [http://100boot.cn](http://100boot.cn) 326 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: res => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | // 获取用户信息 16 | wx.getSetting({ 17 | success: res => { 18 | if (res.authSetting['scope.userInfo']) { 19 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 20 | wx.getUserInfo({ 21 | success: res => { 22 | // 可以将 res 发送给后台解码出 unionId 23 | this.globalData.userInfo = res.userInfo 24 | 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | if (this.userInfoReadyCallback) { 28 | this.userInfoReadyCallback(res) 29 | } 30 | } 31 | }) 32 | } 33 | } 34 | }) 35 | }, 36 | globalData: { 37 | userInfo: null 38 | } 39 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index" 4 | ], 5 | "window":{ 6 | "backgroundTextStyle":"light", 7 | "navigationBarBackgroundColor": "#fff", 8 | "navigationBarTitleText": "WeChat", 9 | "navigationBarTextStyle":"black" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 200rpx 0; 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /pages/index/assets/gift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiaohhgz/bigwheel-miniapp/f66349aafc39c46015bffc621fcf436db7f974f0/pages/index/assets/gift.png -------------------------------------------------------------------------------- /pages/index/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiaohhgz/bigwheel-miniapp/f66349aafc39c46015bffc621fcf436db7f974f0/pages/index/assets/logo.png -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | // pages/main/main.js 2 | var utils = require('../../utils/utils.js'); 3 | var Animation = require('../../utils/Animation.js'); 4 | var Pointer = require('../../utils/Pointer.js'); 5 | var Wheel = require('../../utils/Wheel.js'); 6 | Page({ 7 | 8 | /** 9 | * 页面的初始数据 10 | */ 11 | data: { 12 | windowWidth: 0, 13 | windowHeight: 0, 14 | wheelImg: 'assets/wheel.png', 15 | pointImg: 'assets/point.png', 16 | touch: { x: 0, y: 0, isPressed: false } 17 | }, 18 | 19 | touchMove: function (event) { 20 | 21 | }, 22 | 23 | canvasTouchStart: function (event) { 24 | var touch = event.changedTouches[0]; 25 | touch.isPressed = true; 26 | this.setData({ 27 | touch: touch 28 | }) 29 | }, 30 | 31 | canvasTouchEnd: function (event) { 32 | var touch = event.changedTouches[0]; 33 | touch.isPressed = false; 34 | this.setData({ 35 | touch: touch 36 | }) 37 | }, 38 | 39 | /** 40 | * 生命周期函数--监听页面加载 41 | */ 42 | onLoad: function (options) { 43 | var that = this; 44 | // 把设备的尺寸赋值给画布,以做到全屏效果 45 | wx.getSystemInfo({ 46 | success: function (res) { 47 | that.setData({ 48 | windowWidth: res.windowWidth, 49 | windowHeight: res.windowHeight 50 | }); 51 | }, 52 | }) 53 | }, 54 | 55 | /** 56 | * 生命周期函数--监听页面初次渲染完成 57 | */ 58 | onReady: function () { 59 | wx.setNavigationBarTitle({ 60 | title: '幸运大转盘', 61 | }) 62 | var that = this, 63 | fps = 60, 64 | slicePrizes = ["恭喜中了大奖", "50 积分", "500 积分", "谢谢参与", "200 积分", "100 积分", "150 积分", "谢谢参与"], 65 | slicePrizes = [ 66 | { text: "恭喜中了大奖", img: "assets/gift.png" }, 67 | { text: "50 积分", img: "assets/gift.png" }, 68 | { text: "500 积分", img: "assets/gift.png" }, 69 | { text: "谢谢参与", img: "assets/gift.png" }, 70 | { text: "200 积分", img: "assets/gift.png" }, 71 | { text: "100 积分", img: "assets/gift.png" }, 72 | { text: "150 积分", img: "assets/gift.png" }, 73 | { text: "谢谢参与", img: "assets/gift.png" } 74 | ], 75 | w = this.data.windowWidth, 76 | h = this.data.windowHeight, 77 | context = wx.createCanvasContext('canvas'), 78 | wheel = new Wheel(w / 2, h / 2.5, w / 2 - 50, slicePrizes), 79 | point = new Pointer(w / 2, h / 2.5, 40, wheel), 80 | animation = new Animation(wheel, { w: w, h: h }) 81 | ; 82 | 83 | wheel.prizeWidth = 30; 84 | wheel.prizeHeight = 30; 85 | 86 | // 启用事件 87 | point.inputEvent = true; 88 | point.onInputDown = run; 89 | 90 | // 更新动画 91 | var update = function () { 92 | // 清空 93 | context.clearRect(0, 0, w, h); 94 | // 画转盘 95 | wheel.draw(context); 96 | // 画指针 97 | point.draw(context); 98 | 99 | // 更新数据 100 | animation.draw(context); 101 | // 更新数据 102 | animation.update(); 103 | 104 | // 获取手指点击 105 | var touch = that.data.touch; 106 | if (point.inputEvent && touch.isPressed && point.onInputDown) { 107 | // 如果点击到了指针 108 | if (point.contains(touch)) { 109 | // 调用点击回调方法 110 | point.onInputDown(); 111 | } 112 | } 113 | 114 | // 绘图 115 | context.draw() 116 | 117 | }; 118 | 119 | setInterval(update, 1000 / fps, 1000 / fps); 120 | 121 | // 开始转 122 | function run() { 123 | // 避免重复调用 124 | if (animation.isRun) return; 125 | // 当动画完成时 126 | animation.onComplete = function (prize) { 127 | wx.showToast({ 128 | image: prize.img, 129 | title: prize.text, 130 | duration: 3000, 131 | mask: true, 132 | }) 133 | }; 134 | 135 | // 开始转 136 | animation.run(); 137 | 138 | // 模拟后台返回数据 139 | setTimeout(function () { 140 | // 随机一个奖品 141 | var prizeIndex = utils.getRandom(slicePrizes.length - 1); 142 | // 计算奖品角度 143 | animation.stopTo(prizeIndex); 144 | }, 3000); 145 | } 146 | } 147 | }) 148 | -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /* pages/main/main.wxss */ 2 | page{ 3 | height: 100%; 4 | } 5 | 6 | canvas{ 7 | border: solid 0px black 8 | } -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true 12 | }, 13 | "compileType": "miniprogram", 14 | "libVersion": "2.1.0", 15 | "appid": "wxe4521b01e274dcc7", 16 | "projectname": "%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%B9%B8%E8%BF%90%E5%A4%A7%E8%BD%AC%E7%9B%98", 17 | "isGameTourist": false, 18 | "condition": { 19 | "search": { 20 | "current": -1, 21 | "list": [] 22 | }, 23 | "conversation": { 24 | "current": -1, 25 | "list": [] 26 | }, 27 | "game": { 28 | "currentL": -1, 29 | "list": [] 30 | }, 31 | "miniprogram": { 32 | "current": -1, 33 | "list": [] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /utils/Animation.js: -------------------------------------------------------------------------------- 1 | var utils = require('utils.js'); 2 | /** 3 | * 动画 4 | * @author qiao 5 | * @version 2017/12/30 6 | */ 7 | function Animation(circle, screen) { 8 | this.circle = circle; 9 | this.screen = screen; 10 | // 角速度 11 | this.speed = 0; 12 | // 最大速度 13 | this.maxSpeed = 10; 14 | // 摩擦力 15 | this.friction = 0.98; 16 | // 加速度 17 | this.acceleration = 0.5; 18 | // 是否开始运行 19 | this.isRun = false; 20 | // 圈数 21 | this.rounds = 5; 22 | // 角度 23 | this.degrees = 0; 24 | // 当前角度 25 | this.angle = -90; 26 | // 开始减速 27 | this.speedDown = false; 28 | // 开始加速 29 | this.speedUp = true; 30 | // 顺时针还是逆时针 31 | this.anticlockwise = false; 32 | // 完成 33 | this.onComplete = null; 34 | // debug 35 | this.isDebug = false; 36 | } 37 | 38 | Animation.prototype.run = function () { 39 | this.speedDown = false; 40 | this.speedUp = true; 41 | this.isRun = true; 42 | this.speed = 0; 43 | this.prize = null; 44 | }; 45 | 46 | Animation.prototype.draw = function (context) { 47 | // 保存 48 | context.save(); 49 | if (this.isDebug) { 50 | // 移动到圆心 51 | context.translate(10, 15); 52 | 53 | context.setFontSize(12) 54 | context.setFillStyle('black') 55 | context.fillText("angle: " + Math.round(this.angle), 0, 0); 56 | 57 | context.translate(0, 15); 58 | context.fillText("speed: " + this.speed, 0, 0); 59 | 60 | if (this.prize) { 61 | context.translate(0, 15); 62 | context.fillText("minAngle: " + this.minAngle, 0, 0); 63 | 64 | context.translate(0, 15); 65 | context.fillText("maxAngle: " + this.maxAngle, 0, 0); 66 | 67 | context.translate(0, 15); 68 | context.fillText("prize.text: " + this.prize.text, 0, 0); 69 | } 70 | } 71 | // 画logo 72 | var w = this.screen.w - 20; 73 | var h = 70; 74 | context.drawImage("assets/logo.png", 10, 10, w, h); 75 | 76 | // 还原 77 | context.restore(); 78 | }; 79 | 80 | Animation.prototype.update = function () { 81 | if (this.isRun) { 82 | // 到达奖品区 83 | var isJoin = this.prize && this.angle > this.minAngle && this.angle < this.maxAngle; 84 | // 是否要减速 85 | if (isJoin) { 86 | this.speedDown = true; 87 | } 88 | 89 | // 是否要停止加速 90 | if (this.speed >= this.maxSpeed) { 91 | this.speedUp = false; 92 | } 93 | 94 | // 加速 95 | if (this.speedUp) { 96 | this.speed += this.acceleration; 97 | } 98 | 99 | // 减速 100 | if (this.speedDown) { 101 | if (this.speed <= 2 && isJoin) { 102 | this.isRun = false; 103 | this.speed = 0; 104 | if (this.onComplete) this.onComplete(this.prize); 105 | } else if (this.speed <= 1) { 106 | this.speed = 1; 107 | } else { 108 | this.speed *= this.friction; 109 | } 110 | } 111 | 112 | this.angle += this.speed; 113 | // 转动角度 114 | if (Math.abs(this.angle) > 360) { 115 | this.angle -= 360; 116 | } 117 | // 旋转方向 118 | if (this.anticlockwise) { 119 | this.circle.rotation += (Math.PI / 180) * this.speed; 120 | } else { 121 | this.circle.rotation -= (Math.PI / 180) * this.speed; 122 | } 123 | } 124 | }; 125 | 126 | Animation.prototype.stopTo = function (prizeIndex) { 127 | var wheel = this.circle; 128 | var angle = wheel.angle; 129 | var offset = utils.getRandom(angle / 2); 130 | this.minAngle = prizeIndex * angle + offset; 131 | this.maxAngle = prizeIndex * angle + angle; 132 | this.prize = wheel.slicePrizes[prizeIndex]; 133 | }; 134 | 135 | module.exports = Animation; -------------------------------------------------------------------------------- /utils/Pointer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 大转盘指针 3 | * @author qiao 4 | * @version 2017/12/30 5 | */ 6 | function Circle(x, y, radius, wheel) { 7 | this.x = x; 8 | this.y = y; 9 | this.width = 0; 10 | this.height = 0; 11 | this.radius = radius; 12 | this.rotation = 0; 13 | this.img = null; 14 | this.scaleX = 1; 15 | this.scaleY = 1; 16 | this.inputEvent = false; 17 | this.onInputDown = null; 18 | this.wheel = wheel; 19 | } 20 | 21 | Circle.prototype.draw = function (context) { 22 | // 保存 23 | context.save(); 24 | // 移动到圆心 25 | context.translate(this.x, this.y); 26 | // 旋转 27 | context.rotate(this.rotation); 28 | // 缩放 29 | // context.scale(this.scaleX, this.scaleY); 30 | 31 | if (this.img) { 32 | var imgX = -this.width / 2; 33 | var imgY = -this.height / 2; 34 | context.drawImage(this.img, imgX, imgY); 35 | } 36 | 37 | // 指针 38 | context.beginPath() 39 | context.setFillStyle('#ffffff') 40 | context.moveTo(0, (this.radius + 50) * -1); 41 | context.lineTo(-10, 0); 42 | context.lineTo(10, 0); 43 | context.fill() 44 | 45 | // 圆盘 46 | context.beginPath() 47 | context.setFillStyle('#ffffff') 48 | context.arc(0, 0, this.radius, 0, 2 * Math.PI) 49 | context.fill() 50 | 51 | // 文字 52 | context.setFontSize(30) 53 | context.setFillStyle('#ff2244') 54 | context.setTextAlign("center"); 55 | context.fillText('抽奖', 0, 10) 56 | 57 | var r = this.wheel.radius; 58 | // 跑马灯外框 F6D000 FFA200 59 | context.beginPath() 60 | context.setLineWidth(15); 61 | context.setStrokeStyle("#FFA200") 62 | context.arc(0, 0, r + 20, 0, 2 * Math.PI) 63 | context.stroke() 64 | 65 | // 跑马灯内框 F6D000 66 | context.beginPath() 67 | context.setLineWidth(10); 68 | context.setStrokeStyle("#F6D000") 69 | context.arc(0, 0, r + 8, 0, 2 * Math.PI) 70 | context.stroke() 71 | 72 | // 跑马灯灯 73 | for (var i = 0; i < 32; i++) { 74 | context.beginPath() 75 | context.setFillStyle(i % 2 == 0 ? "#ffffff" : "#FF403A") 76 | context.arc(r + 20, 0, 4, 0, 2 * Math.PI) 77 | context.fill() 78 | context.rotate((360 / 32) * Math.PI / 180); 79 | } 80 | // 还原 81 | context.restore(); 82 | }; 83 | 84 | Circle.prototype.scale = function (x, y) { 85 | this.scaleX = x; 86 | this.scaleY = y; 87 | }; 88 | 89 | Circle.prototype.contains = function (obj) { 90 | return Circle.contains(this, obj.x, obj.y); 91 | }; 92 | 93 | Circle.contains = function (a, x, y) { 94 | // Check if x/y are within the bounds first 95 | if (a.radius > 0 && x >= a.left && x <= a.right && y >= a.top && y <= a.bottom) { 96 | var dx = (a.x - x) * (a.x - x); 97 | var dy = (a.y - y) * (a.y - y); 98 | 99 | return (dx + dy) <= (a.radius * a.radius); 100 | } 101 | else { 102 | return false; 103 | } 104 | }; 105 | 106 | Object.defineProperty(Circle.prototype, "left", { 107 | get: function () { 108 | return this.x - this.radius; 109 | }, 110 | 111 | set: function (value) { 112 | if (value > this.x) { 113 | this.radius = 0; 114 | } 115 | else { 116 | this.radius = this.x - value; 117 | } 118 | } 119 | }); 120 | 121 | Object.defineProperty(Circle.prototype, "right", { 122 | 123 | get: function () { 124 | return this.x + this.radius; 125 | }, 126 | 127 | set: function (value) { 128 | if (value < this.x) { 129 | this.radius = 0; 130 | } 131 | else { 132 | this.radius = value - this.x; 133 | } 134 | } 135 | }); 136 | 137 | Object.defineProperty(Circle.prototype, "top", { 138 | get: function () { 139 | return this.y - this.radius; 140 | }, 141 | 142 | set: function (value) { 143 | if (value > this.y) { 144 | this.radius = 0; 145 | } 146 | else { 147 | this.radius = this.y - value; 148 | } 149 | } 150 | }); 151 | 152 | Object.defineProperty(Circle.prototype, "bottom", { 153 | get: function () { 154 | return this.y + this.radius; 155 | }, 156 | set: function (value) { 157 | if (value < this.y) { 158 | this.radius = 0; 159 | } 160 | else { 161 | this.radius = value - this.y; 162 | } 163 | } 164 | }); 165 | 166 | module.exports = Circle; -------------------------------------------------------------------------------- /utils/Wheel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 大转盘 3 | * @author qiao 4 | * @version 2017/12/30 5 | */ 6 | function Circle(x, y, radius, slicePrizes) { 7 | this.x = x; 8 | this.y = y; 9 | this.width = 0; 10 | this.height = 0; 11 | this.radius = radius; 12 | this.slicePrizes = slicePrizes; 13 | this.rotation = 0; 14 | this.img = null; 15 | this.scaleX = 1; 16 | this.scaleY = 1; 17 | this.inputEvent = false; 18 | this.onInputDown = null; 19 | this.colors = ["#bb33ee", "#ffcc44", "#ff2244", "#00bb88"]; 20 | this.prizeWidth = 20; 21 | this.prizeHeight = 20; 22 | this.prizeFontSize = 14; 23 | this.prizeFontStyle = 'black'; 24 | this.size = slicePrizes.length; 25 | this.angle = 360 / this.size; 26 | } 27 | 28 | Circle.prototype.draw = function (context) { 29 | // 保存 30 | context.save(); 31 | // 移动到圆心 32 | context.translate(this.x, this.y); 33 | // 旋转 34 | context.rotate(this.rotation); 35 | // 缩放 36 | // context.scale(this.scaleX, this.scaleY); 37 | 38 | if (this.img) { 39 | var imgX = -this.width / 2; 40 | var imgY = -this.height / 2; 41 | context.drawImage(this.img, imgX, imgY); 42 | } 43 | var colors = this.colors; 44 | var size = this.size; 45 | var angle = this.angle; 46 | 47 | var r = this.radius; 48 | 49 | // 画扇形 50 | for (var i = 0; i < size; i++) { 51 | context.beginPath() 52 | context.moveTo(0, 0) 53 | context.arc(0, 0, r, 0, angle * Math.PI / 180) 54 | context.setFillStyle(colors[i % colors.length]) 55 | context.fill() 56 | context.rotate(angle * Math.PI / 180); 57 | } 58 | 59 | // 画图案 60 | // 计算圆上任意一点 61 | var offset = this.radius / 2; 62 | var offsetA = angle / 2; 63 | var offsetX = Math.cos(offsetA * Math.PI / 180) * offset; 64 | var offsetY = Math.sin(offsetA * Math.PI / 180) * offset; 65 | for (var i = 0; i < size; i++) { 66 | var prize = this.slicePrizes[i] 67 | context.save(); 68 | 69 | context.translate(offsetX, offsetY); 70 | 71 | context.rotate((180 - ((180 - angle) / 2)) * Math.PI / 180); 72 | context.drawImage(prize.img, -10, -30, this.prizeWidth, this.prizeHeight); 73 | 74 | // 写文字 75 | context.setFontSize(this.prizeFontSize) 76 | context.setFillStyle(this.prizeFontStyle) 77 | context.textAlign = "center"; 78 | context.fillText(prize.text, 0, (offset - 25) * -1) 79 | 80 | // 话圆点参考 81 | // context.beginPath() 82 | // context.arc(0, 0, 2, 0, 2 * Math.PI) 83 | // context.setFillStyle('red') 84 | // context.fill() 85 | 86 | context.restore() 87 | context.rotate(angle * Math.PI / 180); 88 | } 89 | 90 | // 还原 91 | context.restore(); 92 | }; 93 | 94 | Circle.prototype.scale = function (x, y) { 95 | this.scaleX = x; 96 | this.scaleY = y; 97 | }; 98 | 99 | Circle.prototype.contains = function (obj) { 100 | return Circle.contains(this, obj.x, obj.y); 101 | }; 102 | 103 | Circle.contains = function (a, x, y) { 104 | // Check if x/y are within the bounds first 105 | if (a.radius > 0 && x >= a.left && x <= a.right && y >= a.top && y <= a.bottom) { 106 | var dx = (a.x - x) * (a.x - x); 107 | var dy = (a.y - y) * (a.y - y); 108 | 109 | return (dx + dy) <= (a.radius * a.radius); 110 | } 111 | else { 112 | return false; 113 | } 114 | }; 115 | 116 | Object.defineProperty(Circle.prototype, "left", { 117 | get: function () { 118 | return this.x - this.radius; 119 | }, 120 | 121 | set: function (value) { 122 | if (value > this.x) { 123 | this.radius = 0; 124 | } 125 | else { 126 | this.radius = this.x - value; 127 | } 128 | } 129 | }); 130 | 131 | Object.defineProperty(Circle.prototype, "right", { 132 | 133 | get: function () { 134 | return this.x + this.radius; 135 | }, 136 | 137 | set: function (value) { 138 | if (value < this.x) { 139 | this.radius = 0; 140 | } 141 | else { 142 | this.radius = value - this.x; 143 | } 144 | } 145 | }); 146 | 147 | Object.defineProperty(Circle.prototype, "top", { 148 | get: function () { 149 | return this.y - this.radius; 150 | }, 151 | 152 | set: function (value) { 153 | if (value > this.y) { 154 | this.radius = 0; 155 | } 156 | else { 157 | this.radius = this.y - value; 158 | } 159 | } 160 | }); 161 | 162 | Object.defineProperty(Circle.prototype, "bottom", { 163 | get: function () { 164 | return this.y + this.radius; 165 | }, 166 | set: function (value) { 167 | if (value < this.y) { 168 | this.radius = 0; 169 | } 170 | else { 171 | this.radius = value - this.y; 172 | } 173 | } 174 | }); 175 | 176 | module.exports = Circle; -------------------------------------------------------------------------------- /utils/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author qiao 4 | * @version 2017/12/30 5 | */ 6 | var utils = {}; 7 | 8 | utils.getRandom = function (max, min) { 9 | min = arguments[1] || 0; 10 | return Math.floor(Math.random() * (max - min + 1) + min); 11 | }; 12 | 13 | utils.containsPoint = function (rect, x, y) { 14 | return !(x < rect.x || 15 | x > rect.x + rect.width || 16 | y < rect.y || 17 | y > rect.y + rect.height); 18 | }; 19 | 20 | utils.captureTouch = function (element) { 21 | var isTouch = ('ontouchend' in document); 22 | var touchstart = null; 23 | var touchmove = null; 24 | var touchend = null; 25 | if (isTouch) { 26 | touchstart = 'touchstart'; 27 | touchmove = 'touchmove'; 28 | touchend = 'touchend'; 29 | } else { 30 | touchstart = 'mousedown'; 31 | touchmove = 'mousemove'; 32 | touchend = 'mouseup'; 33 | } 34 | var touch = { x: null, y: null, isPressed: false, event: null }, 35 | body_scrollLeft = document.body.scrollLeft, 36 | element_scrollLeft = document.documentElement.scrollLeft, 37 | body_scrollTop = document.body.scrollTop, 38 | element_scrollTop = document.documentElement.scrollTop, 39 | offsetLeft = element.offsetLeft, 40 | offsetTop = element.offsetTop; 41 | 42 | /*传入Event对象*/ 43 | function getPoint(event) { 44 | /*将当前的触摸点坐标值减去元素的偏移位置,返回触摸点相对于element的坐标值*/ 45 | event = event || window.event; 46 | var touchEvent = isTouch ? event.changedTouches[0] : event; 47 | var x = (touchEvent.pageX || touchEvent.clientX + document.body.scrollLeft + document.documentElement.scrollLeft); 48 | x -= element.offsetLeft; 49 | var y = (touchEvent.pageY || touchEvent.clientY + document.body.scrollTop + document.documentElement.scrollTop); 50 | y -= element.offsetTop; 51 | return { 52 | x: x, 53 | y: y 54 | }; 55 | }; 56 | 57 | element.addEventListener(touchstart, function (event) { 58 | var point = getPoint(event); 59 | touch.x = point.x; 60 | touch.y = point.y; 61 | touch.isPressed = true; 62 | touch.event = event; 63 | }, false); 64 | 65 | element.addEventListener(touchend, function (event) { 66 | touch.isPressed = false; 67 | touch.x = null; 68 | touch.y = null; 69 | touch.event = event; 70 | }, false); 71 | 72 | element.addEventListener(touchmove, function (event) { 73 | var point = getPoint(event); 74 | touch.x = point.x; 75 | touch.y = point.y; 76 | touch.event = event; 77 | }, false); 78 | 79 | return touch; 80 | }; 81 | 82 | module.exports = utils; --------------------------------------------------------------------------------