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

用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); // 重新生成心形
}

六、性能优化要点

  1. 粒子生命周期管理:及时移除死亡粒子,避免内存泄漏
javascript 复制代码
for (let i=this.particles.length-1; i>=0; i--) {
    if (!this.particles[i].isAlive()) {
        this.particles.splice(i, 1); // 逆序删除避免索引错乱
    }
}
  1. 画布清理策略 :使用clearRect而非全量重绘
javascript 复制代码
ctx.clearRect(0, 0, canvas.width, canvas.height); // 只清除可见区域
  1. 分层渲染优化:将不同类型粒子分组管理,减少状态判断

效果展示

  • 基础效果:中心悬浮动态心形,粒子随心跳效果呼吸缩放
  • 交互效果
    • 鼠标移动生成彩色拖尾轨迹
    • 点击屏幕触发多层烟花爆炸,伴随真实物理坠落
    • 底部控制栏可调整粒子数量、暂停动画、重置场景

总结

通过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>
相关推荐
打小就很皮...18 分钟前
HBuilder 发行Android(apk包)全流程指南
前端·javascript·微信小程序
集成显卡1 小时前
PlayWright | 初识微软出品的 WEB 应用自动化测试框架
前端·chrome·测试工具·microsoft·自动化·edge浏览器
前端小趴菜052 小时前
React - 组件通信
前端·react.js·前端框架
Amy_cx2 小时前
在表单输入框按回车页面刷新的问题
前端·elementui
dancing9993 小时前
cocos3.X的oops框架oops-plugin-excel-to-json改进兼容多表单导出功能
前端·javascript·typescript·游戏程序
后海 0_o3 小时前
2025前端微服务 - 无界 的实战应用
前端·微服务·架构
Scabbards_3 小时前
CPT304-2425-S2-Software Engineering II
前端
小满zs3 小时前
Zustand 第二章(状态处理)
前端·react.js
程序猿小D3 小时前
第16节 Node.js 文件系统
linux·服务器·前端·node.js·编辑器·vim
萌萌哒草头将军3 小时前
🚀🚀🚀Prisma 发布无 Rust 引擎预览版,安装和使用更轻量;支持任何 ORM 连接引擎;支持自动备份...
前端·javascript·vue.js