流星是一种唯美的天文现象,我一度想用所学知识将它绘制,最近阅读MDN上的canvas教程得到启发,用一个canvas的长尾效果绘制流星……
什么是长尾效果?
我们知道,canvas动画实现依赖于画布的重绘,通过不停的清空画布,绘制画布就能实现基本的动画效果。一般使用clearRect方法清除指定矩形区域,来实现重绘。长尾效果是使用透明的填充色代替clearRect方法来实现的。
使用clearRect
吐槽:由于录屏软件fps跟不上canvas所以这个gif图有点卡顿
使用fillRect
1.透明度为1
2.透明度为0
3.长尾效果
流星
可以将流星解构为一个圆形和长尾效果:
肉眼效果:
封装页面形状
使用面向对象编程完成canvas绘制
月亮类
class Moon {
constructor(x, y, ctx, r = 25) {
this.x = x;
this.y = y;
this.ctx = ctx;
this.r = r;
}
draw() {
this.ctx.fillStyle = 'rgba(255,255,255,0.6)';
this.ctx.shadowBlur = this.r + 5; //光晕半径
this.ctx.shadowColor = "#fff"; // 光晕颜色
this.ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
this.ctx.fill();
}
}
复制代码
因为月亮是静止在页面上的,所以只有一个draw方法,月亮光晕的实现是把阴影和填充设为同一颜色,然后让阴影透明度大于填充透明度,就形成一个外发光的效果。
星星类
class Star extends Moon {
constructor(x, y, ctx, r) {
super(x, y, ctx, r);
}
draw() {
this.ctx.fillStyle = 'rgba(255,255,255,0.8)';
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
this.ctx.closePath();
this.ctx.fill();
}
move() {
this.x += 0.08;
if (this.x > meteorCanvas.width) {
this.x = 0;
}
this.draw();
}
}
复制代码
星星与月亮的唯一区别是可以移动,所以用星星类去继承月亮类,实现面向对象的继承与多态。
用星星缓慢的向右移动可以模拟地球自转带来的效果。
流星类
class Meteor extends Star {
constructor(x, y, ctx, r,angle) {
super(x, y, ctx, r);
this.angle = angle;
}
draw() {
this.ctx.fillStyle = '#ffffff';
this.ctx.rotate(this.angle);
this.ctx.translate(100, -meteorCanvas.height / 1.5);
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
this.ctx.closePath();
this.ctx.fill();
this.ctx.translate(-100, meteorCanvas.height / 1.5);
this.ctx.rotate(-this.angle);
}
move() {
this.x += 4;
this.y += 1;
if (this.x > meteorCanvas.width) {
this.x = Math.random() * 5
this.y = -2 * meteorCanvas.height + Math.random() * meteorCanvas.height * 3;
}
this.draw();
}
}
复制代码
同理用流星类去继承星星类。
注意的是,流星类与星星类运动的不同之处是流星划过天空有一个夹角,所以在绘制时将画布旋转了角度之后,需要回归原位。
为了让流星出现的位置不会太密集,我将流星在y轴出现的位置设置在-2倍画布高度到1倍画布高度之间,并在draw方法中将画布往上挪了画布高度的2/3(同理要将画布归位)。
绘制canvas
流星需要使用长尾效果渲染,星星需要clearRect重绘,月亮就只需要绘制一次。为了三种形状互不干扰,我分别使用了不同画布去渲染它们。
优点:互不干扰,绘制逻辑清晰,优化渲染。
源码
const meteorCanvas = document.getElementById('meteor');
const starCanvas = document.getElementById('star');
const moonCanvas = document.getElementById('moon');
const meteors = [], stars = [];
meteorCanvas.width = document.body.clientWidth;
meteorCanvas.height = document.body.clientHeight;
starCanvas.width = document.body.clientWidth;
starCanvas.height = document.body.clientHeight / 3;
moonCanvas.width = document.body.clientWidth;
moonCanvas.height = document.body.clientHeight / 3;
const meteorCtx = meteorCanvas.getContext('2d');
const starCtx = starCanvas.getContext('2d');
const moonCtx = moonCanvas.getContext('2d');
init();
animate();
function init() {
for (var i = 0; i < 4; i++) {
meteors[i] = new Meteor(Math.random() * meteorCanvas.width,
-2 * meteorCanvas.height + Math.random() * meteorCanvas.height * 3,
meteorCtx, Math.floor(Math.random() * 2) + 1.5, Math.PI / 7);
meteors[i].draw();
}
for (var i = 0; i < 60; i++) {
stars[i] = new Star(Math.random() * starCanvas.width, Math.random() * starCanvas.height,
starCtx, Math.random());
stars[i].draw();
}
moon = new Moon(moonCanvas.width - 50, 50, moonCtx)
moon.draw();
}
function animate() {
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
meteorCtx.fillStyle = `rgba(0, 0, 0, 0.1)`;
meteorCtx.fillRect(0, 0, meteorCanvas.width, meteorCanvas.height);
for (let meteor of meteors)
meteor.move();
for (let star of stars)
star.move();
requestAnimationFrame(animate);
}
function recover() {
for (let meteor of meteors)
meteor = null;
for (let star of stars)
star = null;
moon = null;
}
window.onresize = function () {
meteorCanvas.width = document.body.clientWidth;
meteorCanvas.height = document.body.clientHeight;
starCanvas.width = document.body.clientWidth;
starCanvas.height = document.body.clientHeight / 3;
moonCanvas.width = document.body.clientWidth;
moonCanvas.height = document.body.clientHeight / 3;
recover();
init();
}
复制代码
结语
陪你去看流星雨落在这地球上
让你的泪落在我肩膀
要你相信我的爱只肯为你勇敢……
文章随着《流星雨》的歌声,也走向了尾声。人生如流星划过,转瞬即逝,然而,流星易逝,真情永恒……