说在前面
现在很多地方过年都禁止燃放烟花了,没有烟花总感觉少了点年味,现实中我们需要遵守法律法规,无法燃放烟花,那么为什么不试试到网上来放个烟花
效果展示
预览地址
实现思路
利用 HTML5 的 Canvas 元素和 JavaScript 实现烟花从发射到绽放的动画效果。主要分为以下几个步骤:
- 初始化设置:包括设置画布大小、定义全局变量和封装动画帧请求函数。
- 创建烟花和粒子对象:定义烟花和粒子的属性和行为。
- 更新和绘制对象:不断更新烟花和粒子的位置,并将它们绘制到画布上。
- 处理用户交互:监听鼠标事件,根据用户点击位置发射烟花。
- 循环动画:使用
requestAnimationFrame
实现动画的循环更新。
代码实现
1. HTML 结构
<!doctype html>
<html>
<head>
<style>
body {
background: #132c33;
margin: 0;
color: white;
}
canvas {
cursor: crosshair;
display: block;
}
</style>
</head>
<body>
<canvas id="canvas">Canvas is not supported in your browser.</canvas>
<script>
// JavaScript 代码部分
</script>
</body>
</html>
HTML 部分非常简单,包含一个 canvas
元素用于绘制烟花特效,同时通过 CSS 设置了页面背景颜色和 canvas
的样式。
2. JavaScript 代码
2.1 初始化设置
//requestAnimFrame 封装,可以兼容所有浏览器
window.requestAnimFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
}
);
})();
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
cw = window.innerWidth,
ch = window.innerHeight,
fireworks = [],
particles = [],
hue = 180,
limiterTotal = 5,
limiterTick = 0,
timerTotal = 40,
timerTick = 0,
mousedown = false,
mx,
my;
canvas.width = cw;
canvas.height = ch;
requestAnimFrame
:封装了requestAnimationFrame
函数,确保在不同浏览器中都能正常使用,实现平滑的动画效果。- 获取
canvas
元素和 2D 绘图上下文。 - 获取浏览器窗口的宽度和高度,并设置
canvas
的大小。 - 定义了一些全局变量,用于存储烟花、粒子、颜色、速度控制等信息。
2.2 辅助函数
// 随机函数
function random(min, max) {
return Math.random() * (max - min) + min;
}
// 计算两点距离
function calculateDistance(p1x, p1y, p2x, p2y) {
var xDistance = p1x - p2x,
yDistance = p1y - p2y;
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}
random
函数用于生成指定范围内的随机数。calculateDistance
函数用于计算两点之间的距离,在烟花运动和判断是否到达目标位置时会用到。
2.3 烟花对象
// 定义烟花对象
function Firework(sx, sy, tx, ty) {
this.x = sx;
this.y = sy;
this.sx = sx;
this.sy = sy;
this.tx = tx;
this.ty = ty;
this.distanceToTarget = calculateDistance(sx, sy, tx, ty);
this.distanceTraveled = 0;
this.coordinates = [];
this.coordinateCount = 3;
while (this.coordinateCount--) {
this.coordinates.push([this.x, this.y]);
}
this.angle = Math.atan2(ty - sy, tx - sx);
this.speed = 2;
this.acceleration = 1.05;
this.brightness = random(50, 70);
this.targetRadius = 1;
}
//更新烟花位置
Firework.prototype.update = function (index) {
this.coordinates.pop();
this.coordinates.unshift([this.x, this.y]);
if (this.targetRadius < 8) {
this.targetRadius += 0.3;
} else {
this.targetRadius = 1;
}
this.speed *= this.acceleration;
var vx = Math.cos(this.angle) * this.speed,
vy = Math.sin(this.angle) * this.speed;
this.distanceTraveled = calculateDistance(
this.sx,
this.sy,
this.x + vx,
this.y + vy
);
if (this.distanceTraveled >= this.distanceToTarget) {
createParticles(this.tx, this.ty);
fireworks.splice(index, 1);
} else {
this.x += vx;
this.y += vy;
}
};
// 烟花射线发射
Firework.prototype.draw = function () {
ctx.beginPath();
ctx.moveTo(
this.coordinates[this.coordinates.length - 1][0],
this.coordinates[this.coordinates.length - 1][1]
);
ctx.lineTo(this.x, this.y);
ctx.strokeStyle = "hsl(" + hue + ", 100%, " + this.brightness + "%)";
ctx.stroke();
ctx.beginPath();
ctx.arc(this.tx, this.ty, this.targetRadius, 0, Math.PI * 2);
ctx.stroke();
};
Firework
构造函数:创建一个烟花对象,包含起始位置、目标位置、运动距离、速度、亮度等属性。update
方法:更新烟花的位置和状态。当烟花到达目标位置时,调用createParticles
函数创建粒子效果,并从fireworks
数组中移除该烟花。draw
方法:绘制烟花的射线和目标位置的圆圈。
2.4 粒子对象
//烟花绽放方法
function Particle(x, y) {
this.x = x;
this.y = y;
this.coordinates = [];
this.coordinateCount = 5;
while (this.coordinateCount--) {
this.coordinates.push([this.x, this.y]);
}
this.angle = random(0, Math.PI * 2);
this.speed = random(1, 10);
this.friction = 0.95;
this.gravity = 1;
this.hue = random(hue - 20, hue + 20);
this.brightness = random(50, 80);
this.alpha = 1;
this.decay = random(0.015, 0.03);
}
Particle.prototype.update = function (index) {
this.coordinates.pop();
this.coordinates.unshift([this.x, this.y]);
this.speed *= this.friction;
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed + this.gravity;
this.alpha -= this.decay;
if (this.alpha <= this.decay) {
particles.splice(index, 1);
}
};
// 烟花绽放
Particle.prototype.draw = function () {
ctx.beginPath();
ctx.moveTo(
this.coordinates[this.coordinates.length - 1][0],
this.coordinates[this.coordinates.length - 1][1]
);
ctx.lineTo(this.x, this.y);
ctx.strokeStyle =
"hsla(" +
this.hue +
", 100%, " +
this.brightness +
"%, " +
this.alpha +
")";
ctx.stroke();
};
// 创建粒子
function createParticles(x, y) {
var particleCount = 200;
while (particleCount--) {
particles.push(new Particle(x, y));
}
}
Particle
构造函数:创建一个粒子对象,包含位置、运动方向、速度、颜色、透明度等属性。update
方法:更新粒子的位置和透明度。当粒子的透明度小于一定值时,从particles
数组中移除该粒子。draw
方法:绘制粒子的线条。createParticles
函数:在指定位置创建多个粒子对象,模拟烟花绽放的效果。
2.5 动画循环
function loop() {
requestAnimFrame(loop);
hue += 0.5;
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.fillRect(0, 0, cw, ch);
ctx.globalCompositeOperation = "lighter";
var i = fireworks.length;
while (i--) {
fireworks[i].draw();
fireworks[i].update(i);
}
var i = particles.length;
while (i--) {
particles[i].draw();
particles[i].update(i);
}
if (timerTick >= timerTotal) {
if (!mousedown) {
fireworks.push(
new Firework(cw / 2, ch, random(0, cw), random(0, ch / 2))
);
timerTick = 0;
}
} else {
timerTick++;
}
if (limiterTick >= limiterTotal) {
if (mousedown) {
fireworks.push(new Firework(cw / 2, ch, mx, my));
limiterTick = 0;
}
} else {
limiterTick++;
}
}
loop
函数:使用requestAnimFrame
实现动画的循环更新。- 每次循环更新颜色值
hue
,并通过destination-out
合成模式和半透明黑色填充画布,实现渐变消失的效果。 - 遍历
fireworks
和particles
数组,分别调用draw
和update
方法更新和绘制烟花及粒子。 - 根据计时器控制自动发射烟花和根据鼠标点击发射烟花的频率。
2.6 事件监听
canvas.addEventListener("mousemove", function (e) {
mx = e.pageX - canvas.offsetLeft;
my = e.pageY - canvas.offsetTop;
});
canvas.addEventListener("mousedown", function (e) {
e.preventDefault();
mousedown = true;
});
canvas.addEventListener("mouseup", function (e) {
e.preventDefault();
mousedown = false;
});
window.onload = loop;
- 监听鼠标移动事件,记录鼠标位置。
- 监听鼠标按下和松开事件,控制是否根据鼠标点击发射烟花。
- 页面加载完成后,调用
loop
函数开始动画循环。
源码地址
gitee
github
- 🌟觉得有帮助的可以点个star~
- 🖊有什么问题或错误可以指出,欢迎pr~
- 📬有什么想要实现的组件或想法可以联系我~
公众号
关注公众号『前端也能这么有趣
』,获取更多有趣内容。
公众号发送 加群 可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『
前端也能这么有趣
』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。