1. 项目概述:用代码绘制浪漫星空
去年情人节,我帮朋友用200行JavaScript代码实现了一个动态星空告白页面,当对方打开链接时,无数粒子逐渐汇聚成心形,最终显示出表白文字。这个简单却充满创意的效果,让原本紧张的告白现场瞬间变得浪漫而难忘。
动态粒子效果本质上是通过Canvas或WebGL在网页上绘制大量微小元素(粒子),并通过算法控制它们的运动轨迹。这种技术广泛应用于数据可视化、游戏特效和创意交互场景。相比传统的前端元素,粒子系统具有以下独特优势:
- 可自由控制数千个元素的独立运动
- 能实现流体、星空、烟雾等自然现象模拟
- 性能优化后即使低配设备也能流畅运行
- 完全通过代码定义视觉表现,修改灵活
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心原理与技术选型
2.1 Canvas vs WebGL的选择
在实现粒子系统时,我们主要面临两种技术选择:
javascript复制// Canvas 2D基础绘制示例
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
// WebGL绘制需要更复杂的初始化
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
对于告白场景这类轻量级应用,Canvas 2D完全够用且具有以下优势:
- 学习曲线平缓,API直观易懂
- 兼容性更好(支持到IE9)
- 调试方便,适合快速原型开发
- 粒子数量在5000以下时性能足够
而WebGL更适合:
- 需要3D效果的场景
- 粒子数量超过1万的复杂系统
- 需要着色器自定义渲染的场景
2.2 粒子系统基础架构
一个典型的粒子系统包含以下核心模块:
javascript复制class ParticleSystem {
constructor() {
this.particles = [];
this.maxParticles = 2000; // 根据设备性能调整
}
// 每帧更新粒子状态
update() {
this.particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.life--;
});
this.particles = this.particles.filter(p => p.life > 0);
}
// 渲染所有粒子
render(ctx) {
ctx.save();
this.particles.forEach(p => {
ctx.globalAlpha = p.life / 100;
drawParticle(ctx, p);
});
ctx.restore();
}
}
3. 完整实现步骤
3.1 基础环境搭建
创建标准的HTML5文档结构:
html复制<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>星空告白</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="app.js"></script>
</body>
</html>
在app.js中初始化Canvas:
javascript复制const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 适配不同屏幕尺寸
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
3.2 粒子类实现
定义粒子基础属性:
javascript复制class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 3 + 1;
this.vx = Math.random() * 2 - 1;
this.vy = Math.random() * 2 - 1;
this.color = `hsl(${Math.random() * 60 + 200}, 100%, 50%)`;
this.life = 100;
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
3.3 动画循环与性能优化
使用requestAnimationFrame实现流畅动画:
javascript复制let particles = [];
const PARTICLE_COUNT = 1500;
function animate() {
// 半透明背景实现拖尾效果
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 更新并绘制所有粒子
particles.forEach(p => {
p.update();
p.draw(ctx);
});
// 移除生命周期结束的粒子
particles = particles.filter(p => p.life > 0);
// 补充新粒子
if (particles.length < PARTICLE_COUNT) {
particles.push(new Particle(
canvas.width / 2 + Math.random() * 100 - 50,
canvas.height / 2 + Math.random() * 100 - 50
));
}
requestAnimationFrame(animate);
}
animate();
4. 高级效果实现
4.1 粒子路径控制
要实现粒子组成特定形状(如心形),需要添加吸引力场:
javascript复制class HeartAttractor {
constructor(x, y) {
this.x = x;
this.y = y;
this.strength = 0.1;
}
affect(particle) {
const dx = this.x - particle.x;
const dy = this.y - particle.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// 心形曲线方程
const angle = Math.atan2(dy, dx);
const heartDist = 50 * (
Math.sin(angle) * Math.sqrt(Math.abs(Math.cos(angle))) /
(Math.sin(angle) + 1.4) - 2 * Math.sin(angle) + 2
);
if (dist < heartDist) {
particle.vx += dx * this.strength;
particle.vy += dy * this.strength;
}
}
}
4.2 文字显示效果
通过粒子重组显示文字需要三个步骤:
- 将目标文字转换为点阵数据
- 计算每个粒子到目标位置的最短路径
- 添加过渡动画效果
javascript复制function textToParticles(text, fontSize = 80) {
// 创建隐藏Canvas获取文字像素数据
const textCanvas = document.createElement('canvas');
const textCtx = textCanvas.getContext('2d');
textCanvas.width = canvas.width;
textCanvas.height = canvas.height;
textCtx.font = `${fontSize}px Arial`;
textCtx.fillStyle = 'white';
textCtx.textAlign = 'center';
textCtx.fillText(text, canvas.width/2, canvas.height/2);
const pixels = textCtx.getImageData(0, 0, canvas.width, canvas.height).data;
const positions = [];
// 采样文字像素
for (let y = 0; y < canvas.height; y += 4) {
for (let x = 0; x < canvas.width; x += 4) {
const i = (y * canvas.width + x) * 4;
if (pixels[i] > 0) {
positions.push({ x, y });
}
}
}
return positions;
}
5. 性能优化技巧
5.1 离屏Canvas缓存
对于静态背景或重复使用的元素:
javascript复制const offscreen = document.createElement('canvas');
offscreen.width = 200;
offscreen.height = 200;
const offCtx = offscreen.getContext('2d');
// 绘制到离屏Canvas
drawComplexPattern(offCtx);
// 主Canvas中重复使用
ctx.drawImage(offscreen, 0, 0);
5.2 粒子池技术
避免频繁创建销毁对象:
javascript复制const particlePool = Array(1000).fill().map(() => new Particle());
function getParticle() {
const p = particlePool.find(p => !p.active);
if (p) {
p.reset();
return p;
}
return new Particle();
}
5.3 分层渲染策略
将不同更新频率的元素分开:
html复制<canvas id="bg" class="canvas-layer"></canvas>
<canvas id="particles" class="canvas-layer"></canvas>
<canvas id="ui" class="canvas-layer"></canvas>
<style>
.canvas-layer {
position: absolute;
top: 0;
left: 0;
}
</style>
6. 常见问题解决
6.1 粒子闪烁问题
原因:全局透明度叠加导致。解决方案:
- 使用
ctx.globalCompositeOperation = 'lighter'实现加色混合 - 或在每帧绘制前用半透明矩形清屏(如前文代码所示)
6.2 移动端性能优化
javascript复制// 检测移动设备减少粒子数量
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
const PARTICLE_COUNT = isMobile ? 800 : 1500;
// 减少重绘区域
function dirtyRectRender() {
if (!this.needsRedraw) return;
ctx.clearRect(
this.lastX - 10,
this.lastY - 10,
this.width + 20,
this.height + 20
);
// ...绘制逻辑
}
6.3 跨浏览器兼容性
确保添加必要的polyfill:
javascript复制// requestAnimationFrame polyfill
(function() {
const vendors = ['ms', 'moz', 'webkit', 'o'];
for (let i = 0; i < vendors.length && !window.requestAnimationFrame; i++) {
window.requestAnimationFrame = window[`${vendors[i]}RequestAnimationFrame`];
window.cancelAnimationFrame = window[`${vendors[i]}CancelAnimationFrame`]
|| window[`${vendors[i]}CancelRequestAnimationFrame`];
}
})();
7. 完整案例代码
以下是一个可直接使用的星空告白完整实现:
html复制<!DOCTYPE html>
<html>
<head>
<title>星空告白</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; }
.message {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 2em;
text-align: center;
opacity: 0;
transition: opacity 2s;
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="message" id="message"></div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const messageEl = document.getElementById('message');
// 初始化设置
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
const particles = [];
const PARTICLE_COUNT = 1500;
let phase = 0; // 0:自由运动 1:聚集 2:显示文字
// 粒子类
class Particle {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.targetX = this.x;
this.targetY = this.y;
this.size = Math.random() * 2 + 1;
this.color = `hsl(${Math.random() * 60 + 200}, 100%, ${Math.random() * 30 + 50}%)`;
this.speed = Math.random() * 0.2 + 0.1;
}
update() {
const dx = this.targetX - this.x;
const dy = this.targetY - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 1 && phase === 1) {
phase = 2;
showMessage();
}
this.x += dx * this.speed;
this.y += dy * this.speed;
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
// 显示告白文字
function showMessage() {
messageEl.textContent = '我爱你';
messageEl.style.opacity = 1;
// 为文字添加发光效果
messageEl.style.textShadow = '0 0 10px #fff, 0 0 20px #fff, 0 0 30px #e60073';
}
// 初始化粒子
function initParticles() {
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push(new Particle());
}
}
// 设置心形目标位置
function setHeartTarget() {
const centerX = width / 2;
const centerY = height / 2;
const size = Math.min(width, height) * 0.3;
particles.forEach(p => {
const angle = Math.random() * Math.PI * 2;
// 心形曲线方程
p.targetX = centerX + size * (
16 * Math.pow(Math.sin(angle), 3)
) / 20;
p.targetY = centerY - size * (
13 * Math.cos(angle) -
5 * Math.cos(2*angle) -
2 * Math.cos(3*angle) -
Math.cos(4*angle)
) / 20;
});
phase = 1;
}
// 动画循环
function animate() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, width, height);
particles.forEach(p => {
p.update();
p.draw();
});
requestAnimationFrame(animate);
}
// 窗口大小调整
window.addEventListener('resize', () => {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
});
// 点击开始聚集
window.addEventListener('click', setHeartTarget);
// 启动
initParticles();
animate();
</script>
</body>
</html>
这个实现包含以下特色功能:
- 初始随机粒子分布模拟星空
- 点击后粒子向心形图案聚集
- 聚集完成后显示发光文字
- 自适应不同屏幕尺寸
- 性能优化的绘制逻辑
你可以直接保存为HTML文件在浏览器中打开,或者部署到任意静态网站托管服务。要自定义告白文字,只需修改showMessage()函数中的文本内容。
