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

news2025/6/8 12:21:46

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

在这里插入图片描述


引言

在Web交互设计中,粒子动画因其动态美感和视觉吸引力被广泛应用于节日特效、情感化界面等场景。本文将通过实战案例,详细讲解如何使用HTML5 Canvas和JavaScript实现一个「心之律动」交互式粒子艺术效果,包含心形粒子循环动画、鼠标轨迹粒子、烟花爆炸及坠落效果,并分享关键优化技巧。

技术栈概览

  • HTML5 Canvas:实现高性能粒子渲染
  • JavaScript:粒子系统逻辑控制
  • Tailwind CSS:快速构建UI界面
  • Font Awesome:图标库支持

一、基础框架搭建

1. 画布初始化

<canvas id="canvas"></canvas>
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构建半透明控制栏和信息面板,实现响应式布局:

<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属性区分不同粒子类型(心形/鼠标轨迹/烟花/坠落),实现多态行为:

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. 心形参数方程

使用经典心形参数方程生成粒子初始位置:

// 心形参数方程: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. 粒子循环再生机制

通过每帧检测心形粒子数量,动态补充消失的粒子,实现持续动画:

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实现速度衰减
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. 多阶段爆炸效果

通过延迟释放不同类型粒子,模拟真实烟花层次感:

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. 坠落效果转换

当烟花粒子速度低于阈值时,转换为坠落粒子并添加风力效果:

if (this.type === 'firework' && Math.abs(this.vy) < 0.3) {
    this.type = 'falling';
    this.setupFallingParticle(); // 启用风力和更快下落
}

五、交互功能实现

1. 鼠标轨迹生成

通过高频次生成带随机偏移的粒子,形成连续轨迹:

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. 控制按钮逻辑

实现粒子数量调整、动画暂停和重置功能:

handleIncrease() {
    this.heartParticleCount += 300;
    this.generateHeartParticles(300); // 批量生成
}

handleReset() {
    this.particles = []; // 清空所有粒子
    this.generateHeartParticles(this.heartParticleCount); // 重新生成心形
}

六、性能优化要点

  1. 粒子生命周期管理:及时移除死亡粒子,避免内存泄漏
for (let i=this.particles.length-1; i>=0; i--) {
    if (!this.particles[i].isAlive()) {
        this.particles.splice(i, 1); // 逆序删除避免索引错乱
    }
}
  1. 画布清理策略:使用clearRect而非全量重绘
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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2404122.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【软件工具】批量OCR指定区域图片自动识别内容重命名软件使用教程及注意事项

批量OCR指定区域图片自动识别内容重命名软件使用教程及注意事项 1、操作步骤1-5&#xff1a; 安装与启动&#xff1a;安装成功后&#xff0c;在桌面或开始菜单找到软件图标&#xff0c;双击启动。 导入图片&#xff1a;进入软件主界面&#xff0c;点击 “导入图片” 按钮&a…

数据通信与计算机网络——数字传输

主要内容 数字到数字转换 线路编码 线路编码方案 块编码 扰动 模拟到数字转换 脉冲码调制&#xff08;PCM&#xff09; Delta调制&#xff08;DM&#xff09; 传输模式 并行传输 串行传输 一、数字到数字转换 将数字数据转换为数字信号涉及三种技术&#xff1a; 线…

黄柏基因组-小檗碱生物合成的趋同进化-文献精读142

Convergent evolution of berberine biosynthesis 小檗碱生物合成的趋同进化 摘要 小檗碱是一种有效的抗菌和抗糖尿病生物碱&#xff0c;主要从不同植物谱系中提取&#xff0c;特别是从小檗属&#xff08;毛茛目&#xff0c;早期分支的真双子叶植物&#xff09;和黄柏属&…

前端杂货铺——TodoList

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

Spring Boot SSE流式输出+AI消息持久化升级实践:从粗暴到优雅的跃迁

在 AI 应用落地过程中&#xff0c;我们常常需要将用户和 AI 的对话以“完整上下文”的形式持久化到数据库中。但当 AI 回复非常长&#xff0c;甚至接近上万字时&#xff0c;传统的单条消息保存机制就会出问题。 在本篇文章中&#xff0c;我将深入讲解一次实际项目中对 对话持久…

Model Context Protocol (MCP) 是一个前沿框架

微软发布了 Model Context Protocol (MCP) 课程&#xff1a;mcp-for-beginners。 Model Context Protocol (MCP) 是一个前沿框架&#xff0c;涵盖 C#、Java、JavaScript、TypeScript 和 Python 等主流编程语言&#xff0c;规范 AI 模型与客户端应用之间的交互。 MCP 课程结构 …

内容力重塑品牌增长:开源AI大模型驱动下的智能名片与S2B2C商城赋能抖音生态种草范式

摘要&#xff1a;内容力已成为抖音生态中品牌差异化竞争的核心能力&#xff0c;通过有价值、强共鸣的内容实现产品"种草"与转化闭环。本文基于"开源AI大模型AI智能名片S2B2C商城小程序源码"技术架构&#xff0c;提出"技术赋能内容"的新型种草范式…

手机号在网状态查询接口如何用PHP实现调用?

