免费资源下载
一、Canvas粒子系统的应用价值
在现代Web开发中,Canvas元素为开发者提供了强大的图形绘制能力。粒子系统作为计算机图形学中的重要概念,广泛应用于游戏特效、数据可视化、交互艺术等领域。与传统的CSS动画相比,Canvas粒子系统具有以下优势:
- 高性能:可同时渲染数千个粒子而保持流畅
- 物理真实感:可模拟重力、碰撞、风力等物理效果
- 高度可定制:每个粒子可独立控制属性
- 交互性强:支持鼠标、触摸等交互响应
- 跨平台:在所有现代浏览器中表现一致
本文将带领读者从零开始构建一个完整的物理引擎驱动的粒子系统,涵盖从基础绘制到高级优化的全过程。
二、系统架构设计
2.1 核心类设计
// 系统架构概览
ParticleSystem (粒子系统管理器)
├── Particle (粒子基类)
│ ├── CircleParticle (圆形粒子)
│ ├── RectParticle (矩形粒子)
│ └── ImageParticle (图像粒子)
├── PhysicsEngine (物理引擎)
│ ├── Gravity (重力模拟)
│ ├── Collision (碰撞检测)
│ └── Wind (风力模拟)
├── Renderer (渲染器)
│ ├── CanvasRenderer (Canvas渲染)
│ └── WebGLRenderer (WebGL渲染)
└── Interaction (交互控制器)
├── MouseInteraction (鼠标交互)
└── TouchInteraction (触摸交互)
2.2 物理模型设计
我们将实现基于Verlet积分法的物理模拟,相比欧拉法具有更好的数值稳定性:
// Verlet积分公式
position_new = 2 * position_current - position_old + acceleration * dt²
// 其中:
// position_new: 新位置
// position_current: 当前位置
// position_old: 上一帧位置
// acceleration: 加速度(重力、风力等)
// dt: 时间步长
三、完整代码实现
3.1 HTML结构
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Canvas粒子系统演示</title>
</head>
<body>
<div class="container">
<canvas id="particleCanvas" width="1200" height="800">
您的浏览器不支持Canvas,请升级到现代浏览器
</canvas>
<div class="controls">
<button id="addParticles">添加粒子</button>
<button id="clearParticles">清空粒子</button>
<input type="range" id="gravitySlider" min="0" max="20" value="9.8">
<label for="gravitySlider">重力强度</label>
</div>
<div class="stats">
<span id="particleCount">0</span> 个粒子 |
<span id="fps">60</span> FPS
</div>
</div>
<script src="particle-system.js"></script>
</body>
</html>
3.2 粒子基类实现
// particle-system.js
class Vector2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
add(v) {
return new Vector2(this.x + v.x, this.y + v.y);
}
subtract(v) {
return new Vector2(this.x - v.x, this.y - v.y);
}
multiply(scalar) {
return new Vector2(this.x * scalar, this.y * scalar);
}
distanceTo(v) {
const dx = this.x - v.x;
const dy = this.y - v.y;
return Math.sqrt(dx * dx + dy * dy);
}
length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
normalize() {
const len = this.length();
if (len > 0) {
return new Vector2(this.x / len, this.y / len);
}
return new Vector2(0, 0);
}
}
class Particle {
constructor(x, y, options = {}) {
this.position = new Vector2(x, y);
this.oldPosition = new Vector2(x, y);
this.velocity = new Vector2(0, 0);
this.acceleration = new Vector2(0, 0);
// 粒子属性
this.radius = options.radius || 5;
this.mass = options.mass || 1;
this.color = options.color || '#3498db';
this.life = options.life || Infinity;
this.age = 0;
this.friction = options.friction || 0.98;
this.bounce = options.bounce || 0.8;
// 物理约束
this.fixed = options.fixed || false;
this.constraints = [];
// 渲染状态
this.opacity = 1.0;
this.scale = 1.0;
}
update(deltaTime) {
if (this.fixed) return;
// Verlet积分计算新位置
const velocity = this.position.subtract(this.oldPosition);
this.oldPosition = new Vector2(this.position.x, this.position.y);
// 应用加速度
velocity.x += this.acceleration.x * deltaTime;
velocity.y += this.acceleration.y * deltaTime;
// 应用摩擦力
velocity.x *= this.friction;
velocity.y *= this.friction;
// 更新位置
this.position = this.position.add(velocity);
// 更新生命周期
this.age += deltaTime;
if (this.age >= this.life) {
this.opacity = 1 - (this.age - this.life) / 1000;
}
}
applyForce(force) {
if (this.fixed) return;
const acceleration = force.multiply(1 / this.mass);
this.acceleration = this.acceleration.add(acceleration);
}
constrainToBounds(width, height) {
if (this.position.x width - this.radius) {
this.position.x = width - this.radius;
this.oldPosition.x = this.position.x + (this.position.x - this.oldPosition.x) * this.bounce;
}
if (this.position.y height - this.radius) {
this.position.y = height - this.radius;
this.oldPosition.y = this.position.y + (this.position.y - this.oldPosition.y) * this.bounce;
}
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = this.opacity;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, this.radius * this.scale, 0, Math.PI * 2);
ctx.fill();
// 绘制速度向量(调试用)
if (window.DEBUG_MODE) {
ctx.strokeStyle = '#e74c3c';
ctx.beginPath();
ctx.moveTo(this.position.x, this.position.y);
ctx.lineTo(
this.position.x + this.velocity.x * 10,
this.position.y + this.velocity.y * 10
);
ctx.stroke();
}
ctx.restore();
}
}
3.3 物理引擎实现
class PhysicsEngine {
constructor() {
this.gravity = new Vector2(0, 9.8);
this.wind = new Vector2(0, 0);
this.forces = [];
this.collisionsEnabled = true;
this.spatialGrid = null;
}
setGravity(x, y) {
this.gravity = new Vector2(x, y);
}
setWind(x, y) {
this.wind = new Vector2(x, y);
}
addForce(force) {
this.forces.push(force);
}
update(particles, deltaTime, bounds) {
// 应用全局力
particles.forEach(particle => {
if (particle.fixed) return;
// 重置加速度
particle.acceleration = new Vector2(0, 0);
// 应用重力
particle.applyForce(this.gravity.multiply(particle.mass));
// 应用风力
particle.applyForce(this.wind);
// 应用其他力
this.forces.forEach(force => {
particle.applyForce(force);
});
});
// 更新粒子位置
particles.forEach(particle => {
particle.update(deltaTime);
});
// 边界约束
particles.forEach(particle => {
particle.constrainToBounds(bounds.width, bounds.height);
});
// 碰撞检测(使用空间分割优化)
if (this.collisionsEnabled) {
this.resolveCollisions(particles);
}
}
resolveCollisions(particles) {
// 构建空间网格
const gridSize = 50;
const grid = new Map();
// 将粒子分配到网格
particles.forEach((particle, index) => {
const gridX = Math.floor(particle.position.x / gridSize);
const gridY = Math.floor(particle.position.y / gridSize);
const key = `${gridX},${gridY}`;
if (!grid.has(key)) {
grid.set(key, []);
}
grid.get(key).push(index);
});
// 检查相邻网格中的碰撞
for (const [key, indices] of grid) {
const [gridX, gridY] = key.split(',').map(Number);
// 检查当前网格和相邻8个网格
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy = j) continue; // 避免重复检查
const p1 = particles[i];
const p2 = particles[j];
const distance = p1.position.distanceTo(p2.position);
const minDistance = p1.radius + p2.radius;
if (distance 0) {
// 碰撞响应
const normal = p2.position.subtract(p1.position).normalize();
const overlap = minDistance - distance;
// 分离粒子
const separation = normal.multiply(overlap * 0.5);
if (!p1.fixed) p1.position = p1.position.subtract(separation);
if (!p2.fixed) p2.position = p2.position.add(separation);
// 速度交换(简化版碰撞响应)
if (!p1.fixed && !p2.fixed) {
const relativeVelocity = p2.velocity.subtract(p1.velocity);
const velocityAlongNormal = relativeVelocity.x * normal.x + relativeVelocity.y * normal.y;
if (velocityAlongNormal > 0) continue;
const restitution = Math.min(p1.bounce, p2.bounce);
const impulseScalar = -(1 + restitution) * velocityAlongNormal;
const impulse = normal.multiply(impulseScalar);
p1.velocity = p1.velocity.subtract(impulse.multiply(1 / p1.mass));
p2.velocity = p2.velocity.add(impulse.multiply(1 / p2.mass));
}
}
}
}
}
}
3.4 粒子系统管理器
class ParticleSystem {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.particles = [];
this.physics = new PhysicsEngine();
this.isRunning = false;
this.lastTime = 0;
this.fps = 60;
// 性能监控
this.frameCount = 0;
this.lastFpsUpdate = 0;
// 交互状态
this.mousePosition = new Vector2(0, 0);
this.isMouseDown = false;
this.init();
}
init() {
// 设置Canvas尺寸
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
// 设置交互事件
this.setupInteractions();
// 初始粒子
this.createInitialParticles();
}
resizeCanvas() {
const container = this.canvas.parentElement;
this.canvas.width = container.clientWidth;
this.canvas.height = container.clientHeight;
}
setupInteractions() {
// 鼠标移动
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
this.mousePosition = new Vector2(
e.clientX - rect.left,
e.clientY - rect.top
);
});
// 鼠标点击添加粒子
this.canvas.addEventListener('click', (e) => {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.createParticleExplosion(x, y, 10);
});
// 鼠标拖拽
this.canvas.addEventListener('mousedown', () => {
this.isMouseDown = true;
});
this.canvas.addEventListener('mouseup', () => {
this.isMouseDown = false;
});
// 触摸支持
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = this.canvas.getBoundingClientRect();
this.mousePosition = new Vector2(
touch.clientX - rect.left,
touch.clientY - rect.top
);
}, { passive: false });
}
createInitialParticles() {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
// 创建圆形排列的粒子
const count = 100;
const radius = 150;
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
const particle = new Particle(x, y, {
radius: 3 + Math.random() * 4,
color: this.getRandomColor(),
mass: 0.5 + Math.random() * 1.5,
bounce: 0.7 + Math.random() * 0.3
});
// 给粒子一个切向速度
const tangent = new Vector2(-Math.sin(angle), Math.cos(angle));
particle.velocity = tangent.multiply(2 + Math.random() * 3);
this.particles.push(particle);
}
}
createParticleExplosion(x, y, count) {
for (let i = 0; i {
return particle.age 0;
});
// 应用鼠标交互力
if (this.isMouseDown) {
this.applyMouseForce();
}
// 更新物理
this.physics.update(this.particles, deltaTime, {
width: this.canvas.width,
height: this.canvas.height
});
// 更新FPS计数
this.updateFPS(deltaTime);
}
applyMouseForce() {
const forceRadius = 100;
const forceStrength = 50;
this.particles.forEach(particle => {
const distance = particle.position.distanceTo(this.mousePosition);
if (distance 0) {
const direction = particle.position.subtract(this.mousePosition).normalize();
const force = direction.multiply(forceStrength * (1 - distance / forceRadius));
particle.applyForce(force);
}
});
}
updateFPS(deltaTime) {
this.frameCount++;
this.lastFpsUpdate += deltaTime;
if (this.lastFpsUpdate >= 1000) {
this.fps = Math.round((this.frameCount * 1000) / this.lastFpsUpdate);
this.frameCount = 0;
this.lastFpsUpdate = 0;
// 更新显示
const fpsElement = document.getElementById('fps');
if (fpsElement) {
fpsElement.textContent = this.fps;
}
const countElement = document.getElementById('particleCount');
if (countElement) {
countElement.textContent = this.particles.length;
}
}
}
render() {
// 清空画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制背景
this.drawBackground();
// 绘制所有粒子
this.particles.forEach(particle => {
particle.draw(this.ctx);
});
// 绘制鼠标力场(调试)
if (this.isMouseDown && window.DEBUG_MODE) {
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
this.ctx.beginPath();
this.ctx.arc(this.mousePosition.x, this.mousePosition.y, 100, 0, Math.PI * 2);
this.ctx.stroke();
}
}
drawBackground() {
// 渐变背景
const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
gradient.addColorStop(0, '#1a1a2e');
gradient.addColorStop(1, '#16213e');
this.ctx.fillStyle = gradient;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 网格线
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
this.ctx.lineWidth = 1;
const gridSize = 50;
for (let x = 0; x < this.canvas.width; x += gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, this.canvas.height);
this.ctx.stroke();
}
for (let y = 0; y {
if (!this.isRunning) return;
// 计算时间差
const deltaTime = Math.min(currentTime - this.lastTime, 100) / 1000;
this.lastTime = currentTime;
// 更新和渲染
this.update(deltaTime);
this.render();
// 继续动画循环
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
stop() {
this.isRunning = false;
}
addParticle(particle) {
this.particles.push(particle);
}
clearParticles() {
this.particles = [];
}
}
// 初始化系统
document.addEventListener('DOMContentLoaded', () => {
const particleSystem = new ParticleSystem('particleCanvas');
particleSystem.start();
// 控制按钮
document.getElementById('addParticles').addEventListener('click', () => {
const x = Math.random() * particleSystem.canvas.width;
const y = Math.random() * particleSystem.canvas.height;
particleSystem.createParticleExplosion(x, y, 50);
});
document.getElementById('clearParticles').addEventListener('click', () => {
particleSystem.clearParticles();
});
document.getElementById('gravitySlider').addEventListener('input', (e) => {
const gravityValue = parseFloat(e.target.value);
particleSystem.physics.setGravity(0, gravityValue);
});
// 调试模式切换
window.DEBUG_MODE = false;
document.addEventListener('keydown', (e) => {
if (e.key === 'd' || e.key === 'D') {
window.DEBUG_MODE = !window.DEBUG_MODE;
console.log('调试模式:', window.DEBUG_MODE ? '开启' : '关闭');
}
});
});
四、性能优化技巧
4.1 渲染优化
- 使用离屏Canvas进行预渲染
- 批量绘制调用(particle batching)
- 减少Canvas状态切换
- 使用requestAnimationFrame进行节流
4.2 物理计算优化
- 空间分割算法(四叉树/网格)
- 距离平方比较避免开方运算
- 使用Web Workers进行后台计算
- 实现LOD(细节层次)系统
// 四叉树实现示例
class Quadtree {
constructor(bounds, capacity = 4) {
this.bounds = bounds; // {x, y, width, height}
this.capacity = capacity;
this.particles = [];
this.divided = false;
this.northeast = null;
this.northwest = null;
this.southeast = null;
this.southwest = null;
}
insert(particle) {
if (!this.contains(particle)) {
return false;
}
if (this.particles.length = this.bounds.x &&
particle.position.x = this.bounds.y &&
particle.position.y <= this.bounds.y + this.bounds.height;
}
query(range, found = []) {
if (!this.intersects(range)) {
return found;
}
for (const particle of this.particles) {
if (this.pointInRange(particle.position, range)) {
found.push(particle);
}
}
if (this.divided) {
this.northeast.query(range, found);
this.northwest.query(range, found);
this.southeast.query(range, found);
this.southwest.query(range, found);
}
return found;
}
}
五、实际应用场景
5.1 数据可视化
将数据点映射为粒子,通过粒子的运动展现数据关系和趋势。
5.2 游戏特效
实现爆炸、火焰、烟雾、魔法等粒子特效。
5.3 交互艺术
创建响应鼠标、触摸或声音的视觉艺术装置。
5.4 背景动画
为网站创建动态、交互式的背景效果。
六、总结与扩展
本文详细介绍了如何使用HTML Canvas构建一个完整的物理引擎驱动的粒子系统。我们实现了:
- 基于Verlet积分的物理模拟
- 高效的碰撞检测系统
- 完整的粒子生命周期管理
- 丰富的交互功能
- 性能监控和优化策略
读者可以在此基础上进行以下扩展:
- 添加WebGL渲染后端以获得更好性能
- 实现粒子纹理和精灵动画
- 添加流体动力学模拟
- 集成Three.js进行3D粒子渲染
- 创建粒子编辑器工具
Canvas粒子系统是前端图形编程的重要领域,掌握这项技术能够为你的Web应用增添独特的视觉表现力和交互体验。希望本文能够为你打开Canvas高级应用的大门。

