情人节快到了,一起来看烟花吧🎆🎆

2,373 阅读10分钟

新年来临之前,业务需求要求整一个烟花特效,对canvas研究不多,翻遍GitHub,找到了一个合眼缘的烟花效果,拿到源码了咱们来根据需求进行特制改造。 此库的效果展示,甚合我心,在情人节前夕,分享给各位有心人。

1.结合cursor达成业务需求

   最终效果

2.结合cursor快速理解源码

只研究烟花特效

  • 将sound去掉
  • 将state去掉
  • 只保留canvas元素
  1. 分12种烟花效果

 const shellTypes = {
    Random: randomShell,  //随机 剩余11种里边随机挑选一种
    Crackle: crackleShell, // 裂纹
    Crossette: crossetteShell, 
    Crysanthemum: crysanthemumShell, //菊花型
    "Falling Leaves": fallingLeavesShell,
    Floral: floralShell,
    Ghost: ghostShell, //幽灵效果
    "Horse Tail": horsetailShell,
    Palm: palmShell, //棕榈型
    Ring: ringShell, //环形
    Strobe: strobeShell, //闪烁效果
    Willow: willowShell, //柳树型
  }
  //因为是白色背景的烟花,换成其他颜色,根据业务需要搭配色值
  const COLOR = {
    Pink: "#FFEAB2",
    Yellow: "#F6FF3D",
    Orange: "#FF7E3D",
  };
  1. Crackle:

  1. Crossette:

  1. Crysanthemum

  1. Falling Leaves

  1. Floral

  1. Ghost

  1. Horse Tail

  1. Palm

  1. Ring

  1. Strobe

  1. Willow

  1. 烟花绽放效果源码分析

    1.   Shell - 烟花主体类

    class Shell {
        constructor(options) {
            // 将传入的所有选项复制到实例上
            Object.assign(this, options);
            // 设置星星生命周期变化范围,默认为0.125
            this.starLifeVariation = options.starLifeVariation || 0.125;
            // 设置烟花颜色,如果没有指定则随机选择
            this.color = options.color || randomColor();
            // 设置闪光颜色,默认与主体颜色相同
            this.glitterColor = options.glitterColor || this.color;
            // 如果没有指定星星数量,则根据扩散大小计算
            if (!this.starCount) {
                const density = options.starDensity || 1;
                const scaledSize = this.spreadSize / 54;
                this.starCount = Math.max(6, scaledSize * scaledSize * density);
            }
        }
    
    }
    

      Shell 类支持的主要配置选项包括:

    1.   基本参数
      {
          spreadSize: Number,     // 爆炸扩散范围大小
          starCount: Number,      // 星星数量(可选)
          starLife: Number,       // 星星生命周期
          starLifeVariation: Number, // 生命周期变化范围
          color: String,          // 烟花颜色
          glitterColor: String    // 闪光颜色
      }
      
    1.   特效参数
    {
        glitter: String,        // 闪光效果:'light','medium','heavy','streamer','willow'
        pistil: Boolean,        // 是否有中心点
        pistilColor: String,    // 中心点颜色
        streamers: Boolean,     // 是否有流光效果
        crossette: Boolean,     // 是否有十字交叉效果
        floral: Boolean,        // 是否有花朵效果
        crackle: Boolean       // 是否有爆裂声效果
    }
    

    3. #####   主要方法:

     // 发射烟花
    launch(position, launchHeight) {
        const width = stageW;
        const height = stageH;
        const hpad = 60;        // 屏幕边缘保留空间
        const vpad = 50;        // 顶部保留空间
        const minHeightPercent = 0.45;
        
        // 计算发射和爆炸位置
        const launchX = position * (width - hpad * 2) + hpad;
        const burstY = minHeight - launchHeight * (minHeight - vpad);
        
        // 直接在目标位置爆炸
        this.burst(launchX, burstY);
    }
    
    // 爆炸效果
    burst(x, y) {
        // 计算扩散速度
        const speed = this.spreadSize / 96;
        
        // 创建爆炸效果
        createBurst(this.starCount, (angle, speedMult) => {
            // 创建星星粒子
            const star = Star.add(
                x, y,
                this.color,
                angle,
                speedMult * speed,
                this.starLife + Math.random() * this.starLife * this.starLifeVariation
            );
            
            // 添加特效
            if (this.glitter) {
                star.sparkFreq = sparkFreq;
                star.sparkSpeed = sparkSpeed;
                star.sparkLife = sparkLife;
                star.sparkLifeVariation = sparkLifeVariation;
                star.sparkColor = this.glitterColor;
                star.sparkTimer = Math.random() * star.sparkFreq;
            }
        });
    }
    

    4. #####   特效效果处理

     // 处理双色烟花
    if (Array.isArray(this.color)) {
        // 创建半圆形双色效果
        if (Math.random() < 0.5) {
            const start = Math.random() * Math.PI;
            const start2 = start + Math.PI;
            const arc = Math.PI;
            // 分别创建两种颜色的半圆
            createBurst(this.starCount, starFactory, start, arc);
            createBurst(this.starCount, starFactory, start2, arc);
        } 
        // 创建混合双色效果
        else {
            createBurst(this.starCount / 2, starFactory);
            createBurst(this.starCount / 2, starFactory);
        }
    }
    

    5. #####   附加效果

     // 中心点效果
    if (this.pistil) {
        const innerShell = new Shell({
            spreadSize: this.spreadSize * 0.5,
            starLife: this.starLife * 0.6,
            color: this.pistilColor,
            glitter: "light"
        });
        innerShell.burst(x, y);
    }
    
    // 流光效果
    if (this.streamers) {
        const innerShell = new Shell({
            spreadSize: this.spreadSize * 0.9,
            starLife: this.starLife * 0.8,
            color: COLOR.Pink,
            glitter: "streamer"
        });
        innerShell.burst(x, y);
    }
    

    2. ####   Star - 烟花粒子类

    const Star = {
        // 视觉属性
        drawWidth: 3,          // 绘制宽度
        airDrag: 0.98,         // 标准空气阻力
        airDragHeavy: 0.992,   // 重型粒子空气阻力
    
        // 按颜色存储活动的粒子
        active: createParticleCollection(),  // 创建粒子集合
        _pool: [], // 对象池
    
        // 创建新实例
        _new() {
            return {};
        },
    
        // 添加新粒子
        add(x, y, color, angle, speed, life, speedOffX, speedOffY) {
            // 从对象池获取或创建新实例
            const instance = this._pool.pop() || this._new();
    
            // 基础属性
            instance.visible = true;     // 是否可见
            instance.heavy = false;      // 是否重型粒子
            instance.x = x;              // X坐标
            instance.y = y;              // Y坐标
            instance.prevX = x;          // 前一帧X坐标
            instance.prevY = y;          // 前一帧Y坐标
            instance.color = color;      // 颜色
            
            // 速度分量
            instance.speedX = Math.sin(angle) * speed + (speedOffX || 0);
            instance.speedY = Math.cos(angle) * speed + (speedOffY || 0);
            
            // 生命周期
            instance.life = life;        // 当前生命值
            instance.fullLife = life;    // 总生命值
    
            // 自旋相关属性
            instance.spinAngle = Math.random() * PI_2;  // 自旋角度
            instance.spinSpeed = 0.8;                   // 自旋速度
            instance.spinRadius = 0;                    // 自旋半径
    
            // 火花效果属性
            instance.sparkFreq = 0;      // 火花发射频率
            instance.sparkSpeed = 1;     // 火花速度
            instance.sparkTimer = 0;     // 火花计时器
            instance.sparkColor = color; // 火花颜色
            instance.sparkLife = 750;    // 火花生命周期
            instance.sparkLifeVariation = 0.25;  // 火花生命变化范围
            
            // 特殊效果
            instance.strobe = false;     // 是否闪烁
            instance.alpha = 1;          // 透明度
    
            // 将粒子添加到对应颜色的活动集合中
            this.active[color].push(instance);
            return instance;
        },
    
        // 回收粒子实例到对象池
        returnInstance(instance) {
            // 触发死亡回调
            instance.onDeath && instance.onDeath(instance);
    
            // 重置属性
            instance.onDeath = null;
            instance.secondColor = null;
            instance.transitionTime = 0;
            instance.colorChanged = false;
    
            // 放回对象池
            this._pool.push(instance);
        }
    };
    
    1.   对象池模式
    _pool: [],
    _new() {
        return {};
    },
    
    • 使用对象池模式来减少内存分配和垃圾回收

    • 复用对象而不是频繁创建新对象

    1.   粒子属性系统
    // 位置和运动
    x, y            // 当前位置
    prevX, prevY    // 上一帧位置
    speedX, speedY  // 速度分量
    
    // 生命周期
    life            // 当前生命值
    fullLife        // 总生命值
    
    // 自旋系统
    spinAngle       // 自旋角度
    spinSpeed       // 自旋速度
    spinRadius      // 自旋半径
    

    3. #####   火花效果系统

    // 火花属性
    sparkFreq           // 发射频率
    sparkSpeed          // 火花速度
    sparkTimer          // 计时器
    sparkColor          // 火花颜色
    sparkLife           // 生命周期
    sparkLifeVariation  // 生命变化范围
    

    4. #####   性能优化

    // 按颜色分组存储
    active: createParticleCollection(),
    
    // 使用对象池
    const instance = this._pool.pop() || this._new();
    

    5. #####   特效支持

    // 支持多种特效
    strobe          // 闪烁效果
    alpha           // 透明度渐变
    secondColor     // 颜色过渡
    transitionTime  // 过渡时间
    

    6. #####   使用示例:

    // 创建一个新的星星粒子
    const star = Star.add(
        100,                    // x位置
        100,                    // y位置
        COLOR.Yellow,          // 颜色
        Math.PI / 4,           // 角度
        2,                     // 速度
        1000,                  // 生命周期
        0,                     // X方向速度偏移
        0                      // Y方向速度偏移
    );
    
    // 添加火花效果
    star.sparkFreq = 100;      // 每100ms发射一次火花
    star.sparkSpeed = 0.5;     // 火花速度
    star.sparkLife = 500;      // 火花持续500ms
    
    // 添加闪烁效果
    star.strobe = true;
    

    7. #####   更新机制:

        粒子的更新在主循环中进行,包括:
    
    *   位置更新
    *   速度衰减(空气阻力)
    *   生命值递减
    *   特效处理(火花、闪烁等)
    *   自旋运动计算
    *   透明度变化
    
        这个类的设计非常精巧,通过组合不同的属性可以创造出各种绚丽的烟花效果。同时通过对象池和颜色分组等优化手段,保证了在大量粒子同时存在的情况下依然能保持良好的性能。
    
    1.   Spark - 火花效果类

    const Spark = {
        // 基础视觉属性
        drawWidth: 0,      // 绘制宽度
        airDrag: 0.9,      // 空气阻力系数
    
        // 存储系统
        active: createParticleCollection(),  // 按颜色分组存储活动的火花
        _pool: [],         // 对象池
    
        // 创建新实例
        _new() {
            return {};
        },
    
        // 添加新火花
        add(x, y, color, angle, speed, life) {
            // 从对象池获取或创建新实例
            const instance = this._pool.pop() || this._new();
    
            // 设置位置属性
            instance.x = x;          // 当前X坐标
            instance.y = y;          // 当前Y坐标
            instance.prevX = x;      // 前一帧X坐标
            instance.prevY = y;      // 前一帧Y坐标
            
            // 设置基本属性
            instance.color = color;  // 火花颜色
            
            // 设置运动属性
            instance.speedX = Math.sin(angle) * speed;  // X方向速度
            instance.speedY = Math.cos(angle) * speed;  // Y方向速度
            instance.life = life;    // 生命周期
            instance.alpha = 1;      // 透明度
    
            // 将火花添加到对应颜色的活动集合中
            this.active[color].push(instance);
            return instance;
        },
    
        // 回收火花实例到对象池
        returnInstance(instance) {
            this._pool.push(instance);
        }
    };
    
    1.   简化设计
    drawWidth: 0,    // 极小的绘制宽度
    airDrag: 0.9,    // 较大的空气阻力,使火花快速减速
    

    2. #####   对象池管理

    _pool: [],
    _new() {
        return {};
    },
    
    • 相比Star类,Spark类设计更简单

    • 火花通常比星星小且生命周期更短

    1.   对象池管理
    _pool: [],
    _new() {
        return {};
    },
    
    • 使用对象池模式优化内存使用

    • 减少垃圾回收压力

    1.   分组存储
    active: createParticleCollection(),
    
    • 按颜色分组存储活动的火花

    1.    火花实例属性
    {
        // 位置属性
        x: Number,        // 当前X坐标
        y: Number,        // 当前Y坐标
        prevX: Number,    // 上一帧X坐标
        prevY: Number,    // 上一帧Y坐标
        
        // 运动属性
        speedX: Number,   // X方向速度
        speedY: Number,   // Y方向速度
        
        // 视觉属性
        color: String,    // 颜色
        alpha: Number,    // 透明度
        
        // 生命周期
        life: Number      // 剩余生命值
    }
    

    6. #####    使用示例

    // 创建一个新的火花
    const spark = Spark.add(
        100,                // x位置
        100,                // y位置
        COLOR.Yellow,       // 颜色
        Math.random() * PI_2, // 随机角度
        1.5,                // 速度
        300                 // 生命周期(300ms)
    );
    

    7. #####   更新机制

      在主循环中的更新逻辑:

    // 火花更新逻辑
    COLOR_CODES.forEach(color => {
        const sparks = Spark.active[color];
        for (let i = sparks.length - 1; i >= 0; i = i - 1) {
            const spark = sparks[i];
            
            // 更新生命值
            spark.life -= timeStep;
            
            // 检查是否存活
            if (spark.life <= 0) {
                // 移除死亡的火花
                sparks.splice(i, 1);
                Spark.returnInstance(spark);
            } else {
                // 更新位置
                spark.prevX = spark.x;
                spark.prevY = spark.y;
                spark.x += spark.speedX * speed;
                spark.y += spark.speedY * speed;
                
                // 应用空气阻力
                spark.speedX *= sparkDrag;
                spark.speedY *= sparkDrag;
                
                // 应用重力
                spark.speedY += gAcc;
            }
        }
    });
    

    8. #####   渲染实现

    // 在渲染循环中的绘制逻辑
    trailsCtx.lineWidth = Spark.drawWidth;
    trailsCtx.lineCap = "butt";
    
    COLOR_CODES.forEach(color => {
        const sparks = Spark.active[color];
        trailsCtx.strokeStyle = color;
        trailsCtx.beginPath();
        
        sparks.forEach(spark => {
            // 设置透明度
            trailsCtx.globalAlpha = globalAlpha * 0.8;
            // 绘制火花轨迹
            trailsCtx.moveTo(spark.x, spark.y);
            trailsCtx.lineTo(spark.prevX, spark.prevY);
        });
        
        trailsCtx.stroke();
    });
    

    9. #####   与Star类的主要区别

    • 简化属性

      • 没有自旋系统
      • 没有火花发射系统
      • 没有特效系统(如闪烁)
    • 物理特性

      • 更大的空气阻力(0.9 vs 0.98)
      • 更小的绘制宽度(0 vs 3)
      • 更短的生命周期
    • 视觉效果

      • 更细小的粒子
      • 更快的消失速度
      • 更简单的运动轨迹

      Spark类主要用于创建烟花爆炸时的小型火花效果,它们数量多但生命周期短,通过大量这样的小火花可以创造出绚丽的视觉效果。其简化的设计也保证了在大量火花同时存在的情况下,仍能保持良好的性能。

    1.   BurstFlash - 爆炸闪光效果类

    const BurstFlash = {
        // 存储系统
        active: [],     // 活动的闪光效果数组
        _pool: [],      // 对象池
    
        // 创建新实例
        _new() {
            return {};
        },
    
        // 添加新的爆炸闪光
        add(x, y, radius) {
            // 从对象池获取或创建新实例
            const instance = this._pool.pop() || this._new();
    
            // 设置基本属性
            instance.x = x;          // 闪光中心X坐标
            instance.y = y;          // 闪光中心Y坐标
            instance.radius = radius; // 闪光半径
    
            // 将实例添加到活动数组
            this.active.push(instance);
            return instance;
        },
    
        // 回收实例到对象池
        returnInstance(instance) {
            this._pool.push(instance);
        }
    };
    
  1. 简单结构
{
    x: Number,      // 位置X
    y: Number,      // 位置Y
    radius: Number  // 闪光半径
}
  • 相比Star和Spark类,结构更简单
  • 只需要位置和半径信息
  1. 对象池管理
