By
donglegend
更新日期:
自己撸个转盘,闲着也是闲着
demo 效果
需求
很多场景都需要做各种活动,抽奖最是司空见惯了,跑马灯的,转盘的,下面先花几分钟撸出一个转盘的吧,当然网上至少有一打的 demo 可供参考。
真的只需要一点点时间而已。
书写伪代码
实现一个东西,一般都先写伪代码,这里也不例外。
初步想法功能主要有两点:
- 实现一个类 class,只要传入一个元素节点,就可以控制转动,
- 转动角度和时长以及动画曲线 可 通过参数进行配置
考虑一下,这个类需要什么方法功能?
根据上面的两个需求,
- 第一 需要一个 设置参数的 方法
- 第二需要提供一个 开启动画的方法
- 第三,既然是动画,脱不开定时器功能,所以需要一个动画主循环
这里再提供一个 init 方法用作初始化操作,比如设置参数或者还有其他处理,增加兼容性。
下面是伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class RotatePlate { constructor(options) { this.init(); }
init() { this.setOptions(); }
rotate() {}
setOptions() {}
_animate() {} }
|
实现参数 options 配置方法
为了方便使用和兼容处理,我们开发者常用的做法就是配置默认参数,然后用 调用者的参数去覆盖默认参数。所以,先给类增加一些默认配置,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| constructor(options) { this.customOps = options; this._parameters = { angle: 0, animateTo: 0, step: null, easing: function(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, duration: 3000, callback: () => {}, }; this._angle = 0; } this.init();
|
接下来实现 setOptions 方法,并且在 init 方法中进行调用,这个方法实现没什么难度,就是对象合并操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| init() { this.setOptions(Object.assign({}, this.customOps)); }
setOptions(parameters) { try { if (typeof parameters.el === 'string') { this.el = document.querySelector(parameters.el); } else { this.el = parameters.el; } if (typeof parameters.angle === 'number') this._angle = parameters.angle; Object.assign(this._parameters, parameters); } catch (err) {} }
|
实现一个设置元素样式的方法
上面设置完了参数,我们还没办法验证参数是否正确。
为了实现旋转效果,我们有两种方式可供选择,第一种,利用 css3 的 transform,第二种利用 canvas 绘图。其实两种方法都比较简单,这里先选择 css3 实现一版,结尾再附上 canvas 版本的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
function getSupportCSS() { let supportedCSS = null; const styles = document.getElementsByTagName('head')[0].style; const toCheck = 'transformProperty WebkitTransform OTransform msTransform MozTransform'.split( ' ' ); for (var a = 0; a < toCheck.length; a++) { if (styles[toCheck[a]] !== undefined) { supportedCSS = toCheck[a]; break; } } return supportedCSS; }
this.supportedCSS = getSupportCSS();
|
然后 给类增加一个 设置样式的方法_rotate
1 2 3 4 5 6 7 8 9 10 11 12
| _rotate(angle) { const el = this.el; this._angle = angle; el.style[this.supportedCSS] = `rotate3d(0,0,1,${angle % 360}deg)`; }
init() { this.setOptions(Object.assign({}, this.customOps)); this._rotate(this._angle); }
|
在这里,就可以写一个 demo,进行测试了,当然还么有动画,只能测试初始角度 angle 设置
demo 代码,顺便看看我们的脚本代码变成了什么样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .ball { width: 100px; height: 100px; background: red; margin: 40px auto; } </style> </head> <body> <div class="ball" id="ball"></div> <script> class RotatePlate { constructor(options) { this.supportedCSS = getSupportCSS(); this.customOps = options; this._parameters = { angle: 0, animateTo: 0, step: null, easing: function(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, duration: 3000, callback: () => {}, }; this._angle = 0; this.init(); } /** * 初始化操作 */ init() { this.setOptions(Object.assign({}, this.customOps)); this._rotate(this._angle); } /** * 启动转动函数 */ rotate() {} /** * 设置配置参数 */ setOptions(parameters) { try { if (typeof parameters.el === 'string') { this.el = document.querySelector(parameters.el); } else { this.el = parameters.el; } if (typeof parameters.angle === 'number') this._angle = parameters.angle; Object.assign(this._parameters, parameters); } catch (err) {} } _rotate(angle) { const el = this.el; this._angle = angle; el.style[this.supportedCSS] = `rotate3d(0,0,1,${angle % 360}deg)`; } /** * 动画主循环 */ _animate() {} }
/** * 判断运行环境支持的css,用作css制作动画 */ function getSupportCSS() { let supportedCSS = null; const styles = document.getElementsByTagName('head')[0].style; const toCheck = 'transformProperty WebkitTransform OTransform msTransform MozTransform'.split( ' ' ); for (var a = 0; a < toCheck.length; a++) { if (styles[toCheck[a]] !== undefined) { supportedCSS = toCheck[a]; break; } } return supportedCSS; }
</script> <script> const r = new RotatePlate({ el: document.querySelector('#ball'), angle: 60, animateTo: 1800, }) </script> </body>
</html>
|
实现动画主循环
写到这里,虽然说了一大推话,但是代码去掉注释,真的还没有几行。
但是我们只差一个定时器循环了,接下里实现这个主循环,不断更新 angle 值就可以了。
说起定时器,我们需要计算动画时间来 判断是否应该取消定时器等等,一些附加操作,所以增加一个_animateStart 方法清理和计时, 下面直接上代码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| _animateStart() { if (this._timer) { clearTimeout(this._timer); } this._animateStartTime = Date.now(); this._animateStartAngle = this._angle; this._animate(); }
_animate() { const actualTime = Date.now(); const checkEnd = actualTime - this._animateStartTime > this._parameters.duration; if (checkEnd) { clearTimeout(this._timer); } else { if (this.el) { var angle = this._parameters.easing( actualTime - this._animateStartTime, this._animateStartAngle, this._parameters.animateTo - this._animateStartAngle, this._parameters.duration ); this._rotate(~~(angle * 10) / 10); } if (this._parameters.step) { this._parameters.step.call(this, this._angle); } this._timer = setTimeout(() => { this._animate(); }, 10); } if (this._parameters.callback && checkEnd) { this._angle = this._parameters.animateTo; this._rotate(this._angle); this._parameters.callback.call(this); } }
|
然后再 rotate 方法调用_animateStart 就好了
1 2 3 4 5 6 7
| rotate() { if (this._angle === this._parameters.animateTo) { this._rotate(this._angle); } else { this._animateStart(); } }
|
至此,一个利用 css3 实现的脚本就完成了,有木有很简单,下面贴上完整代码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
class RotatePlate { constructor(options) { this.supportedCSS = getSupportCSS(); this.customOps = options; this._parameters = { angle: 0, animateTo: 0, step: null, easing: function(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, duration: 3000, callback: () => {}, }; this._angle = 0; this.init(); }
init(newOps = {}) { this.setOptions(Object.assign({}, this.customOps, newOps)); this._rotate(this._angle); }
rotate() { if (this._angle === this._parameters.animateTo) { this._rotate(this._angle); } else { this._animateStart(); } }
setOptions(parameters) { try { if (typeof parameters.el === 'string') { this.el = document.querySelector(parameters.el); } else { this.el = parameters.el; } if (typeof parameters.angle === 'number') this._angle = parameters.angle; Object.assign(this._parameters, parameters); } catch (err) {} } _rotate(angle) { const el = this.el; this._angle = angle; el.style[this.supportedCSS] = `rotate3d(0,0,1,${angle % 360}deg)`; } _animateStart() { if (this._timer) { clearTimeout(this._timer); } this._animateStartTime = Date.now(); this._animateStartAngle = this._angle; this._animate(); }
_animate() { const actualTime = Date.now(); const checkEnd = actualTime - this._animateStartTime > this._parameters.duration; if (checkEnd) { clearTimeout(this._timer); } else { if (this.el) { var angle = this._parameters.easing( actualTime - this._animateStartTime, this._animateStartAngle, this._parameters.animateTo - this._animateStartAngle, this._parameters.duration ); this._rotate(~~(angle * 10) / 10); } if (this._parameters.step) { this._parameters.step.call(this, this._angle); } this._timer = setTimeout(() => { this._animate(); }, 10); } if (this._parameters.callback && checkEnd) { this._angle = this._parameters.animateTo; this._rotate(this._angle); this._parameters.callback.call(this); } } }
function getSupportCSS() { let supportedCSS = null; const styles = document.getElementsByTagName('head')[0].style; const toCheck = 'transformProperty WebkitTransform OTransform msTransform MozTransform'.split( ' ' ); for (var a = 0; a < toCheck.length; a++) { if (styles[toCheck[a]] !== undefined) { supportedCSS = toCheck[a]; break; } } return supportedCSS; }
|
下面再补充一个 canvas 实现的动画方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| _rotateCanvas(angle) { this._angle = angle; const radian = ((angle % 360) * Math.PI) / 180; this._canvas.width = this.WIDTH * this.devicePixelRatio; this._canvas.height = this.HEIGHT * this.devicePixelRatio; this._cnv.scale(this.devicePixelRatio, this.devicePixelRatio); this._cnv.translate(this.WIDTH / 2, this.HEIGHT / 2); this._cnv.rotate(radian); this._cnv.translate(-this.WIDTH / 2, -this.HEIGHT / 2); this._cnv.drawImage(this._img, 0, 0, this.WIDTH, this.HEIGHT); }
|
源码下载
完整源码请到 github 下载,查看