用HTML5 Canvas打造交互式心形粒子动画:从基础到优化实战

引言
在Web交互设计中,粒子动画因其动态美感和视觉吸引力被广泛应用于节日特效、情感化界面等场景。本文将通过实战案例,详细讲解如何使用HTML5 Canvas和JavaScript实现一个「心之律动」交互式粒子艺术效果,包含心形粒子循环动画、鼠标轨迹粒子、烟花爆炸及坠落效果,并分享关键优化技巧。
技术栈概览
- HTML5 Canvas:实现高性能粒子渲染
- JavaScript:粒子系统逻辑控制
- Tailwind CSS:快速构建UI界面
- Font Awesome:图标库支持
一、基础框架搭建
1. 画布初始化
html
<canvas id="canvas"></canvas>
javascript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 自适应屏幕尺寸
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
2. UI界面设计
使用Tailwind CSS构建半透明控制栏和信息面板,实现响应式布局:
html
<div class="controls">
<div class="control-btn" id="reset-btn"><i class="fa fa-refresh"></i></div>
<!-- 暂停/增加/减少按钮 -->
</div>
<div class="info-panel">
<div>粒子数量: <span id="particle-count">0</span></div>
<p><i class="fa fa-mouse-pointer"></i> 鼠标移动生成轨迹</p>
</div>
二、核心粒子系统实现
1. 粒子类设计
定义Particle
类,通过type
属性区分不同粒子类型(心形/鼠标轨迹/烟花/坠落),实现多态行为:
javascript
class Particle {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
// 根据类型初始化不同属性
type === 'heart' ? this.setupHeartParticle() :
type === 'mouse' ? this.setupMouseParticle() :
type === 'firework' ? this.setupFireworkParticle() :
this.setupFallingParticle();
}
// 心形粒子专属属性
setupHeartParticle() {
this.layer = Math.floor(Math.random() * 4); // 0-3层
this.color = particleColors[this.layer][Math.floor(Math.random() * 3)];
this.size = 2 + (8 - this.layer * 2) * Math.random();
this.angle = Math.random() * 2 * Math.PI; // 随机方向
this.life = 150 + 100 * Math.random() - this.layer * 30; // 分层寿命
}
// 更新粒子状态
update() {
// 心形粒子使用极坐标运动
if (this.type === 'heart') {
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed;
}
// 烟花粒子使用笛卡尔坐标+物理模拟
else if (this.type === 'firework') {
this.vx *= this.friction; // 摩擦力
this.vy += this.gravity; // 重力
this.x += this.vx;
this.y += this.vy;
}
// 生命周期管理
this.life--;
this.currentAlpha = this.life / this.maxLife;
}
}
三、心形动画核心实现
1. 心形参数方程
使用经典心形参数方程生成粒子初始位置:
javascript
// 心形参数方程:x=16sin³t,y=13cost-5cos2t-2cos3t-cos4t
generateHeartPoint(t, scale) {
const x = 16 * Math.pow(Math.sin(t), 3);
const y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t);
// 映射到画布中心并缩放
return {
x: canvas.width/2 + x * (canvas.width*0.35*scale)/16,
y: canvas.height/2 - y * (canvas.width*0.35*scale)/16
};
}
2. 粒子循环再生机制
通过每帧检测心形粒子数量,动态补充消失的粒子,实现持续动画:
javascript
class HeartAnimation {
constructor() {
this.heartParticleCount = 1500; // 目标粒子数
this.heartRegenRate = 5; // 每帧再生数量
this.generateHeartParticles(this.heartParticleCount);
}
animate() {
// 检测存活心形粒子数量
let heartCount = this.particles.filter(p => p.type === 'heart').length;
// 补充缺失粒子
if (heartCount < this.heartParticleCount) {
const toAdd = Math.min(this.heartRegenRate, this.heartParticleCount - heartCount);
this.regenerateHeartParticles(toAdd);
}
requestAnimationFrame(this.animate.bind(this));
}
}
四、烟花效果深度优化
1. 物理模拟增强
- 笛卡尔坐标系 :使用
vx/vy
分量精确控制运动 - 重力系统 :
this.gravity = 0.05
模拟自由落体 - 空气阻力 :
this.friction = 0.97
实现速度衰减
javascript
setupFireworkParticle() {
this.vx = Math.cos(this.angle) * this.baseSpeed;
this.vy = Math.sin(this.angle) * this.baseSpeed;
this.gravity = 0.05;
this.friction = 0.97 + Math.random()*0.01;
}
2. 多阶段爆炸效果
通过延迟释放不同类型粒子,模拟真实烟花层次感:
javascript
createFirework(x, y) {
// 主爆炸
this.createFireworkWave(x, y, 180, 0);
// 150ms后释放外围粒子
setTimeout(() => {
this.createFireworkWave(x, y, 120, 10, false, 1.5);
}, 150);
// 250ms后释放精细粒子
setTimeout(() => {
this.createFireworkWave(x, y, 150, 15, true);
}, 250);
}
3. 坠落效果转换
当烟花粒子速度低于阈值时,转换为坠落粒子并添加风力效果:
javascript
if (this.type === 'firework' && Math.abs(this.vy) < 0.3) {
this.type = 'falling';
this.setupFallingParticle(); // 启用风力和更快下落
}
五、交互功能实现
1. 鼠标轨迹生成
通过高频次生成带随机偏移的粒子,形成连续轨迹:
javascript
handleMouseMove(e) {
const now = Date.now();
if (now - this.lastMouseMove > 15) {
// 每次移动生成6个偏移粒子
for (let i=0; i<6; i++) {
this.particles.push(new Particle(
e.clientX + (Math.random()-0.5)*20,
e.clientY + (Math.random()-0.5)*20,
'mouse'
));
}
this.lastMouseMove = now;
}
}
2. 控制按钮逻辑
实现粒子数量调整、动画暂停和重置功能:
javascript
handleIncrease() {
this.heartParticleCount += 300;
this.generateHeartParticles(300); // 批量生成
}
handleReset() {
this.particles = []; // 清空所有粒子
this.generateHeartParticles(this.heartParticleCount); // 重新生成心形
}
六、性能优化要点
- 粒子生命周期管理:及时移除死亡粒子,避免内存泄漏
javascript
for (let i=this.particles.length-1; i>=0; i--) {
if (!this.particles[i].isAlive()) {
this.particles.splice(i, 1); // 逆序删除避免索引错乱
}
}
- 画布清理策略 :使用
clearRect
而非全量重绘
javascript
ctx.clearRect(0, 0, canvas.width, canvas.height); // 只清除可见区域
- 分层渲染优化:将不同类型粒子分组管理,减少状态判断
效果展示
- 基础效果:中心悬浮动态心形,粒子随心跳效果呼吸缩放
- 交互效果 :
- 鼠标移动生成彩色拖尾轨迹
- 点击屏幕触发多层烟花爆炸,伴随真实物理坠落
- 底部控制栏可调整粒子数量、暂停动画、重置场景
总结
通过HTML5 Canvas的高性能渲染能力,结合物理模拟和粒子系统设计,我们实现了一个兼具视觉美感和交互乐趣的心形动画。核心技术点包括:
- 基于参数方程的几何图形生成
- 多类型粒子的状态机设计
- 物理引擎(重力、摩擦力、风力)的实现
- 交互式粒子系统的性能优化
完整代码
心之律动 | 交互式粒子艺术
<div class="overlay">
<h1 class="title animate-pulse-slow">心之律动</h1>
<p class="subtitle">鼠标滑过留下痕迹,点击释放烟花</p>
</div>
<div class="controls">
<div class="control-btn" id="reset-btn" title="重置">
<i class="fa fa-refresh"></i>
</div>
<div class="control-btn" id="pause-btn" title="暂停/继续">
<i class="fa fa-pause"></i>
</div>
<div class="control-btn" id="increase-btn" title="增加粒子">
<i class="fa fa-plus"></i>
</div>
<div class="control-btn" id="decrease-btn" title="减少粒子">
<i class="fa fa-minus"></i>
</div>
</div>
<div class="info-panel">
<div class="particles-count">
<span id="particle-count">粒子数量: 0</span>
</div>
<div class="instructions">
<p><i class="fa fa-mouse-pointer heart-icon"></i> 鼠标移动: 留下粒子轨迹</p>
<p><i class="fa fa-hand-pointer-o heart-icon"></i> 点击: 释放烟花</p>
<p><i class="fa fa-refresh heart-icon"></i> 重置: 重新生成心形</p>
</div>
</div>
<script>
// 初始化画布
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 设置画布尺寸
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 粒子颜色方案
const particleColors = [
'#FF5E87', '#FF85A2', '#FFB3C6', '#FFC2D1', '#FFD7E4',
'#FF9AA2', '#FFB7B2', '#FFDAC1', '#E2F0CB', '#B5EAD7', '#C7CEEA',
'#A79AFF', '#C2A7FF', '#D8A7FF', '#EAA7FF', '#F5A7FF'
];
// 粒子类
class Particle {
constructor(x, y, type = 'heart') {
this.x = x;
this.y = y;
this.type = type; // 'heart', 'mouse', 'firework', 'falling'
// 根据粒子类型设置不同属性
if (type === 'heart') {
this.setupHeartParticle();
} else if (type === 'mouse') {
this.setupMouseParticle();
} else if (type === 'firework') {
this.setupFireworkParticle();
} else if (type === 'falling') {
this.setupFallingParticle();
}
// 为烟花粒子添加延迟效果
if (type === 'firework') {
this.delay = Math.random() * 15; // 延迟发射时间
this.isActive = false;
}
}
setupHeartParticle() {
// 粒子层次(0=外层,1=中层,2=内层,3=中心)
this.layer = Math.floor(Math.random() * 4);
// 根据层次确定颜色
const colorPools = [
particleColors.slice(0, 3), // 外层颜色 - 冷色
particleColors.slice(3, 6), // 中层颜色 - 中色
particleColors.slice(6, 9), // 内层颜色 - 暖色
particleColors.slice(9) // 中心颜色 - 最暖色
];
this.color = colorPools[this.layer][Math.floor(Math.random() * colorPools[this.layer].length)];
// 根据层次确定大小
this.size = 2 + Math.random() * (8 - this.layer * 2);
// 根据层次确定速度
this.speed = 0.1 + Math.random() * 0.3 + this.layer * 0.05;
// 随机方向
this.angle = Math.random() * Math.PI * 2;
// 粒子寿命
this.life = 150 + Math.random() * 100 - this.layer * 30;
this.maxLife = this.life;
}
setupMouseParticle() {
// 鼠标轨迹粒子属性 - 延长寿命
this.color = particleColors[Math.floor(Math.random() * particleColors.length)];
this.size = 1 + Math.random() * 3;
this.speed = 0.05 + Math.random() * 0.1; // 降低速度,延长轨迹
this.angle = Math.random() * Math.PI * 2;
this.life = 100 + Math.random() * 80; // 延长寿命
this.maxLife = this.life;
}
setupFireworkParticle() {
// 烟花粒子属性 - 更大范围
this.color = particleColors[Math.floor(Math.random() * particleColors.length)];
this.size = 1.5 + Math.random() * 4;
this.baseSpeed = 2 + Math.random() * 3; // 更高初始速度,更大范围
this.angle = Math.random() * Math.PI * 2;
this.life = 100 + Math.random() * 80; // 延长烟花粒子寿命
this.maxLife = this.life;
this.gravity = 0.05; // 增加重力效果
this.friction = 0.97 + Math.random() * 0.01; // 添加摩擦力
// 使用笛卡尔坐标系统
this.vx = Math.cos(this.angle) * this.baseSpeed;
this.vy = Math.sin(this.angle) * this.baseSpeed;
}
setupFallingParticle() {
// 坠落粒子属性
this.color = particleColors[Math.floor(Math.random() * particleColors.length)];
this.size = 0.5 + Math.random() * 2;
this.speed = 0.5 + Math.random() * 1.5;
// 确保角度主要向下(π到2π之间)
this.angle = Math.PI + (Math.random() - 0.5) * Math.PI * 0.6;
this.life = 80 + Math.random() * 120;
this.maxLife = this.life;
this.gravity = 0.03; // 增加重力效果
this.wind = (Math.random() - 0.5) * 0.003; // 水平风力,减小偏移
}
update() {
// 烟花粒子延迟激活
if (this.type === 'firework' && !this.isActive) {
this.delay--;
if (this.delay <= 0) {
this.isActive = true;
}
return;
}
// 更新位置 - 使用笛卡尔坐标系统
if (this.type === 'firework' && this.isActive) {
// 应用摩擦力
this.vx *= this.friction;
this.vy *= this.friction;
// 应用重力
this.vy += this.gravity;
this.x += this.vx;
this.y += this.vy;
// 当烟花粒子速度足够慢时,转换为坠落粒子
if (Math.abs(this.vy) > 0.3 && Math.random() < 0.08 && this.life > 40) {
this.type = 'falling';
this.setupFallingParticle();
}
} else {
// 其他粒子使用极坐标系统
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed;
}
if (this.type === 'falling') {
this.speed += this.gravity;
this.x += this.wind;
}
// 更新寿命
this.life--;
// 心跳效果 - 改变粒子大小和不透明度
const heartbeatPhase = (Date.now() / 800) % (Math.PI * 2);
const heartbeatFactor = 1.0 + 0.15 * Math.sin(heartbeatPhase);
// 对于心形粒子,使用更明显的心跳效果
if (this.type === 'heart') {
this.currentSize = this.size * heartbeatFactor * (this.life / this.maxLife);
this.currentAlpha = (this.life / this.maxLife) * (0.8 + 0.2 * Math.sin(heartbeatPhase + this.layer * 0.5));
} else {
this.currentSize = this.size * (this.life / this.maxLife);
this.currentAlpha = this.life / this.maxLife;
}
}
draw() {
// 延迟的烟花粒子不绘制
if (this.type === 'firework' && !this.isActive) {
return;
}
// 绘制粒子
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.currentSize, 0, Math.PI * 2);
ctx.closePath();
ctx.globalAlpha = this.currentAlpha;
ctx.fill();
ctx.globalAlpha = 1;
}
isAlive() {
return this.life > 0;
}
}
// 心形粒子动画类
class HeartAnimation {
constructor() {
this.particles = [];
this.isPaused = false;
this.mouse = { x: 0, y: 0, isDown: false };
this.lastMouseMove = 0;
this.particleCount = 0;
this.heartParticleCount = 1500; // 心形粒子目标数量
this.heartRegenRate = 5; // 每帧重新生成的心形粒子数量
// 绑定事件处理函数
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleReset = this.handleReset.bind(this);
this.handlePause = this.handlePause.bind(this);
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDecrease = this.handleDecrease.bind(this);
// 注册事件监听器
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mousedown', this.handleMouseDown);
window.addEventListener('mouseup', this.handleMouseUp);
document.getElementById('reset-btn').addEventListener('click', this.handleReset);
document.getElementById('pause-btn').addEventListener('click', this.handlePause);
document.getElementById('increase-btn').addEventListener('click', this.handleIncrease);
document.getElementById('decrease-btn').addEventListener('click', this.handleDecrease);
// 生成初始心形粒子
this.generateHeartParticles(this.heartParticleCount);
// 开始动画循环
this.animate();
}
// 判断点是否在心形内部
isInsideHeart(x, y, scale = 1) {
// 归一化坐标
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const nx = (x - centerX) / (canvas.width / 2);
const ny = (y - centerY) / (canvas.height / 2);
// 心形方程: (x² + y² - 1)³ - x²y³ ≤ 0
const heartEq = Math.pow(nx*nx + ny*ny - 1, 3) - nx*nx*ny*ny*ny;
return heartEq <= 0;
}
// 生成心形参数方程的点
generateHeartPoint(t, scale = 1) {
// 心形参数方程
const x = 16 * Math.pow(Math.sin(t), 3);
const y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t);
// 归一化并缩放
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const heartScale = Math.min(canvas.width, canvas.height) * 0.35 * scale;
return {
x: centerX + x * heartScale / 16,
y: centerY - y * heartScale / 16 // 注意y轴是向下的
};
}
// 生成心形粒子
generateHeartParticles(count) {
for (let i = 0; i < count; i++) {
// 随机选择层次
const layer = Math.floor(Math.random() * 4);
// 根据层次确定缩放比例
let scale;
if (layer === 0) scale = 1.0; // 外层
else if (layer === 1) scale = 0.95; // 中层
else if (layer === 2) scale = 0.85; // 内层
else scale = 0.7; // 中心层
// 生成随机角度
const t = Math.random() * Math.PI * 2;
// 计算粒子位置
const point = this.generateHeartPoint(t, scale);
// 对于中心层,添加一些随机偏移,使其填充更均匀
if (layer === 3) {
const offset = Math.random() * 0.2 * (canvas.width / 2);
point.x += (Math.random() - 0.5) * offset;
point.y += (Math.random() - 0.5) * offset;
// 确保点仍然在心形内部
if (!this.isInsideHeart(point.x, point.y)) {
continue;
}
}
// 创建粒子
this.particles.push(new Particle(point.x, point.y, 'heart'));
}
this.updateParticleCount();
}
// 重新生成心形粒子
regenerateHeartParticles(count) {
for (let i = 0; i < count; i++) {
// 随机选择层次
const layer = Math.floor(Math.random() * 4);
// 根据层次确定缩放比例
let scale;
if (layer === 0) scale = 1.0; // 外层
else if (layer === 1) scale = 0.95; // 中层
else if (layer === 2) scale = 0.85; // 内层
else scale = 0.7; // 中心层
// 生成随机角度
const t = Math.random() * Math.PI * 2;
// 计算粒子位置
const point = this.generateHeartPoint(t, scale);
// 对于中心层,添加一些随机偏移,使其填充更均匀
if (layer === 3) {
const offset = Math.random() * 0.2 * (canvas.width / 2);
point.x += (Math.random() - 0.5) * offset;
point.y += (Math.random() - 0.5) * offset;
// 确保点仍然在心形内部
if (!this.isInsideHeart(point.x, point.y)) {
continue;
}
}
// 创建粒子
this.particles.push(new Particle(point.x, point.y, 'heart'));
}
}
// 鼠标移动处理
handleMouseMove(e) {
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
// 限制鼠标轨迹粒子生成频率
const now = Date.now();
if (now - this.lastMouseMove > 15) { // 增加生成频率
// 创建更多鼠标轨迹粒子,形成更连续的轨迹
for (let i = 0; i < 6; i++) {
const offsetX = (Math.random() - 0.5) * 20; // 更大的偏移范围
const offsetY = (Math.random() - 0.5) * 20;
this.particles.push(new Particle(
this.mouse.x + offsetX,
this.mouse.y + offsetY,
'mouse'
));
}
this.lastMouseMove = now;
this.updateParticleCount();
}
}
// 鼠标按下处理
handleMouseDown() {
this.mouse.isDown = true;
this.createFirework(this.mouse.x, this.mouse.y);
}
// 鼠标释放处理
handleMouseUp() {
this.mouse.isDown = false;
}
// 创建烟花效果
createFirework(x, y) {
// 主烟花爆炸 - 分阶段释放粒子
this.createFireworkWave(x, y, 180, 0);
// 外围烟花 - 延迟释放,更大范围
setTimeout(() => {
if (this.isPaused) return;
this.createFireworkWave(x, y, 120, 10, false, 1.5);
}, 150);
// 精细粒子 - 延迟释放,更精细的粒子
setTimeout(() => {
if (this.isPaused) return;
this.createFireworkWave(x, y, 150, 15, true);
}, 250);
this.updateParticleCount();
}
// 创建一波烟花粒子
createFireworkWave(x, y, count, baseDelay, fineParticles = false, speedMultiplier = 1) {
for (let i = 0; i < count; i++) {
const p = new Particle(x, y, 'firework');
// 更精细的粒子
if (fineParticles) {
p.size = 0.5 + Math.random() * 1.5;
p.baseSpeed = 1.5 + Math.random() * 2;
p.life = 80 + Math.random() * 60;
} else {
p.size = 1 + Math.random() * 3;
p.baseSpeed = (2 + Math.random() * 3) * speedMultiplier;
}
p.delay = baseDelay + Math.random() * 10;
p.vx = Math.cos(p.angle) * p.baseSpeed;
p.vy = Math.sin(p.angle) * p.baseSpeed;
this.particles.push(p);
}
}
// 重置动画
handleReset() {
// 清空现有粒子
this.particles = [];
// 生成新的心形粒子
this.generateHeartParticles(this.heartParticleCount);
}
// 暂停/继续动画
handlePause() {
this.isPaused = !this.isPaused;
const pauseBtn = document.getElementById('pause-btn');
pauseBtn.innerHTML = this.isPaused ?
'<i class="fa fa-play"></i>' :
'<i class="fa fa-pause"></i>';
}
// 增加粒子数量
handleIncrease() {
this.heartParticleCount += 300;
this.generateHeartParticles(300);
}
// 减少粒子数量
handleDecrease() {
// 保留最近添加的300个粒子
if (this.heartParticleCount > 300) {
this.heartParticleCount -= 300;
// 移除部分粒子
let removed = 0;
for (let i = this.particles.length - 1; i >= 0; i--) {
if (this.particles[i].type === 'heart') {
this.particles.splice(i, 1);
removed++;
if (removed >= 300) break;
}
}
this.updateParticleCount();
}
}
// 更新粒子数量显示
updateParticleCount() {
this.particleCount = this.particles.length;
document.getElementById('particle-count').textContent = `粒子数量: ${this.particleCount}`;
}
// 动画循环
animate() {
if (!this.isPaused) {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 计算当前心形粒子数量
let heartParticles = 0;
// 更新和绘制所有粒子
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
if (particle.type === 'heart') {
heartParticles++;
}
particle.update();
if (particle.isAlive()) {
particle.draw();
} else {
// 移除死亡的粒子
this.particles.splice(i, 1);
}
}
// 补充心形粒子
if (heartParticles < this.heartParticleCount) {
const toGenerate = Math.min(
this.heartRegenRate,
this.heartParticleCount - heartParticles
);
this.regenerateHeartParticles(toGenerate);
}
// 更新粒子数量显示
if (this.particleCount !== this.particles.length) {
this.updateParticleCount();
}
}
// 继续动画循环
requestAnimationFrame(this.animate.bind(this));
}
}
// 初始化动画
const animation = new HeartAnimation();
</script>