_pool: [],
_new() {
    return {};
}
  • 使用对象池模式优化内存使用
  • 减少垃圾回收压力
  1. 2使用场景
// 在烟花爆炸时创建闪光效果
BurstFlash.add(
    shell.x,           // 爆炸位置X
    shell.y,           // 爆炸位置Y
    shell.spreadSize / 4  // 闪光半径为扩散范围的1/4
);

4. ##### 渲染实现

// 在渲染循环中的绘制逻辑
while (BurstFlash.active.length) {
    // 获取并移除一个闪光实例
    const bf = BurstFlash.active.pop();

    // 创建径向渐变
    const burstGradient = trailsCtx.createRadialGradient(
        bf.x, bf.y, 0,    // 内圆心和半径
        bf.x, bf.y, bf.radius  // 外圆心和半径
    );

    // 设置渐变颜色停止点
    burstGradient.addColorStop(0.024, 'rgba(255, 255, 255, 1)');     // 中心纯白
    burstGradient.addColorStop(0.125, 'rgba(255, 160, 20, 0.2)');    // 过渡区域
    burstGradient.addColorStop(0.32, 'rgba(255, 140, 20, 0.11)');    // 过渡区域
    burstGradient.addColorStop(1, 'rgba(255, 120, 20, 0)');          // 边缘透明

    // 应用渐变并绘制
    trailsCtx.fillStyle = burstGradient;
    trailsCtx.fillRect(
        bf.x - bf.radius,
        bf.y - bf.radius,
        bf.radius * 2,
        bf.radius * 2
    );

    // 回收实例
    BurstFlash.returnInstance(bf);
}

