基本动画
前几章节,我们已经通过图元来绘制图形,但是图元一旦绘制出来就保持那样了。当然我们可以通过可以用window.setInterval(), window.setTimeout(),和window.requestAnimationFrame()来设定定期执行一个指定函数修改图元的属性来实现动画效果。下面我们通过一个例子实现通过图元实现动画。
let rectangle = new Rectangle(this.canvas, {
position: new Point(this.canvas.width / 2, 400),
width: 500,
height: 100,
type: Rectangle.TYPE.FILL,
linearGradient: [
[0, '#DEDEDE'],
[0.3, '#999999'],
[0.5, '#777777'],
[0.7, '#444444'],
[1, '#000000'],
],
});
setInterval(() => {
if (rectangle.rotation >= 360) {
rectangle.rotation = 0;
}
// 每次旋转10度
rectangle.rotation += 10;
this.canvas.paint();
}, 100 / 6);
this.canvas.addChild(rectangle);
this.canvas.paint();
上面我们通过新建了一个矩形,并通过定时器setInterval每16.667ms使矩形旋转10度。由上面的例子可以看出,通过修改图元属性可以很方便的实现动画效果。
虽然通过setInterval可以很方便的实现动画效果,然而如果需要动画的图元很多,那么如何管理这些动画,以及动画效果如何复用也是一个需要考虑的问题。下面,我们通过构建动作类来对动画进行管理。
构建动作类Action
首先我们来确定一下一个动画所包含的属性:
-动画时长 duration
-动画状态 status: 1、运行前,2、运行中,3、运行结束,4、运行终止, 5、暂停
-动画帧率 fps
根据上述属性,构建动画类
export default class Action {
static STATUS = {
BEFORE_RUN: 1, // 动画运行前
RUNNING: 2, // 动画运行中
PAUSE: 3, // 动画暂停
STOP: 4, // 动画终止
}
/**
* @param _duration 运行时长 以秒为单位
*/
constructor(_duration, fps = 60) {
this.status = Action.STATUS.BEFORE_RUN; // 动作状态
this.duration = _duration;
this.allFrames = Math.round(_duration * 1000 * fps / 60); // 根据运行时间与帧率,计算该动作所有帧率
this.currentFrame = 0; // 当前帧
this.fpsRatio = 60 / fps; // 帧率控制 浏览器帧率为60,如果fps为30,那么以为着浏览器每两次刷新运行一次动作
this.fpsCount = 0; // 帧率计数器
this.runCallback = undefined; // 动作运行回调函数
}
/**
* 运行函数
* @param node
* @param callback 每一帧回调函数,暴露给外围使用
*/
run(node, callback) {
if (this.fpsCount < this.fpsRatio) {
this.fpsCount++;
requestAnimationFrame((timestamp) => {
//console.log(timestamp);
this.run(node, callback);
});
return;
}
if(!this.runCallback) {
this.runCallback = callback;
}
// 调用动画更新函数
this.update(node, this.currentFrame, this.allFrames);
callback && callback(node, this);
if (this.status === Action.STATUS.RUNNING) {
// 当动作为运行状态
if (this.duration < 0 || this.currentFrame < this.allFrames) {
// 如果总帧数小于0 或者 总帧数大于0 且当前帧小于总帧数, 动作进入下一帧
this.currentFrame += 1;
this.fpsCount = 0; // 帧率计数器置0
requestAnimationFrame(() => {
this.run(node, callback);
});
} else {
// 运行结束
// 设置动画为停止
this.status = Action.STATUS.STOP;
// 调用动画生命周期停止回调函数
this.onStop && this.onStop(node);
}
}
}
/**
* 抽象方法,由子类继承
* @param node
* @param frame
*/
update(node, frame, frames) {
}
/**
* 动作控制函数,停止操作
*/
stop() {
this.status = Action.STATUS.STOP;
if (this.allFrames > 0) {
// 确保当前帧为最后一帧
this.currentFrame = this.allFrames;
}
// 调用动画生命周期停止回调函数
this.onStop && this.onStop(node);
}
/**
* 动作控制函数, 暂停操作
*/
pause() {
this.status = Action.STATUS.PAUSE;
// 调用动画生命周期暂停回调函数
this.onPause && this.onPause();
}
/**
* 动作控制函数,暂停重新启动
* @param node
*/
restart(node) {
if (this.status === Action.STATUS.PAUSE) {
this.fpsCount = 0;
this.status = Action.STATUS.RUNNING;
this.run(node, this.runCallback);
// 生命周期函数 暂停启动回调
this.onRestart && this.onRestart();
}
}
/**
* 动画重新启动
* @param node
*/
reset(node) {
this.currentFrame = 0;
this.fpsCount = 0;
this.status = Action.STATUS.RUNNING;
this.run(node, this.runCallback);
// 生命周期函数 重启回调
this.onReset && this.onReset()
}
}
构造器传入2个参数,分别为动作运行时间与帧率。在运行函数中,我们可以看到,我们通过调用window.requestAnimationFrame来实现递归运行。window.requestAnimationFrame函数提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒钟回调函数执行60次,也有可能会被降低。这里我们假定每秒钟执行60次,具体优化可以参考可以在Game development zone 参考这篇文章 Anatomy of a video game。Action构造器函数中的参数fps向外提供了分频器的作用,通过修改fps可以实现分频的功能。假定构造器参数fps为30,那么对于正常浏览器帧率60来说,该动作在浏览器每两次刷频中执行一次run函数,控制代码如下:
if (this.fpsCount < this.fpsRatio) {
this.fpsCount++;
requestAnimationFrame(() => {
this.run(node, callback);
});
return;
}
Action类最主要的函数为run函数,该函数调用自身的update方法进行动作执行图元的更新操作,而update方法为抽象函数供子类继承实现。具体的动作子类通过实现update方法更新图元属性,从而实现动画效果,而动作的时序时间的控制则交给Action类控制,保持对子类的透明。
update函数为抽象函数,由子类实现,该函数提供三个参数,分别为执行动作的图元node,当前帧frame以及所有帧frames。
在实现动作类后,我们需要给图元函数增加执行动作的方法,在Node类中,加入如下代码:
export default class Node {
//...其他代码
runAction(action, callback) {
if (action instanceof Action) {
action.reset(this);
action.run(this, callback);
} else {
throw new Error('Error Arguments: action is not a instance of class Action');
}
}
stopAction(action) {
if (action instanceof Action) {
action.stop();
} else {
throw new Error('Error Arguments: action is not a instance of class Action');
}
}
}
扩展动作类及实践
下面我们通过简单的时钟示例来看看如何使用Action类。首先我们创建一个旋转动作RotateAction继承Action。
export default class RotateAction extends Action {
constructor(duration, fps, angle) {
super(duration, fps);
this.angle = angle;
}
update(_sprite, frame) {
_sprite.rotation += this.angle
_sprite.canvas.paint();
}
}
RotateAction代码很简单,构造函数中传入每帧旋转的角度angle,实现抽象函数update,在update每一帧中对图元旋转属性加上旋转角度。
接下来,我们创建两个线框为10单位的直线,并执行相应的动作。
// 秒针线
let secondHand = new Line(this.canvas, {
lineWidth: 10,
position: new Point(this.canvas.width / 2, this.canvas.height / 2),
to: new Point(this.canvas.width / 2, this.canvas.height / 2 + 100),
});
// 分针线
let miniHand = new Line(this.canvas, {
lineWidth: 10,
position: new Point(this.canvas.width / 2, this.canvas.height / 2),
to: new Point(this.canvas.width / 2, this.canvas.height / 2 + 60),
});
// 创建秒针线动作,运行实现-1不停止,帧率为1,每秒旋转角度6度
let secondHandAction = new RotateAction(-1, 1, 6);
let miniHandAction = new RotateAction(-1, 1 / 60, 6);
this.canvas.addChild(secondHand, miniHand);
// 运行旋转动作
secondHand.runAction(secondHandAction);
miniHand.runAction(miniHandAction);
this.canvas.paint();
目录
【实现自己的可视化引擎01】认识Canvas
【实现自己的可视化框架引擎02】抽象图像元素
【实现自己的可视化引擎03】构建基础图元库
【实现自己的可视化引擎04】图像元素动画
【实现自己的可视化引擎05】交互与事件
【实现自己的可视化引擎06】折线图
【实现自己的可视化引擎07】柱状图
【实现自己的可视化引擎08】条形图
【实现自己的可视化引擎09】饼图
【实现自己的可视化引擎10】散点图
【实现自己的可视化引擎11】雷达图
【实现自己的可视化引擎12】K线图
【实现自己的可视化引擎13】仪表盘
【实现自己的可视化引擎14】地图
【实现自己的可视化引擎15】关系图