彻底没有canvas基础的同窗建议先刷一下 [Canvas的基本用法 - Web API 接口参考 | MDN]
重点是理解canvas动画的基本步骤,在[基本的动画 - MDN]中,动画分为4步走
javascript
初学者能够再简单一些,咱们先无论状态保存,直接两步走: 前端
用定时器或者window.requestAnimationFrame
定时重复以上两步便可
vue
想象一下整个业务场景,咱们先梳理出3个要解决的核心问题: java
下面都是基于vue的代码,不能直接跑的,主要用于理解核心功能git
最好是本身理解核心原理后亲自动手作个最简单的demo,有助于加深理解
github
页面上感受有不少不少金币在按各类角度掉落 chrome
其实页面上一共就4种金币图片,只是他们的大小、速度不同,看起来有每个都不同 canvas
咱们能够先把这4张图片全都加载好
api
// 缓存几种金币图片为DOM元素,避免canvas绘制时还须要异步读取图片
loadImgs(arr) {
return new Promise(resolve => {
let count = 0;
// 循环图片数组,每张图片都生成一个新的图片对象
const len = arr.length;
for (let i = 0; i < len; i++) {
// 建立图片对象
const image = new Image();
// 成功的异步回调
image.onload = () => {
count++;
arr.splice(i, 1, {
// 加载完的图片对象都缓存在这里了,canvas能够直接绘制
img: image,
// 这里能够直接生成并缓存离屏canvas,用于优化性能,但本次不用,只是举个例子
offScreenCanvas: this.createOffScreenCanvas(image)
});
// 这里说明 整个图片数组arr里面的图片全都加载好了
if (count == len) {
this.preloaded = true;
resolve();
}
};
image.src = arr[i].img;
}
});
},
复制代码
建立离屏canvas的方法以下
数组
createOffScreenCanvas(image) {
const offscreenCanvas = document.createElement("canvas");
const offscreenContext = offscreenCanvas.getContext("2d");
// 这里能够是动态宽高
offscreenContext.width = 30;
offscreenContext.height = 30;
offscreenContext.drawImage(
image,
0,
0,
offscreenContext.width,
offscreenContext.height
);
// return这个offscreenCanvas
return offscreenCanvas;
},复制代码
首先初始化canvas
这里咱们直接把canvas的上下文ctx
存在data
里面,方便在各个方法里面读取。
在vue里面写不像单独的一个JS模块,能够用闭包来封装一个独立的上下文,而在vue里面也不建议声明全局变量
initCanvas() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
this.ctx = canvas.getContext("2d");
// 初始化时同步进行图片预加载
this.loadImgs(this.imgArr);
}
},复制代码
绘制多图,其实就是循环遍历上面建立好的图片数组imgArr
,而后对于每一个图片对象,都调用this.ctx.drawImage()
方法便可
下面咱们把图片转变化金币对象
把图片数组imgArr
替换成金币对象数组coinArr
,这个数组是由一个个的金币对象Coin
组成,金币对象自身除了有图片,还有大小、物理位置、下落速度等参数,也就是说,每一个金币对象缓存本身的全部绘制信息,这里用的是面向对象的思惟
const Coin = {
x: 'x轴位置',
y: 'y轴位置', // 运动的关键是在每一帧都改变y
radius: '金币大小',
img: '前面缓存好的金币图片',
speed: '金币的下落速度'
};复制代码
每一帧,循环这个金币数组,而后绘制出全部的金币对象
若是要运动起来,每一帧让每一个金币的y轴位置往下掉一点,就是这句y: coin.y + coin.speed
那么绘制下一帧时,其余信息都不变,每一个金币都往下移动了一点点,连贯起来,这不一样的一帧一帧组合起来就成了运动的动画了
先看绘制的代码
drawCoins() {
// 遍历这个金币对象数组
this.coinArr.forEach((coin, index) => {
const newCoin = {
x: coin.x,
// 运动的关键 每次只有y不同
y: coin.y + coin.speed,
radius: coin.radius,
img: coin.img,
speed: coin.speed
};
// 绘制某个金币对象时,也同时生成一个新的金币对象,替换掉原来的它,惟一的区别就是它的y变了,下一帧绘制这个金币时,就运动了一点点距离
this.coinArr.splice(index, 1, newCoin);
this.ctx.drawImage(
coin.img,
coin.x,
coin.y,
coin.radius,
coin.radius * 1.5
);
});
},复制代码
那么怎么连贯运动起来呢,不断的执行this.drawCoins()方法便可
既然作动画,咱们确定得知道【window.requestAnimationFrame】这个api
还记得刚开始说的动画核心两步走吗
moveCoins() {
// 清空canvas
this.ctx.clearRect(0, 0, this.innerWidth, this.innerHeight);
// 绘制新的一帧动画
this.drawCoins();
// 不断执行绘制,造成动画
this.moveCoinAnimation = window.requestAnimationFrame(this.moveCoins);
},复制代码
到这里,咱们其实已经能让金币运动起来了,不过咱们要作的是让不少不少金币不断的往下掉,因此咱们选择在运动的过程当中,不断生成新的金币对象,而后push到this.coinArr
中
pushCoins() {
// 每次随机生成1~3个金币
const random = this.randomRound(3, 6);
let arr = [];
for (let i = 0; i < random; i++) {
// 建立新的金币对象
const newCoin = {
x: this.random(
this.calculatePos(10),
this.innerWidth - this.calculatePos(150)
), // 横向随机 金币不要贴近边边
y: 0 - this.calculatePos(Math.random() * 150), // -150内高度 随机
radius: this.calculatePos(120 + Math.random() * 30), // 100宽 大小浮动15
img: this.coinObjs[this.randomRound(0, 3)].img, // 随机取一个金币图片对象,这几个图片对象在页面初始化时就已经缓存好了
speed: this.calculatePos(Math.random() * 7 + 5) // 下落速度 随机
};
arr.push(newCoin);
}
// 每次都插入一批新金币对象arr到运动的金币数组this.coinArr
this.coinArr = [...this.coinArr, ...arr];
// 间隔多久生成一批金币
this.addCoinsTimer = setTimeout(() => {
this.pushCoins();
}, 600);
},复制代码
由于每一个金币的初始y的位置都是屏幕上方,因此看起来都是不断生成金币而后往下掉的
至于计算大小的方法,这个比较随意了
最后,把上面的汇总起来,开启动画的方法是这样的
start() {
this.pushCoins(); // 不断增长金币
this.moveCoins(); // 金币开始运动
// 开始10秒倒计时
this.runCountdownTimer = setInterval(() => {
//...倒计时10s后,作一些中止动画的工做
}, 1000);
},复制代码
到这里,运动过程就已经结束了,先总结一下上面的内容
this.coinArr
window.requestAnimationFrame
,每一帧都用canvas从新遍历绘制this.coinArr
,每一帧都改变this.coinArr
里面的每个对象的y值大小,造成运动感经过上面的效果图,咱们能够看到,点击金币时,对应的这个金币会消失(若是有重叠,只会消失最上面的那个金币),并且还会有个+1的效果,并缓慢上移消失
先思考一下逻辑
首先,canvas自己就是一个DOM对象,绘制在它上面的金币并非dom对象,没法绑定点击事件,因此只能绑定在canvas上面,经过event
拿到点击位置,有点事件代理的味道吧
listenClick() { const canvas = document.getElementById("canvas"); canvas.addEventListener("click", e => { const pos = { x: e.clientX, y: e.clientY }; }); },复制代码
既然拿到此刻的点击位置,而当前的金币数组this.coinArr
也知道,数组里面的每一个金币对象都维护了自身的信息,其中就包括了位置和金币大小
那么,只要遍历一下,若是点击位置在这个金币的大小范围以内,那么是否是能够认为点击中了这个金币?
// 判断点击位置 是否处于某个coin之中
isIntersect(point, coin) {
const distanceX = point.x - coin.x;
const distanceY = point.y - coin.y;
const withinX = distanceX > 0 && distanceX < coin.radius;
// 金币图片是长方形的 咱们只计算下半部的正方形 不计算金币尾巴
const withinY =
distanceY > 0 &&
distanceY > coin.radius * 0.5 &&
distanceY < coin.radius * 1.5;
return withinX && withinY;
},复制代码
但,同一时刻,有可能点中了不少个重叠的金币,那么咱们遍历时,把这几个金币都拿出来,只要最上面那个就行了
listenClick() {
const canvas = document.getElementById("canvas");
canvas.addEventListener("click", e => {
// 点击位置
const pos = {
x: e.clientX,
y: e.clientY
};
// 全部点中的金币都存这
const clickedCoins = [];
this.coinArr.forEach((coin, index) => {
// 判断点击位置是否在该金币范围内
if (this.isIntersect(pos, coin)) {
clickedCoins.push({
x: e.clientX,
y: e.clientY,
// 索引很重要,用于删除this.coinArr内的该金币
index: index
});
}
});
// 若是点击中了重叠的金币,只取第一个便可 也只删除第一个金币 count也只增长一次
if (clickedCoins.length > 0) {
this.count += 1;
const bubble = {
x: clickedCoins[0].x,
y: clickedCoins[0].y,
opacity: 1
};
// 这跟生成+1冒泡效果相关,下面立刻讲
this.bubbleArr.push(bubble);
// 移除被点中的第一个金币对象
this.coinArr.splice(clickedCoins[0].index, 1);
}
});
},复制代码
既然拿到了此刻的位置,在当前位置绘制一个冒泡效果应该不是难事,只要处理好冒泡的移动和消失便可,本质上就跟上面绘制金币是同样的
this.bubbleArr
数组,动画中循环遍历绘制它里面的对象bubble
bubble
有位置信息,加多一个透明度opacity
,运动的过程当中,不断减少透明度,直到变为0,就把这个bubble
从数组上删除便可drawBubble() {
this.bubbleArr.forEach((ele, index) => {
if (ele.opacity > 0) {
// 透明度渐变
this.ctxBubble.globalAlpha = ele.opacity;
this.ctxBubble.drawImage(
this.bubbleImage,
ele.x,
ele.y,
this.calculatePos(60),
this.calculatePos(60)
);
// 更新:每次画完就减小0.02透明度,同时位置移动
const newEle = {
x: ele.x + this.calculatePos(1),
y: ele.y - this.calculatePos(2),
opacity: ele.opacity - 0.02
};
this.bubbleArr.splice(index, 1, newEle);
}
});
},
keepDrawBubble() {
this.ctxBubble.clearRect(0, 0, this.innerWidth, this.innerHeight);
// 把opacity为0的所有清除
this.bubbleArr.forEach((ele, index) => {
if (ele.opacity < 0) {
this.bubbleArr.splice(index, 1);
}
});
this.drawBubble();
this.bubbleAnimation = window.requestAnimationFrame(this.keepDrawBubble);
},复制代码
到这里,整个运动的核心原理就讲完了,咱们测试一下动画的性能
在chrome的性能测试里面能够看到,整个运动过程的fps稳稳保持在60帧每秒,能够说是性能很不错了
感谢您耐心看到这里,但愿有所收获!
我在学习过程当中喜欢作记录,分享的是本身在前端之路上的一些积累和思考,但愿能跟你们一块儿交流与进步,更多文章请看【amandakelake的Github博客】