HTML5 Canvas烟花效果代码实现

924 阅读5分钟

使用 Canvas 模拟烟花效果

1. 介绍

在 Web 开发中,Canvas 提供了一种强大的方式来进行 2D 绘图。本文将介绍如何使用 HTML5 Canvas 实现高密度粒子烟花效果,包括其原理、代码规划及关键部分的详细解释。

2. 实现原理

烟花的模拟主要由以下几个部分组成:

  1. 烟花上升阶段:烟花从底部随机位置出发,以一定角度向目标高度上升。
  2. 烟花爆炸阶段:到达目标高度后,烟花爆炸并生成多个粒子向四周扩散。
  3. 粒子运动和衰减:粒子受空气阻力和重力影响,逐渐减速、下落并最终消失。

物理效果的主要实现方式:

  • 角度计算:使用 Math.atan2 计算烟花的上升角度,使其沿着正确轨迹前进。
  • 爆炸扩散:粒子具有随机的扩散方向、初始速度,并受空气阻力和重力影响。
  • 颜色变化:使用 HSLA 颜色模式,使粒子在爆炸过程中呈现色彩渐变效果。
  • 拖尾效果:使用半透明黑色背景覆盖画布,实现视觉上的动态残影。

3. 代码规划

整个程序的核心由以下部分组成:

  1. Firework

    • 负责管理烟花的上升过程,包括位置计算、目标高度检测。
    • 在到达目标高度后触发爆炸,生成多个粒子。
  2. Particle

    • 负责管理粒子的运动,包括扩散方向、速度衰减、重力影响。
    • 控制粒子的颜色变化和透明度衰减,实现视觉效果。
  3. 主循环 animate()

    • 负责更新和绘制所有烟花。
    • 控制新烟花的生成频率,确保画面流畅。
    • 移除已经消失的烟花,提高性能。

4. 重点代码解析

4.1 烟花上升逻辑

this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed;
  • 计算烟花的上升方向,使其沿着正确的轨迹前进。
  • Math.cosMath.sin 结合角度计算,确保烟花向目标点移动。

4.2 烟花爆炸

for (let i = 0; i < particleCount; i++) {
    this.particles.push(new Particle(this.x, this.y, this.hue));
}
  • 生成 particleCount 个粒子,每个粒子的初始位置与烟花爆炸点相同。
  • 粒子的 hue 颜色基于烟花的颜色,但在爆炸过程中会有一定范围的变化。

4.3 粒子运动物理模拟

this.vx *= this.resistance;
this.vy *= this.resistance;
this.vy += this.gravity;
  • resistance 代表空气阻力,逐渐减小粒子的速度。
  • gravity 模拟重力,使粒子向下运动。
  • 这些参数配合可以让粒子表现出自然的爆炸扩散和下落效果。

4.4 颜色动态变化

get currentColor() {
    const currentHue = (this.baseHue + this.hueShift * (1 - this.alpha)) % 360;
    return `hsla(${currentHue}, 100%, 50%, ${this.alpha})`;
}
  • hueShift 让粒子在爆炸过程中呈现丰富的色彩变化。
  • 透明度 alpha 逐渐减少,使粒子渐隐。
  • HSLA 颜色模式允许粒子具有动态透明度。

5. 运行效果

代码运行后,屏幕上会不断生成烟花,每个烟花爆炸后产生高密度的粒子,粒子颜色变化且逐渐消失。

视觉特效:

  • 拖尾效果:使用 rgba(0, 0, 0, 0.08) 作为背景覆盖,实现动态残影。
  • 动态色彩:粒子的 hue 值在爆炸过程中不断变化。
  • 自然物理效果:粒子扩散、减速、受重力影响逐渐下落。

6. 可能的优化点

  • 增加交互性

    • 点击屏幕可以触发额外的烟花爆炸。
    • 根据鼠标位置影响烟花的生成。
  • 性能优化

    • 限制同时存在的烟花数量,避免过多粒子影响性能。
    • 使用 requestAnimationFrame 进行高效动画渲染。
  • 添加音效

    • 可以结合 Audio API,在爆炸时播放烟花声音,提高沉浸感。

7. 结论

本文介绍了如何使用 HTML5 Canvas 实现高密度粒子烟花效果,并分析了核心逻辑和关键代码。该效果可以用于网页背景动画或节日特效,进一步优化可以加入交互、音效等功能,使其更加生动。

image.png

<!DOCTYPE html>
<html>