5. ##### 特点分析

  • 即时渲染

    • 闪光效果是即时的,不需要动画过渡
    • 一旦渲染完成就立即回收实例
  • 渐变效果

    •     // 渐变颜色设置
          0.024 -> 纯白色 (1.0)     // 中心最亮
          0.125 -> 橙色 (0.2)      // 快速衰减
          0.32  -> 橙色 (0.11)     // 继续衰减
          1.0   -> 橙色 (0.0)      // 完全透明
      
    • 使用径向渐变创造真实的爆炸光效
    • 从中心向外逐渐衰减
  1. 性能优化
// 使用矩形绘制而不是圆形
trailsCtx.fillRect(
    bf.x - bf.radius,
    bf.y - bf.radius,
    bf.radius * 2,
    bf.radius * 2
);
  • 使用fillRect代替arc+fill提高性能
  • 径向渐变确保视觉效果是圆形的
  1. 应用示例
// 在Shell类的burst方法中使用
burst(x, y) {
    // ... 其他爆炸效果代码 ...

    // 创建爆炸闪光
    BurstFlash.add(x, y, this.spreadSize / 4);

    // ... 其他效果代码 ...
}

8. ##### 与其他效果类的关系

  • 与Star类的配合

    • 在烟花爆炸时提供初始闪光效果
    • 增强爆炸瞬间的视觉冲击
  • 与Spark类的配合

    • 为火花提供初始照明效果
    • 增强整体爆炸效果的真实感
  • 特点对比

    • Star: 持续运动的粒子效果
    • Spark: 短暂的火花效果
    • BurstFlash: 瞬时的闪光效果

BurstFlash类虽然结构简单,但在整个烟花效果中起到了重要的视觉增强作用。它通过模拟真实烟花爆炸时的瞬间强光,使整个效果更加逼真和震撼。同时,其优化的实现方式也确保了在频繁的烟花爆炸中保持良好的性能。