canvas实现
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>烟花</title>
</head>
<body>
<style type="text/css">
body {
background: #000;
margin: 0;
}
canvas {
cursor: crosshair;
display: block;
}
</style>
<canvas id="canvas">Canvas is not supported in your browser.</canvas>
<script type="text/javascript">
// 当在画布上做动画时,最好使用requestAnimationFrame而不是setTimeout或setInterval
// 但不是所有浏览器都支持,有时需要前缀,所以我们需要一个shim
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 = 120,
// 默认烟花发射的个数阈值
limiterTotal = 5,
// 计数器
limiterTick = 0,
// 默认烟花发射的时间间隔
timerTotal = 80,
// 计时器
timerTick = 0,
mousedown = false,
// 鼠标的x和y轴位置
mx,
my;
canvas.width = cw;
canvas.height = ch;
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));
}
// 创建一个烟火
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) {
if (Math.random() < 0.3) {
// 爆开两朵花
createParticles(this.tx, this.ty, 30);
setTimeout(() => {
createParticles(this.tx, this.ty, 15);
}, 600);
} else {
// 爆开一朵花(大小:20-40)
createParticles(this.tx, this.ty, 30);
}
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();
};
// 爆开的烟火(一个粒子)
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.001, 0.05);
}
// 更新粒子位置
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, num) {
var particleCount = num;
while (particleCount--) {
particles.push(new Particle(x, y));
}
}
// 发射烟花
function loop() {
// 这个函数将通过requestAnimationFrame无休止地运行
requestAnimFrame(loop);
hue += 0.5;
// 通常,clearRect()将用于清除画布
// 我们想要创造一个尾随效果
// 将复合操作设置为destination-out将允许我们以特定的不透明度清除画布,而不是完全清除它
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.fillRect(0, 0, cw, ch);
// lighter创造明亮的亮点,因为烟花和粒子相互重叠
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);
}
//随机烟花, timerTotal是烟花发送的时间间隔,timerTick是计数器
if (timerTick >= timerTotal) {
if (!mousedown) {
fireworks.push(
// 屏幕的左上角为坐标原点
new Firework(cw / 2, ch, random(0, cw), random(0, ch / 2))
);
timerTick = 0;
}
} else {
timerTick++;
}
// 鼠标点击烟花, limiterTotal是烟花发射的阈值, 当超过该阈值可以发射烟花
if (limiterTick >= limiterTotal) {
if (mousedown) {
fireworks.push(new Firework(cw / 2, ch, mx, my));
limiterTick = 0;
}
} else {
limiterTick++;
}
}
// 获取鼠标位置
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;
</script>
</body>
</html>
效果:
编写canvas动画的大致规律
定义一个实体对象
,包含需要绘制对象的基本信息, 一般包:对象位置(x,y)、对象方向(angle)、对象速度(speed)以及一些其他的对象属性- 准备一个
对象数组
,用来包含定义好的实体对象数组 - 定义一个
绘制函数
,用来绘目标对象图形,一般根对象的位置属性来确定绘制的位置,再结合canvas绘制出对应的图形 - 定义一个
更新函数
,用来更新目标对象的位置和形状,一般根据对象的速度和方向计算出对象水平和竖直方向移动的距离,更新位置,并确定对象的消失条件 - 对象
创建事件
:实体对象需要一定的时机进行创建,一般是触发某个事件,鼠标移动或点击事件,创建的实体对象需要存储在定义的对象数组
中 - canvas动画绘制函数:一切准备好之后就可以绘制动画效果了。一般定义一个定时器 setInterval、setTimeout(结合自调用) 或者 window.requestAnimationFrame 来实现动态效果。每次处理时,先调用canvas的函数清除上一"帧"的内容,再遍历
对象数组
调用绘制函数
将对象数组中的内容绘制出来,同时调用更新函数
更新对象的位。