文章目录
  1. 1. 需求
  2. 2. 书写伪代码
  3. 3. 实现参数 options 配置方法
  4. 4. 实现一个设置元素样式的方法
  5. 5. 实现动画主循环
  6. 6. 源码下载

自己撸个转盘,闲着也是闲着

demo 效果

需求

很多场景都需要做各种活动,抽奖最是司空见惯了,跑马灯的,转盘的,下面先花几分钟撸出一个转盘的吧,当然网上至少有一打的 demo 可供参考。
真的只需要一点点时间而已。

书写伪代码

实现一个东西,一般都先写伪代码,这里也不例外。
初步想法功能主要有两点:

  • 实现一个类 class,只要传入一个元素节点,就可以控制转动,
  • 转动角度和时长以及动画曲线 可 通过参数进行配置

考虑一下,这个类需要什么方法功能?
根据上面的两个需求,

  1. 第一 需要一个 设置参数的 方法
  2. 第二需要提供一个 开启动画的方法
  3. 第三,既然是动画,脱不开定时器功能,所以需要一个动画主循环

这里再提供一个 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; // 维护一个当前时刻角度的私有变量,下面setOptions就知道如何使用了
}
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
// 实现一个css3样式,我们需要处理兼容性,确定浏览器类型,选择对应的属性
// 这里添加一个辅助方法
/**
* 判断运行环境支持的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;
}
// 在constructor构造函数里面增加一个属性
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里面增加 _rotate方法,初始化元素 初始角度
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) {
// 调用缓动函数,获取当前时刻 angle值
var angle = this._parameters.easing(
actualTime - this._animateStartTime,
this._animateStartAngle,
this._parameters.animateTo - this._animateStartAngle,
this._parameters.duration
);
// 设置 el 元素的样式
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
/**
* 功能: 开发一个旋转插件,传入一个元素节点即可控制旋转
* 转动角度和时长以及动画曲线 可 通过参数进行配置
*
* 参数列表:
* - dom 需要一个容器 必选
* - angle 初始角度 非必选
* - animateTo 结束角度 非必选
* - duration 动画时长 非必选
* - easing 缓动函数 非必选
* - step 角度每次更新调用 非必选
* - callback 动画结束回调 非必选
*/
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);
}
}
}

/**
* 判断运行环境支持的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;
}

下面再补充一个 canvas 实现的动画方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_rotateCanvas(angle) {
// devicePixelRatio 是设备像素比,为了解决canvas模糊问题设置的
// 原理把 canvas画布扩大,然后缩小显示在屏幕
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);
// 平移canvas原点
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 下载,查看

文章目录
  1. 1. 需求
  2. 2. 书写伪代码
  3. 3. 实现参数 options 配置方法
  4. 4. 实现一个设置元素样式的方法
  5. 5. 实现动画主循环
  6. 6. 源码下载
顶部