需求
很多场景都需要做各种活动,抽奖最是司空见惯了,跑马灯的,转盘的,下面先花几分钟撸出一个转盘的吧,当然网上至少有一打的 demo 可供参考。
真的只需要一点点时间而已。
书写伪代码
实现一个东西,一般都先写伪代码,这里也不例外。
初步想法功能主要有两点:
- 实现一个类 class,只要传入一个元素节点,就可以控制转动,
- 转动角度和时长以及动画曲线 可 通过参数进行配置
考虑一下,这个类需要什么方法功能?
根据上面的两个需求,
- 第一 需要一个 设置参数的 方法
- 第二需要提供一个 开启动画的方法
- 第三,既然是动画,脱不开定时器功能,所以需要一个动画主循环
这里再提供一个 init 方法用作初始化操作,比如设置参数或者还有其他处理,增加兼容性。
下面是伪代码
class RotatePlate {
constructor(options) {
this.init();
}
/**
* 初始化操作
*/
init() {
this.setOptions();
}
/**
* 启动转动函数
*/
rotate() {}
/**
* 设置配置参数
*/
setOptions() {}
/**
* 动画主循环
*/
_animate() {}
}
实现参数 options 配置方法
为了方便使用和兼容处理,我们开发者常用的做法就是配置默认参数,然后用 调用者的参数去覆盖默认参数。所以,先给类增加一些默认配置,如下:
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 方法中进行调用,这个方法实现没什么难度,就是对象合并操作
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 版本的。
// 实现一个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
_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 代码,顺便看看我们的脚本代码变成了什么样子:
Document
实现动画主循环
写到这里,虽然说了一大推话,但是代码去掉注释,真的还没有几行。
但是我们只差一个定时器循环了,接下里实现这个主循环,不断更新 angle 值就可以了。
说起定时器,我们需要计算动画时间来 判断是否应该取消定时器等等,一些附加操作,所以增加一个_animateStart 方法清理和计时, 下面直接上代码,
_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 就好了
rotate() {
if (this._angle === this._parameters.animateTo) {
this._rotate(this._angle);
} else {
this._animateStart();
}
}
至此,一个利用 css3 实现的脚本就完成了,有木有很简单,下面贴上完整代码.
/**
* 功能: 开发一个旋转插件,传入一个元素节点即可控制旋转
* 转动角度和时长以及动画曲线 可 通过参数进行配置
*
* 参数列表:
* - 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 实现的动画方法:
_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);
}