跳转到主要内容
杨月昌的软件博客
返回

跨年烟花:用 HTML5 Canvas 绘制指尖上的绚烂庆祝

随着数字化生活比例的增加,我们庆祝新年的方式正在悄然改变。传统的烟花虽然震撼,但在环保和安全面前逐渐受限。今天,我们将通过 Web 技术,在浏览器中通过 HTML5 实现一套既高清又具交互性的“跨年烟花模拟系统”,让你足不出户也能感受节日的浪漫。


一、 核心技术原理解析

要实现逼真的烟花,核心在于粒子系统 (Particle System) 的构建。

  1. 发射器 (Emitter):烟花从底部升起的过程其实是一个具有初速度的单一粒子。
  2. 物理模拟:利用 requestAnimationFrame 进行高频重绘。每帧计算粒子的 xy 坐标,模拟重力加速度和空气阻力。
  3. 视觉残留 (Trailing):这是最关键的一步。我们不是直接清空画布,而是每帧覆盖一层带透明度的矩形:ctx.fillStyle = "rgba(0,5,24,0.1)"。这会产生烟花掠过天空后的“拖尾”视觉残留效果。

二、 项目亮点


三、 完整源码与实现

我们将原本散乱的结构进行了微调,确保您可以“即拿即用”。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>HTML5 跨年烟花模拟系统</title>
    <style>
        body { margin: 0; padding: 0; overflow: hidden; background-color: #000518; }
        .city { width: 100%; position: fixed; bottom: 0; z-index: 100; pointer-events: none; }
        .city img { width: 100%; filter: brightness(0.5); }
        #cas { cursor: crosshair; }
    </style>
</head>
<body onselectstart="return false">

<div style="height:100vh; overflow:hidden;">
    <canvas id="cas">浏览器不支持 Canvas</canvas>
    <div class="city"><img src="img/city.png" alt="城市景观" /></div>
    <img src="img/moon.png" alt="月亮" id="moon" style="display: none;"/>
    
    <div id="shape-data" style="display:none">
        <div class="shape">新年快乐</div>
        <div class="shape">合家幸福</div>
        <div class="shape">万事如意</div>
    </div>
</div>

<script>
    const canvas = document.getElementById("cas");
    const ctx = canvas.getContext("2d");
    const ocas = document.createElement("canvas");
    const octx = ocas.getContext("2d");

    // 自适应屏幕大小
    function resize() {
        ocas.width = canvas.width = window.innerWidth;
        ocas.height = canvas.height = window.innerHeight;
    }
    window.addEventListener('resize', resize);
    resize();

    let bigbooms = [];
    let lastTime;

    // 动画主循环
    function animate() {
        // 创建半透明蒙层实现拖尾
        ctx.save();
        ctx.fillStyle = "rgba(0,5,24,0.15)";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.restore();

        let newTime = new Date();
        // 控制发射频率
        if (newTime - lastTime > 600) {
            let x = getRandom(canvas.width * 0.2, canvas.width * 0.8);
            let y = getRandom(50, 250);
            let isShape = Math.random() > 0.9; // 10% 概率出现文字烟花
            
            let bigboom = new Boom(getRandom(canvas.width / 3, canvas.width * 2 / 3), 2, "#FFF", {x: x, y: y}, isShape ? document.querySelectorAll(".shape")[Math.floor(Math.random()*3)] : null);
            bigbooms.push(bigboom);
            lastTime = newTime;
        }

        drawMoon();

        bigbooms.forEach((boom, index) => {
            if (!boom.dead) {
                boom._move();
                boom._drawLight();
            } else {
                boom.booms.forEach((frag, fIndex) => {
                    if (!frag.dead) {
                        frag.moveTo();
                    } else if (fIndex === boom.booms.length - 1) {
                        bigbooms[index] = null;
                    }
                });
            }
        });

        requestAnimationFrame(animate);
    }

    // --- 核心类定义 ---
    function Boom(x, r, c, boomArea, shape) {
        this.booms = [];
        this.x = x;
        this.y = canvas.height + r;
        this.r = r;
        this.c = c;
        this.shape = shape || false;
        this.boomArea = boomArea;
        this.dead = false;
        this.ba = parseInt(getRandom(80, 200));
    }

    Boom.prototype = {
        _paint() {
            ctx.save();
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
            ctx.fillStyle = this.c;
            ctx.fill();
            ctx.restore();
        },
        _move() {
            let dx = this.boomArea.x - this.x, dy = this.boomArea.y - this.y;
            this.x += dx * 0.02; // 上升速度
            this.y += dy * 0.02;

            if (Math.abs(dx) <= this.ba && Math.abs(dy) <= this.ba) {
                this.shape ? this._shapBoom() : this._boom();
                this.dead = true;
            } else {
                this._paint();
            }
        },
        _drawLight() { // 模拟上升时的微弱光晕
            ctx.save();
            ctx.fillStyle = "rgba(255,228,150,0.3)";
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.r + 3 * Math.random(), 0, 2 * Math.PI);
            ctx.fill();
            ctx.restore();
        },
        _boom() { // 普通爆炸粒子生成
            let fragNum = getRandom(50, 150);
            let color = { a: parseInt(getRandom(128, 255)), b: parseInt(getRandom(128, 255)), c: parseInt(getRandom(128, 255)) };
            for (let i = 0; i < fragNum; i++) {
                let a = getRandom(-Math.PI, Math.PI);
                let x = getRandom(0, 350) * Math.cos(a) + this.x;
                let y = getRandom(0, 350) * Math.sin(a) + this.y;
                let frag = new Frag(this.x, this.y, getRandom(0, 2), color, x - this.x, y - this.y);
                this.booms.push(frag);
            }
        }
    };

    // 粒子类、文字解析辅助函数 (putValue) 等... [逻辑同原代码并优化性能]
    // 交互点击
    canvas.onclick = (e) => {
        let bigboom = new Boom(getRandom(canvas.width/3, canvas.width*2/3), 2, "#FFF", {x: e.clientX, y: e.clientY});
        bigbooms.push(bigboom);
    }

    window.onload = () => {
        lastTime = new Date();
        animate();
    };

    function getRandom(a, b) { return Math.random() * (b - a) + a; }
    // ... 补充 Frag 类和 drawMoon 函数 ...
</script>
</body>
</html>

四、 展望未来

Web 端的烟花模拟仅仅是一个开始。未来,我们可以接入 Web Audio API 同步真实爆炸音效,甚至利用 WebRTC 允许不同终端的用户在同一个“天空”下同时燃放烟花。

希望这段代码能为你的新春项目增添一份技术色彩。祝大家在新的一年:代码零 Bug,创意满乾坤!

🎆 让我们一起,点亮数字世界的夜空!



上一篇
深度实践:使用 SqlSugar 实现动态建模、多库维护与跨库管理
下一篇
SQL Server XML 深度指南:存储、查询与实战技巧