<head>
    <title>高密度粒子烟花</title>
    <style>
        body {
            margin: 0;
            background: #000;
            overflow: hidden;
        }

        canvas {
            display: block;
        }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>

    <script>
        // 获取Canvas上下文
        const canvas = document.getElementById('canvas')
        const ctx = canvas.getContext('2d')

        // 设置Canvas尺寸为窗口大小
        canvas.width = window.innerWidth
        canvas.height = window.innerHeight

        /**
         * 烟花类:管理单个烟花的发射和爆炸
         */
        class Firework {
            constructor() {
                this.reset()       // 初始化烟花参数
                this.particles = [] // 爆炸粒子数组
                this.phase = 'launching' // 状态:发射中/爆炸中/结束
            }

            // 重置烟花参数(用于创建新烟花)
            reset() {
                this.x = Math.random() * canvas.width // 起始X坐标(底部随机位置)
                this.y = canvas.height                // 起始Y坐标(屏幕底部)
                this.targetY = Math.random() * canvas.height * 0.4 // 目标高度(屏幕上半部)
                this.speed = 8 + Math.random() * 4     // 上升速度(8-12)
                this.angle = Math.atan2(                // 发射角度(指向屏幕中心)
                    this.targetY - this.y,
                    canvas.width / 2 - this.x
                )
                this.hue = Math.random() * 360         // 基础色相(0-360)
                this.phase = 'launching'              // 初始状态为发射中
            }

            // 更新烟花状态(每帧调用)
            update() {
                if (this.phase === 'launching') {
                    // 计算上升轨迹
                    this.x += Math.cos(this.angle) * this.speed
                    this.y += Math.sin(this.angle) * this.speed

                    // 到达目标高度时触发爆炸
                    if (this.y <= this.targetY) {
                        this.explode()
                    }
                }

                // 更新所有粒子并移除已消失的粒子
                this.particles.forEach((p, i) => {
                    p.update()
                    if (p.alpha <= 0) this.particles.splice(i, 1)
                })
            }

            // 爆炸效果生成粒子
            explode() {
                this.phase = 'exploding'
                const particleCount = 250 + Math.random() * 200 // 粒子数量250-400
                for (let i = 0; i < particleCount; i++) {
                    this.particles.push(new Particle(
                        this.x,
                        this.y,
                        this.hue
                    ))
                }
            }

            // 绘制烟花(发射轨迹和粒子)
            draw() {
                // 绘制上升轨迹
                if (this.phase === 'launching') {
                    ctx.beginPath()
                    ctx.arc(this.x, this.y, 2, 0, Math.PI * 2)
                    ctx.fillStyle = `hsl(${this.hue}, 100%, 50%)`
                    ctx.fill()
                }

                // 绘制所有粒子
                this.particles.forEach(p => p.draw())
            }
        }

        /**
         * 粒子类:管理单个爆炸粒子的运动效果
         */
        class Particle {
            constructor(x, y, baseHue) {
                this.x = x                         // 初始X坐标
                this.y = y                         // 初始Y坐标
                this.angle = Math.random() * Math.PI * 2 // 随机扩散角度
                this.speed = Math.random() * 6 + 4 // 初始速度(4-12)
                this.vx = Math.cos(this.angle) * this.speed // X方向速度
                this.vy = Math.sin(this.angle) * this.speed // Y方向速度
                this.baseHue = baseHue             // 基础颜色
                this.hueShift = Math.random() * 180 - 40 // 色相偏移量(-40°~+40°),爆炸后彩色
                this.alpha = 1                     // 初始透明度
                this.decay = Math.random() * 0.01 + 0.015 // 透明度衰减速度
                this.gravity = 0.2                 // 重力加速度
                this.resistance = .97             // 空气阻力系数,爆炸面积
            }

            // 更新粒子状态(每帧调用)
            update() {
                // 应用物理效果
                this.vx *= this.resistance  // X方向速度衰减
                this.vy *= this.resistance  // Y方向速度衰减
                this.vy += this.gravity     // 施加重力

                // 更新位置
                this.x += this.vx
                this.y += this.vy

                // 更新透明度
                this.alpha -= this.decay
            }

            // 获取当前颜色(动态计算)
            get currentColor() {
                // 色相随透明度变化:初始色相 + 偏移量*(1-透明度)
                const currentHue = (this.baseHue + this.hueShift * (1 - this.alpha)) % 360
                // 使用HSLA颜色模式实现渐变透明
                return `hsla(${currentHue}, 100%, 50%, ${this.alpha})`
            }

            // 绘制粒子
            draw() {
                ctx.beginPath()
                ctx.arc(this.x, this.y, 1.2, 0, Math.PI * 2) // 绘制圆形粒子
                ctx.fillStyle = this.currentColor         // 设置动态颜色
                ctx.fill()
            }
        }

        // 烟花数组(同时存在的烟花实例)
        const fireworks = []

        /**
         * 创建新烟花(数量控制)
         */
        function createFirework() {
            // 同时最多存在6个烟花(性能优化)
            if (fireworks.length < 6) {
                fireworks.push(new Firework())
            }
        }

        /**
         * 动画主循环
         */
        function animate() {
            // 用半透明黑色覆盖实现拖尾效果
            ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'
            ctx.fillRect(0, 0, canvas.width, canvas.height)

            // 更新并绘制所有烟花
            fireworks.forEach((fw, i) => {
                fw.update()
                fw.draw()

                // 移除已完成烟花(粒子全部消失且处于爆炸状态)
                if (fw.particles.length === 0 && fw.phase === 'exploding') {
                    fireworks.splice(i, 1)
                }
            })

            // 随机生成新烟花(5%概率每帧)
            if (Math.random() < 0.05) createFirework()

            // 请求下一帧动画
            requestAnimationFrame(animate)
        }

        // 窗口大小变化时重置Canvas尺寸
        window.addEventListener('resize', () => {
            canvas.width = window.innerWidth
            canvas.height = window.innerHeight
        })

        // 启动动画
        animate();
    </script>
</body>

</html>