现在将介绍一个基于 Canvas 的动态爱心粒子动画实现,主要功能是生成一个由粒子组成的、会周期性脉动的爱心轮廓,粒子会沿着轨迹移动并留下拖尾效果。以下是详细的技术分析:
一、整体结构与核心依赖
- HTML:仅包含一个
<canvas>
元素作为绘图容器,无其他DOM元素。 - CSS:设置 canvas 为全屏覆盖,背景为半透明黑色(
rgba(0,0,0,.2)
)。 - JavaScript:核心逻辑集中在 Canvas 绘图、粒子系统控制和动画循环。
二、代码
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>💗</title>
<style>
canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .2);
}
</style>
</head>
<body>
<canvas id="heart" width="1920" height="947"></canvas>
<script>
window.requestAnimationFrame =
window.__requestAnimationFrame ||
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
(function () {
return function (callback, element) {
var lastTime = element.__lastTime;
if (lastTime === undefined) {
lastTime = 0;
}
var currTime = Date.now();
var timeToCall = Math.max(1, 33 - (currTime - lastTime));
window.setTimeout(callback, timeToCall);
element.__lastTime = currTime + timeToCall;
};
})();
window.isDevice = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(((navigator.userAgent || navigator.vendor || window.opera)).toLowerCase()));
var loaded = false;
var init = function () {
if (loaded) return;
loaded = true;
var mobile = window.isDevice;
var koef = mobile ? 0.5 : 1;
var canvas = document.getElementById('heart');
var ctx = canvas.getContext('2d');
var width = canvas.width = koef * innerWidth;
var height = canvas.height = koef * innerHeight;
var rand = Math.random;
ctx.fillStyle = "rgba(0,0,0,1)";
ctx.fillRect(0, 0, width, height);
var heartPosition = function (rad) {
//return [Math.sin(rad), Math.cos(rad)];
return [Math.pow(Math.sin(rad), 3), -(15 * Math.cos(rad) - 5 * Math.cos(2 * rad) - 2 * Math.cos(3 * rad) - Math.cos(4 * rad))];
};
var scaleAndTranslate = function (pos, sx, sy, dx, dy) {
return [dx + pos[0] * sx, dy + pos[1] * sy];
};
window.addEventListener('resize', function () {
width = canvas.width = koef * innerWidth;
height = canvas.height = koef * innerHeight;
ctx.fillStyle = "rgba(0,0,0,1)";
ctx.fillRect(0, 0, width, height);
});
var traceCount = mobile ? 20 : 50;
var pointsOrigin = [];
var i;
var dr = mobile ? 0.3 : 0.1;
for (i = 0; i < Math.PI * 2; i += dr) pointsOrigin.push(scaleAndTranslate(heartPosition(i), 210, 13, 0, 0));
for (i = 0; i < Math.PI * 2; i += dr) pointsOrigin.push(scaleAndTranslate(heartPosition(i), 150, 9, 0, 0));
for (i = 0; i < Math.PI * 2; i += dr) pointsOrigin.push(scaleAndTranslate(heartPosition(i), 90, 5, 0, 0));
var heartPointsCount = pointsOrigin.length;
var targetPoints = [];
var pulse = function (kx, ky) {
for (i = 0; i < pointsOrigin.length; i++) {
targetPoints[i] = [];
targetPoints[i][0] = kx * pointsOrigin[i][0] + width / 2;
targetPoints[i][1] = ky * pointsOrigin[i][1] + height / 2;
}
};
var e = [];
for (i = 0; i < heartPointsCount; i++) {
var x = rand() * width;
var y = rand() * height;
e[i] = {
vx: 0,
vy: 0,
R: 2,
speed: rand() + 5,
q: ~~(rand() * heartPointsCount),
D: 2 * (i % 2) - 1,
force: 0.2 * rand() + 0.7,
f: "hsla(240," + ~~(40 * rand() + 60) + "%," + ~~(60 * rand() + 20) + "%,.3)", //修改颜色
trace: []
};
for (var k = 0; k < traceCount; k++) e[i].trace[k] = { x: x, y: y };
}
var config = {
traceK: 0.4,
timeDelta: 0.01
};
var time = 0;
var loop = function () {
var n = -Math.cos(time);
pulse((1 + n) * .5, (1 + n) * .5);
time += ((Math.sin(time)) < 0 ? 9 : (n > 0.8) ? .2 : 1) * config.timeDelta;
ctx.fillStyle = "rgba(0,0,0,.1)";
ctx.fillRect(0, 0, width, height);
for (i = e.length; i--;) {
var u = e[i];
var q = targetPoints[u.q];
var dx = u.trace[0].x - q[0];
var dy = u.trace[0].y - q[1];
var length = Math.sqrt(dx * dx + dy * dy);
if (10 > length) {
if (0.95 < rand()) {
u.q = ~~(rand() * heartPointsCount);
} else {
if (0.99 < rand()) {
u.D *= -1;
}
u.q += u.D;
u.q %= heartPointsCount;
if (0 > u.q) {
u.q += heartPointsCount;
}
}
}
u.vx += -dx / length * u.speed;
u.vy += -dy / length * u.speed;
u.trace[0].x += u.vx;
u.trace[0].y += u.vy;
u.vx *= u.force;
u.vy *= u.force;
for (k = 0; k < u.trace.length - 1;) {
var T = u.trace[k];
var N = u.trace[++k];
N.x -= config.traceK * (N.x - T.x);
N.y -= config.traceK * (N.y - T.y);
}
ctx.fillStyle = u.f;
for (k = 0; k < u.trace.length; k++) {
ctx.fillRect(u.trace[k].x, u.trace[k].y, 1, 1);
}
}
ctx.fillStyle = "rgba(255,255,255,1)";
for (i = u.trace.length + 13; i--;) ctx.fillRect(targetPoints[i][0], targetPoints[i][1], 2, 2);
window.requestAnimationFrame(loop, canvas);
};
loop();
};
var s = document.readyState;
if (s === 'complete' || s === 'loaded' || s === 'interactive') init();
else document.addEventListener('DOMContentLoaded', init, false);
</script>
</body>
</html>
三、关键技术点解析
1. Canvas 初始化与自适应
- 画布尺寸:初始设置
width="1920" height="947"
,但通过 JavaScript 动态调整为窗口实际尺寸(koef * innerWidth/Height
),确保全屏显示。 - 重绘处理:窗口 resize 时,重新设置画布尺寸并清空背景,避免画面拉伸变形。
2. 爱心轮廓的数学生成
- 心形参数方程:通过
heartPosition
函数定义心形的极坐标方程,转换为笛卡尔坐标:return [Math.pow(Math.sin(rad), 3), -(15 * Math.cos(rad) - 5 * Math.cos(2 * rad) - 2 * Math.cos(3 * rad) - Math.cos(4 * rad))];
scaleAndTranslate
)后,形成居中的爱心轮廓。 - 多层级轮廓:通过三次不同缩放(210、150、90)生成三层爱心轮廓,增加细节层次。
3. 粒子系统设计
- 粒子属性:每个粒子(对象
e[i]
)包含位置(trace
数组记录轨迹)、速度(vx/vy
)、大小(R
)、运动参数(speed
、force
)、颜色(f
)等。 - 轨迹拖尾:通过
trace
数组记录粒子历史位置(长度由traceCount
控制),绘制时从旧到新逐点绘制,形成逐渐消失的拖尾效果。 - 目标追踪:粒子会向随机选择的心形轮廓点(
targetPoints
)移动,通过计算粒子与目标点的距离调整速度方向(类似“引力”效果)。
4. 动态脉动效果
- 时间控制:通过
time
变量和余弦函数(Math.cos(time)
)控制心形的周期性收缩与膨胀:pulse((1 + n) * .5, (1 + n) * .5); // n = -Math.cos(time)
pulse
函数调整心形轮廓点的缩放比例,使爱心呈现“呼吸感”。
5. 性能优化
- 设备适配:通过
window.isDevice
检测移动设备,减少粒子数量(traceCount
)和运动步长(dr
),降低计算压力。 - 绘制优化:使用
ctx.fillRect(x, y, 1, 1)
绘制单像素粒子,比绘制圆形更高效;背景用半透明黑色覆盖(rgba(0,0,0,.1)
),利用“残影”实现拖尾效果,避免频繁清空画布。
四、视觉效果总结
- 核心元素:动态脉动的爱心轮廓 + 沿轨迹移动的粒子拖尾。
- 颜色风格:粒子颜色为随机蓝紫色(
hsla(240, ..., ..., .3)
),背景为深色,突出粒子轨迹。 - 动态表现:爱心周期性收缩膨胀,粒子不断向轮廓点移动并更新轨迹,形成流动的“星光”效果。
总结
主要通过 Canvas 和粒子系统实现了一个简洁而优雅的动态爱心动画,适合作为情感类网页(如情侣空间、婚礼网站)的背景或装饰元素。核心亮点在于利用数学方程生成心形轮廓、粒子的轨迹拖尾效果,以及自适应不同设备的性能优化。