一、什么是手机号在网状态查询接口 通过精准探测手机号的状态&#xff0c;帮助平台减少此类问题的发生&#xff0c;提供更个性化的服务或进行地域性营销 二、应用场景 1. 金融风控 通过运营商在网态查询接口&#xff0c;金融机构可以核验贷款申请人的手机状态&#xff0c;拦…

【Java微服务组件】分布式协调P4-一文打通Redisson:从API实战到分布式锁核心源码剖析

欢迎来到啾啾的博客&#x1f431;。 记录学习点滴。分享工作思考和实用技巧&#xff0c;偶尔也分享一些杂谈&#x1f4ac;。 有很多很多不足的地方&#xff0c;欢迎评论交流&#xff0c;感谢您的阅读和评论&#x1f604;。 目录 引言Redisson基本信息Redisson网站 Redisson应用…

一个简单的德劳内三角剖分实现

德劳内&#xff08;Delaunay&#xff09;三角剖分是一种经典的将点集进行三角网格化预处理的手段&#xff0c;在NavMesh、随机地牢生成等场景下都有应用。 具体内容百度一大堆&#xff0c;就不介绍了。 比较知名的算法是Bowyer-Watson算法&#xff0c;也就是逐点插入法。 下雨闲…

C#子线程更新主线程UI及委托回调使用示例

1.声明线程方法 2.线程中传入对象 3.声明委托与使用 声明委托对象 委托作为参数传入方法 4.在线程中传入委托 5.调用传入的委托

使用VuePress2.X构建个人知识博客,并且用个人域名部署到GitHub Pages中

使用VuePress2.X构建个人知识博客&#xff0c;并且用个人域名部署到GitHub Pages中 什么是VuePress VuePress 是一个以 Markdown 为中心的静态网站生成器。你可以使用 Markdown 来书写内容&#xff08;如文档、博客等&#xff09;&#xff0c;然后 VuePress 会帮助你生成一个…

手写Promise.all

前言 之前在看远方os大佬直播的时候看到有让手写的Promise.all的问题&#xff0c;然后心血来潮自己准备手写一个 开始 首先&#xff0c;我们需要明确原本js提供的Promise.all的特性 Promise.all返回的是一个Promise如果传入的数据中有一个reject即整个all返回的就是reject&…

2025年6月|注意力机制|面向精度与推理速度提升的YOLOv8模型结构优化研究:融合ACmix的自研改进方案

版本&#xff1a; 8.3.143(Ultralytics YOLOv8框架) ACmix模块原理 在目标检测任务中&#xff0c;小目标&#xff08;如裂缝、瑕疵、零件边缘等&#xff09;由于其尺寸较小、纹理信息稀疏&#xff0c;通常更容易受到图像中复杂背景或噪声的干扰&#xff0c;从而导致漏检或误检…

利用qcustomplot绘制曲线图

本文详细介绍了qcustomplot绘制曲线图的流程&#xff0c;一段代码一段代码运行看效果。通过阅读本文&#xff0c;读者可以了解到每一项怎么用代码进行配置&#xff0c;进而实现自己想要的图表效果。&#xff08;本文只针对曲线图&#xff09; 1 最简单的图形&#xff08;入门&…

【基础算法】枚举(普通枚举、二进制枚举)

文章目录 一、普通枚举1. 铺地毯(1) 解题思路(2) 代码实现 2. 回文日期(1) 解题思路思路一&#xff1a;暴力枚举思路二&#xff1a;枚举年份思路三&#xff1a;枚举月日 (2) 代码实现 3. 扫雷(2) 解题思路(2) 代码实现 二、二进制枚举1. 子集(1) 解题思路(2) 代码实现 2. 费解的…

智能对联网页小程序的仓颉之旅

#传统楹联遇上AI智能体&#xff1a;我的Cangjie Magic开发纪实 引言&#xff1a;一场跨越千年的数字对话 "云对雨&#xff0c;雪对风&#xff0c;晚照对晴空"。昨天晚上星空璀璨&#xff0c;当我用仓颉语言写下第一个智能对联网页小程序的Agent DSL代码时&#xff0…

Python分形几何可视化—— 复数迭代、L系统与生物分形模拟

Python分形几何可视化—— 复数迭代、L系统与生物分形模拟 本节将深入探索分形几何的奇妙世界&#xff0c;实现Mandelbrot集生成器和L系统分形树工具&#xff0c;并通过肺部血管分形案例展示分形在医学领域的应用。我们将使用Python的NumPy进行高效计算&#xff0c;结合Matplo…

【超详细】英伟达Jetson Orin NX-YOLOv8配置与TensorRT测试

文章主要内容如下&#xff1a; 1、基础运行环境配置 2、Torch-GPU安装 3、ultralytics环境配置 4、Onnx及TensorRT导出详解 5、YOLOv8推理耗时分析 基础库版本&#xff1a;jetpack5.1.3, torch-gpu2.1.0, torchvision0.16.0, ultralytics8.3.146 设备的软件开发包基础信息 需…

Go语言学习-->项目中引用第三方库方式

Go语言学习–&#xff1e;项目中引用第三方库方式 1 执行 go mod tidy 分析引入的依赖有没有正常放在go.mod里面 找到依赖的包会自动下载到本地 并添加在go.mod里面 执行结果&#xff1a; 2 执行go get XXXX&#xff08;库的名字&#xff09;