序列帧动画 – 弱音 SilkyTone

简介: 前文 什么是序列帧动画,序列帧动画(Sprite Animation)是通过快速切换连续图像产生运动效果的技术,广泛应用于游戏角色动画、加载动效、交

前文

什么是序列帧动画,序列帧动画(Sprite Animation)是通过快速切换连续图像产生运动效果的技术,广泛应用于游戏角色动画、加载动效、交互反馈等场景。与传统CSS动画相比,序列帧能实现更复杂的视觉效果且资源控制更灵活。

作者已将其实现并发布到 npm 上,快来使用吧!@silkytone/sequence-frame

原理与核心技术

先说原理,序列帧和动画应该分开描述,这样你是不是好理解多了,一个是序列帧,一个是动画,序列帧是按顺序排列的静态图像集合,动画是在一秒切换的多少张图片的实现。

那么核心的技术是什么呢?canvas 和 requestAnimationFrame,canvas 我们应该都知道,是一个 html 中的元素标签,可以显示图片,那么 requestAnimationFrame 呢,是浏览器的一个帧定时器,与 setTimeout,setInterval 定时器不同,requestAnimationFrame 它要求浏览器在下一次重绘之前,调用用户提供的回调函数。下面实现了简单的使用,打开控制台可以观看打印的内容。

function animate(timestamp){ console.log('animate', timestamp); requestAnimationFrame(animate);}

封装与实现

在上文说过了,使用 canvas 和 requestAnimationFrame 就可以实现一个序列帧动画,那么我们现在一步步的实现,先简单的封装,让我们使用;

我们创建一个 Animate 类,为其实现 run,start,stop,三个方法和 fps,handler 两个传参,handler 是我们需要处理的方法,fps 为限制帧率。start,stop 用来控制动画,run 方法用来处理 handler 以及实现动画。

class Animate {

status = false;

constructor(handler,fps) {

this.fps = fps || 30;

this.handler = handler;

}

run(timestamp) {}

start() {

if (this.status) return;

this.status = true;

this.run();

}

stop() {

if (!this.status) return;

this.status = false;

}

}

有小伙伴问,为什么要创建一个 status 参数,这里我解释一下在 run 方法执行后,我们需要一个状态来判断 run 方法是否进行执行。

status 创建好了以后我们来实现 run 方法。在 run 方法中我们判断 status 的值为 false 我们退出执行。

创建了 get frameTime 参数用于获取多少毫秒执行一帧 ,

run 方法很简单,获取上一帧的时间,获取当前时间,当前时间减去上一帧的时候就得到了,上一帧到当前的用时;同时使用 setTimeout 定时器,将其限制在我们规定的 fps 范围内。然后就是使用我们的 requestAnimationFrame 方法,进入下一次浏览器重绘之前,运行 run 方法

class Animate {

...// 保持原来的代码

get frameTime() {

return 1000 / this.fps;

}

run(timestamp) {

if(!this.status) return;

//

const last = this.lastTime || Date.now();

const now = Date.now();

this.lastTime = now;

//

setTimeout(() => {

this.handler(timestamp);

}, this.frameTime - ((now - last) % this.frameTime));

//

requestAnimationFrame(this.run.bind(this));

}

}

下面是完整的 Animate 类的封装

class Animate {

status = false;

constructor(handler, fps) {

this.fps = fps || 30;

this.handler = handler;

}

get frameTime() {

return 1000 / this.fps;

}

run(timestamp) {

if (!this.status) return;

//

const last = this.lastTime || Date.now();

const now = Date.now();

this.lastTime = now;

//

setTimeout(() => {

this.handler(timestamp);

}, this.frameTime - ((now - last) % this.frameTime));

//

requestAnimationFrame(this.run.bind(this));

}

start() {

if (this.status) return;

this.status = true;

this.run();

}

stop() {

if (!this.status) return;

this.lastTime = undefined;

this.status = false;

}

}

接下来我们将 canvas 和 Animate 类封装在一起让我们的使用更加简单;

然我们来实现 CanvasAnimate 类型并实现 createCanvas ,handler,add,play,stop 几个方法,以及两个传参,el,fps。

play,stop 用于控制 Animate 类实现 播放/停止,add 用于添加序列帧图片,createCanvas 用户绑定或创建一个 canvas,handler 的实现,canvas 的绘画。

class CanvasAnimate {

get length() {

return this.images.length;

}

constructor(el, fps) {

this.count = 0;

this.images = [];

this.createCanvas(el);

this.animate = new Animate(this.handler.bind(this), fps);

}

createCanvas(el) {}

add(image) {

this.images.push(image);

}

handler() {}

play() {

this.animate.start();

}

stop() {

this.animate.stop();

}

}

