我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛
前言
大家好,我是追梦玩家。中秋即将到来,提前祝大家中秋快乐啊!!!19年,我写了一篇文章手把手教你实现一个canvas智绘画板,为什么要说这件事呢?没错,又要使用 canvas 来实现满月中秋,流星夜空。废话少说,赶紧开始吧。
效果展示
具体效果就是这样,其实就是这几个内容:月亮、星星、流星。
canvas 是什么,能做什么?
mdn 文档:
<canvas>
是一个可以使用脚本(通常为JavaScript)来绘制图形的 HTML 元素.例如,它可以用于绘制图表、制作图片构图或者制作简单的(以及不那么简单的)动画
简单来说,Canvas 是 HTML5 新增的组件,它就像一块幕布,可以用 JavaScript 在上面绘制各种图表、动画等。
使用面向对象编程实现效果
书写 HTML
处理逻辑,是放在单独的 js 文件:main.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>明月当空,共赏花好月圆夜</title>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
position: fixed;
top: 0;
left: 0;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="./main.js"></script>
</body>
</html>
初始化
let canvas = document.getElementById('canvas'),
context = canvas.getContext('2d'),
width = window.innerWidth,
height = window.innerHeight,
count = 0,
meteors = []; // 流星集合
canvas.width = width;
canvas.height = height;
月亮类
主要使用到 canvas 的 createRadialGradient 方法,实现径向渐变效果。
class Moon {
constructor(context, width, height) {
this.context = context;
this.width = width;
this.height = height;
this.circle_x = width / 2; // 旋转轨迹圆心的 X 坐标
this.circle_y = width; // 旋转轨迹圆心的 Y 坐标
this.circle_r = width; // 旋转轨迹的半径
this.angle = Math.atan2(Math.sqrt(width * width * 3 / 4), -width / 2); // 旋转轨迹的角度
this.startAngle = Math.atan2(Math.sqrt(width * width * 3 / 4), -width / 2 - 400); // 开始旋转的角度
this.endAngle = Math.atan2(Math.sqrt(width * width * 3 / 4), width / 2 + 200); // 结束旋转的角度
this.x = 0; // 月亮的 X 坐标
this.y = 0; // 月亮的 Y 坐标
}
draw() {
const { context, x, y, width, height } = this;
// createRadialGradient 实现径向渐变
const gradient = context.createRadialGradient(x, y, 50, x, y, 600);
gradient.addColorStop(0, 'rgb(255, 255, 255)');
gradient.addColorStop(0.01, 'rgb(70, 80, 80)');
gradient.addColorStop(0.2, 'rgb(40, 40, 50)');
gradient.addColorStop(0.4, 'rgb(20, 20, 30)');
gradient.addColorStop(1, '#080d23');
// save 方法:将当前状态放入栈中,保存 canvas 全部状态的方法
context.save();
context.fillStyle = gradient; // 填充
context.fillRect(0, 0, width, height); // 绘制一个填充了内容的矩形
context.restore(); // 将 canvas 恢复到最近的保存状态
}
// 让月亮动起来
move() {
const { circle_x, circle_y, circle_r, angle, startAngle, endAngle } = this;
this.angle = angle - 0.0001;
if (this.angle <= endAngle) {
this.angle = startAngle;
}
this.x = circle_x + (circle_r * Math.cos(angle));
this.y = circle_y - (circle_r * Math.sin(angle)) + 50;
}
}
星星类
通过 getStars 方法生成一个星星的集合,所有的星星都保存在实例的 stars 中,闪烁效果,是通过调用 blink 函数就可以实现,主要是随机改变每个星星的半径大小。
class Stars {
constructor(context, width, height, amount) {
this.context = context;
this.width = width;
this.height = height;
// 通过方法去生成星星集合
this.stars = this.getStars(amount);
}
// 获取指定数量的星星
getStars(amount) {
let stars = [];
while (amount--) {
stars.push({
x: Math.random() * this.width,
y: Math.random() * this.height,
r: Math.random() + 0.5,
})
}
return stars;
}
// 描绘
draw() {
const { context } = this;
context.save();
context.fillStyle = 'white';
this.stars.forEach(star => {
context.beginPath();
context.arc(star.x, star.y, star.r, 0, 2 * Math.PI);
context.fill();
})
context.restore();
}
// 闪烁,让星星半径随机变大或变小,实现一闪一闪亮晶晶的效果
blink() {
this.stars = this.stars.map(star => {
const sign = Math.random() > 0.5 ? 1 : -1;
star.r += sign * 0.2;
if (star.r < 0) {
star.r = -star.r;
} else if (star.r > 1) {
star.r -= 0.2;
}
return star;
})
}
}
流星类
flow 方法,判断当前流星是否出界,所谓出界,就是离开视野之内,就销毁流星实例,回收内存。
流星是怎么实现的呢?
流星其实有流星头和流星尾,流星头是一个半圆组成,流星尾是一个三角形组成,然后整体倾角45度,并且填充时用上一个径向渐变,实现好看的流星效果。
class Meteor {
constructor(context, x, h) {
this.context = context;
this.x = x;
this.y = 0;
this.h = h;
this.vx = -(5 + Math.random() * 5);
this.vy = -this.vx;
this.len = Math.random() * 300 + 100;
}
flow() {
// 判定流星出界
if (this.x < -this.len || this.y > this.h + this.len) {
return false;
}
this.x += this.vx;
this.y += this.vy;
return true;
}
draw() {
const { context } = this;
// 径向渐变,从流星头尾圆心,半径越大,透明度越高
let gradient = context.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.len);
const PI = Math.PI;
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
context.save();
context.fillStyle = gradient;
context.beginPath();
// 流星头,二分之一圆
context.arc(this.x, this.y, 0.5, PI / 4, 5 * PI / 4);
// 绘制流星尾,三角形
context.lineTo(this.x + this.len, this.y - this.len);
context.closePath();
context.fill();
context.restore();
}
}
// 生成流星
const meteorGenerator = () => {
const x = Math.random() * width;
meteors.push(new Meteor(context, x, height));
}
每一帧动画生成函数
所有的动画都是通过帧来实现的。 调用 frame 方法,就可以生成每一帧动画。
// 实例化星星和月亮
let moon = new Moon(context, width, height),
stars = new Stars(context, width, height, 200);
// 每一帧动画生成函数
const frame = () => {
count++;
// 每隔 10 帧星星闪烁一次,节省计算资源
count % 10 == 0 && stars.blink();
// 每隔 300 帧,会有流星划过夜空
count % 300 == 0 && meteorGenerator();
moon.move();
moon.draw();
stars.draw();
meteors.forEach((meteor, index, arr) => {
// 如果流星离开视野之内,销毁流星实例,回收内存
if (meteor.flow()) {
meteor.draw();
} else {
arr.splice(index, 1);
}
})
requestAnimationFrame(frame);
}
frame();
canvas 使用到的属性和方法汇总
- createRadialGradient
- addColorStop
- save
- fillStyle
- fillRect
- restore
- beginPath
- arc
- fill
- lineTo
- closePath
参考
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。