接下来我们实现 createCanvas 和 handler 方法。createCanvas 查询或创建一个 canvas 绑定,并调用方法 getContext(‘2d’) 创建画布。handler 用户处理擦除画布的内容和将帧绘画到画布上。

class CanvasAnimate {

...// 保持原来的代码

createCanvas(el) {

if (typeof el === "string") {

this.canvas = document.querySelector(el);

if (!this.canvas) {

throw new Error(`el: ${el} 不存在`);

}

} else if (el.nodeName.toLowerCase() === "canvas") {

this.canvas = el;

} else {

this.canvas = document.createElement("canvas");

el.appendChild(this.canvas);

this.canvas.getContext("2d");

}

this.ctx = this.canvas.getContext("2d");

}

handler() {

const { width, height } = this.canvas;

this.ctx.clearRect(0, 0, width, height);

//

this.count = (this.count + 1) % this.length;

const image = this.images[this.count];

//

this.ctx.drawImage(image, 0, 0, width, height);

}

}

完整的代码如下

class CanvasAnimate {

constructor(el, fps) {

this.count = 0;

this.images = [];

this.createCanvas(el);

this.animate = new Animate(this.handler.bind(this), fps);

}

createCanvas(el) {

if (typeof el === "string") {

this.canvas = document.querySelector(el);

if (!this.canvas) {

throw new Error(`el: ${el} 不存在`);

}

} else if (el.nodeName.toLowerCase() === "canvas") {

this.canvas = el;

} else {

this.canvas = document.createElement("canvas");

el.appendChild(this.canvas);

this.canvas.getContext("2d");

}

this.ctx = this.canvas.getContext("2d");

}

add(image) {

this.images.push(image);

}

get length() {

return this.images.length;

}

handler() {

const { width, height } = this.canvas;

this.ctx.clearRect(0, 0, width, height);

//

this.count = (this.count + 1) % this.length;

const image = this.images[this.count];

//

this.ctx.drawImage(image, 0, 0, width, height);

}

play() {

this.animate.start();

}

stop() {

this.animate.stop();

}

}

使用

用我们创建的和封装的代码来使用吧,将我们封装的代码 canvasAnimate 类实例化,创建一个长度为 100 数组遍历数组,加载序列帧图片,并返回 Promise 用于等待图片的加载。等待全部加载完成后,开始播放序列帧动画,这样在你的页面中就能看到动画了;

jshtmlCanvasAnimate.jsAnimate.js

const canvasAnimate = new CanvasAnimate("#canvas", 30);

const images = Array.from(new Array(100)).map((_, index) => {

const img = document.createElement("img");

canvasAnimate.add(img);

return new Promise((resolve) => {

img.onload = function () {

resolve(true);

};

img.src = `url/${index}.png`;

});

});

Promise.all(images).then(() => {

canvasAnimate.play();

});

序列帧动画

class CanvasAnimate {

constructor(el, fps) {

this.count = 0;

this.images = [];

this.createCanvas(el);

this.animate = new Animate(this.handler.bind(this), fps);

}

createCanvas(el) {

if (typeof el === "string") {

this.canvas = document.querySelector(el);

if (!this.canvas) {

throw new Error(`el: ${el} 不存在`);

}

} else if (el.nodeName.toLowerCase() === "canvas") {

this.canvas = el;

} else {

this.canvas = document.createElement("canvas");

el.appendChild(this.canvas);

this.canvas.getContext("2d");

}

this.ctx = this.canvas.getContext("2d");

}

add(image) {

this.images.push(image);

}

get length() {

return this.images.length;

}

handler() {

const { width, height } = this.canvas;

this.ctx.clearRect(0, 0, width, height);

//

this.count = (this.count + 1) % this.length;

const image = this.images[this.count];

//

this.ctx.drawImage(image, 0, 0, width, height);

}

play() {

this.animate.start();

}

stop() {

this.animate.stop();

}

}

class Animate {

status = false;

constructor(handler, fps) {

this.fps = fps || 30;

this.handler = handler;

}

get frameTime() {

return 1000 / this.fps;

}

run(timestamp) {

if (!this.status) return;

//

const last = this.lastTime || Date.now();

const now = Date.now();

this.lastTime = now;

//

setTimeout(() => {

this.handler(timestamp);

}, this.frameTime - ((now - last) % this.frameTime));

//

requestAnimationFrame(this.run.bind(this));

}

start() {

if (this.status) return;

this.status = true;

this.run();

}

stop() {

if (!this.status) return;

this.lastTime = undefined;

this.status = false;

}

}