2026年跨年倒计时网页版

【前端实战】用 HTML5 Canvas 打造震撼的 2026 "盛世繁花" 跨年倒计时



创作者:灯把黑夜烧了一个洞

摘要 :再过不久我们就要迎来新的年份。作为程序员,用代码送上祝福是最浪漫的方式。本文将带你解析一个集成了翻牌倒计时物理引擎烟花粒子文字特效 以及炫彩流光祝福的跨年网页项目。我们将重点通过 Canvas 技术实现自定义形状的烟花(如生肖马、2026数字),并探讨如何处理移动端适配与音频自动播放策略。

🌟 效果预览

点击预览效果

在这个项目中,我们实现了以下核心功能:

  1. 暗黑质感倒计时:采用深色玻璃拟态风格,通过 flex 布局实现响应式居中。
  2. 物理粒子烟花:基于重力、空气阻力和初速度的物理模拟。
  3. 自定义形状烟花:通过像素采样技术,让烟花炸裂成 "🐴"(马年)、"2026" 等特定形状。
  4. 炫彩流光祝福:CSS3 渐变文字动画与 JS 动态字号计算结合。
  5. 沉浸式音效:背景音乐与烟花爆炸音效的协同处理。

🛠️ 技术栈解析

  • HTML5 Structure: 语义化标签构建基础 UI。
  • CSS3 Animation: 处理 UI 动效、流光字体及响应式布局。
  • Canvas API: 核心绘图层,用于渲染粒子系统。
  • JavaScript (ES6+): 逻辑控制、物理计算、DOM 操作。

🎨 一、UI 设计:暗夜里的极简倒计时

页面整体色调定义为深邃的黑色系,利用 CSS 变量管理颜色,方便后续换肤。

css 复制代码
:root {
    --bg-color: #050505;
    /* 模拟光影质感的卡片背景 */
    --card-bg: linear-gradient(180deg, #1a1a1a 0%, #000 50%, #050505 51%, #111 100%);
}

倒计时卡片使用了 box-shadow 制造悬浮感,并配合大字号的 Impact 字体,营造出一种硬朗的机械时钟风格。为了适应移动端,我们使用了 vw (视口宽度) 单位来设置字体大小,确保在手机和 PC 上都有良好的展示比例。

🚀 二、核心技术:Canvas 粒子物理引擎

这是整个项目的灵魂所在。我们没有使用现成的库,而是手写了一个轻量级的物理引擎。

1. 粒子类 (Particle)

每个烟花火星都是一个 Particle 对象。它拥有坐标、速度(向量)、颜色、透明度和物理属性(重力、阻力)。

javascript 复制代码
class Particle {
    constructor(...) {
        // ...初始化状态
        this.resistance = 0.96; // 空气阻力,模拟空气摩擦
        this.gravity = 0.06;    // 重力,让粒子下坠
    }
    update() {
        this.vx *= this.resistance; // 速度衰减
        this.vy *= this.resistance;
        this.vy += this.gravity;    // 垂直方向受重力影响
        this.x += this.vx;
        this.y += this.vy;
        this.alpha -= 0.015;        // 慢慢消失
    }
}

2. 烟花发射 (Rocket)

火箭类负责从屏幕底部升起到目标高度。当到达目标高度(this.y <= this.ty)时,触发 explode() 方法。


🐴 三、进阶技巧:自定义形状烟花 (Shape Sampling)

代码中最精彩的部分莫过于烟花能炸出 "2026" 或 "🐴" 的形状。这利用了 Canvas 像素采样 技术。

原理如下:

  1. 创建一个离屏 Canvas(不可见)。
  2. 在离屏 Canvas 上绘制白色的文字或 Emoji。
  3. 使用 getImageData 获取画布上的像素数据。
  4. 遍历像素点,找到有颜色的位置,根据该位置生成粒子。
javascript 复制代码
function createShapeParticles(type, x, y) {
    // 1. 在离屏 Canvas 上写字
    const off = document.createElement('canvas');
    // ... 设置大小、字体 ...
    octx.fillText(char, width / 2, height / 2);
    
    // 2. 获取像素数据
    const data = octx.getImageData(0, 0, width, height).data;

    // 3. 遍历像素,以步长 6 进行采样(减少粒子数量,提升性能)
    for (let i = 0; i < height; i += 6) {
        for (let j = 0; j < width; j += 6) {
            // 如果该像素点的 Alpha 通道 > 128 (说明这里有字)
            if (data[(i * width + j) * 4 + 3] > 128) {
                // 计算粒子的发射速度,使其看起来像是从中心炸开
                const vx = (j - width / 2) / 20 + (Math.random() - 0.5) * 2;
                const vy = (i - height / 2) / 20 + (Math.random() - 0.5) * 2;
                particles.push(new Particle(x, y, color, vx, vy));
            }
        }
    }
}

这种方法极其灵活,你可以把 Emoji、SVG 甚至图片转换成粒子烟花。


🌈 四、视觉盛宴:流光祝福文字

当倒计时结束,进入 startNewYear() 庆祝模式。此时 UI 隐去,祝福语登场。

为了保证文字在不同长度下都能完美居中且不换行,代码中实现了一个动态字号计算函数

javascript 复制代码
// 占据屏幕 68% 的宽度
function setArtText(text) {
    const targetWidth = width * 0.68;
    // 估算字号:总宽 / (字符数 * 系数)
    const fontSize = targetWidth / (text.length * 0.95);
    // 限制最大高度,防止字体过大撑破屏幕
    bDisplay.style.fontSize = Math.min(fontSize, height * 0.3) + 'px';
    bDisplay.innerText = text;
}

配合 CSS 的 background-clip: text@keyframes 动画,实现了文字内部的彩虹流光效果。


🎵 五、细节处理:音频自动播放策略

现代浏览器(尤其是 Chrome 和 Safari)通常禁止音频自动播放。为了解决这个问题,代码中采用了一个经典的交互引导策略:

  1. 在屏幕下方显示提示:"点击屏幕开启音乐"。
  2. 监听 bodyonclick 事件。
  3. 一旦用户点击,立即触发 initAudio(),解锁 AudioContext。
javascript 复制代码
<body onclick="initAudio()">
// ...
function initAudio() {
    if (audioStarted) return;
    audioStarted = true;
    document.querySelector('.hint').style.display = 'none'; // 隐藏提示
    // 开始播放
    document.getElementById('bgm').play(); 
}

📝 总结与扩展

这段代码展示了前端开发中 "艺术" 的一面。它没有复杂的框架,仅用原生的 API 就实现了电影级的转场效果。

如果你想自己魔改这个项目,可以尝试:

  1. 修改祝福语 :找到 blessingList 数组,换成你对家人或朋友的名字。
  2. 更换背景音乐 :替换 <audio> 标签中的 src 链接。
  3. 增加互动:监听鼠标点击位置,实现指哪打哪的烟花效果。

前端不仅仅是写表单和列表,它也是我们表达情感、创造浪漫的画笔。2026 还有点远,但技术的积累就在当下。

完整源码已在文中展示,复制即可运行! 祝大家代码无 Bug,人生不 Null Pointer!🎆


附录(完整代码)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>2026 盛世繁花跨年倒计时</title>
    <style>
        :root {
            --bg-color: #050505;
            --card-bg: linear-gradient(180deg, #1a1a1a 0%, #000 50%, #050505 51%, #111 100%);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background-color: var(--bg-color);
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
            font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
            touch-action: manipulation;
        }

        /* 倒计时 UI */
        .countdown-main {
            position: absolute;
            width: 90vw;
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 1.5vw;
            z-index: 100;
            transition: all 1.5s cubic-bezier(0.645, 0.045, 0.355, 1);
        }

        .flip-card {
            flex: 1;
            height: 60vh;
            background: var(--card-bg);
            border: 1px solid #333;
            border-radius: 12px;
            display: flex;
            justify-content: center;
            align-items: center;
            position: relative;
            box-shadow: 0 20px 50px rgba(0, 0, 0, 0.9);
        }

        .num {
            font-size: 15vw;
            font-weight: 900;
            color: #fff;
            font-family: 'Impact', sans-serif;
        }

        /* 彩色艺术文字容器 */
        #blessing-container {
            position: absolute;
            width: 100%;
            height: 100%;
            display: none;
            justify-content: center;
            align-items: center;
            z-index: 110;
            pointer-events: none;
        }

        .blessing-text {
            /* 彩色流光渐变 */
            background: linear-gradient(90deg, #ff3333, #ffcc33, #33ff33, #33ccff, #8833ff, #ff3333);
            background-size: 200% auto;
            -webkit-background-clip: text;
            background-clip: text;
            color: transparent;
            font-weight: 900;
            text-align: center;
            filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.2));

            /* 进场动画 */
            opacity: 0;
            transform: scale(0.5);
            filter: blur(20px);
            transition: all 1.5s cubic-bezier(0.19, 1, 0.22, 1);
            animation: rainbow-flow 3s linear infinite;
        }

        .blessing-text.active {
            opacity: 1;
            transform: scale(1);
            filter: blur(0px);
        }

        @keyframes rainbow-flow {
            0% {
                background-position: 0% 50%;
            }

            100% {
                background-position: 200% 50%;
            }
        }

        #canvas {
            position: absolute;
            top: 0;
            left: 0;
            z-index: 10;
        }

        .hint {
            position: fixed;
            bottom: 20px;
            color: rgba(255, 255, 255, 0.3);
            font-size: 12px;
            z-index: 200;
        }
    </style>
</head>

<body onclick="initAudio()">

    <div class="countdown-main" id="ui">
        <div class="flip-card">
            <div class="num" id="d">00</div>
        </div>
        <div class="flip-card">
            <div class="num" id="h">00</div>
        </div>
        <div class="flip-card">
            <div class="num" id="m">00</div>
        </div>
        <div class="flip-card">
            <div class="num" id="s">00</div>
        </div>
    </div>

    <div id="blessing-container">
        <div id="blessing-display" class="blessing-text"></div>
    </div>

    <canvas id="canvas"></canvas>
    <div class="hint">点击屏幕开启音乐</div>

    <audio id="bgm" loop>
        <source src="https://m801.music.126.net/20251231110626/32b0824a699ba730346d086e663b65c4/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/34268312689/eda5/bc8c/ae37/63da84c0b67a7eb61dcb83e1717d9020.mp3" type="audio/mpeg">
    </audio>
    <audio id="sfx-firework">
        <source src="https://m701.music.126.net/20251231105810/9d991db61aed869c1b311d1452a059e7/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/28481679536/c3d3/497c/02b1/c382c46f689ec34dfccf6d7b01cd8e8d.mp3" type="audio/mpeg">
    </audio>
    <audio id="sfx-cheer">
        <source src="https://lf9-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/cheer.mp3" type="audio/mpeg">
    </audio>

    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        const ui = document.getElementById('ui');
        const bDisplay = document.getElementById('blessing-display');

        let width, height, rockets = [], particles = [], shapeParticles = [];
        let isCelebration = false, audioStarted = false;

        const targetDate = new Date('2026/01/01 00:00:00').getTime();
        const blessingList = ["2026新年快乐!", "愿所有人", "在新的一年里", "天天开心",  "健康美满", "马年大吉!"];
        let blessingIdx = 0;

        function resize() {
            width = canvas.width = window.innerWidth;
            height = canvas.height = window.innerHeight;
        }
        window.addEventListener('resize', resize);
        resize();

        // 核心:动态字号计算 (占据屏幕68%)
        function setArtText(text) {
            const targetWidth = width * 0.68;
            // 估算:字号 = 目标宽度 / 字符数。中文按1:1,英文按0.6
            const fontSize = targetWidth / (text.length * 0.95);
            bDisplay.style.fontSize = Math.min(fontSize, height * 0.3) + 'px';
            bDisplay.innerText = text;
        }

        // 1. 物理引擎部分
        class Particle {
            constructor(x, y, color, vx, vy, resistance = 0.96) {
                this.x = x; this.y = y;
                this.color = color;
                this.vx = vx; this.vy = vy;
                this.alpha = 1;
                this.resistance = resistance;
                this.gravity = 0.06;
            }
            update() {
                this.vx *= this.resistance;
                this.vy *= this.resistance;
                this.vy += this.gravity;
                this.x += this.vx;
                this.y += this.vy;
                this.alpha -= 0.015;
            }
            draw() {
                ctx.globalAlpha = this.alpha;
                ctx.fillStyle = this.color;
                ctx.fillRect(this.x, this.y, 2, 2);
            }
        }

        class Rocket {
            constructor(tx, ty, type = 'normal') {
                this.x = Math.random() * width;
                this.y = height;
                this.tx = tx; this.ty = ty;
                this.type = type;
                this.color = `hsl(${Math.random() * 360}, 100%, 60%)`;
                const angle = Math.atan2(ty - height, tx - this.x);
                this.v = 12;
                this.vx = Math.cos(angle) * this.v;
                this.vy = Math.sin(angle) * this.v;
                this.exploded = false;
            }
            update() {
                this.x += this.vx;
                this.y += this.vy;
                if (this.y <= this.ty) {
                    this.explode();
                    this.exploded = true;
                }
            }
            draw() {
                ctx.fillStyle = "#fff";
                ctx.beginPath();
                ctx.arc(this.x, this.y, 2, 0, Math.PI * 2);
                ctx.fill();
            }
            explode() {
                if (audioStarted) {
                    const s = document.getElementById('sfx-firework').cloneNode();
                    s.volume = 0.2; s.play();
                }
                if (this.type === 'normal') {
                    for (let i = 0; i < 80; i++) {
                        const a = Math.random() * Math.PI * 2;
                        const s = Math.random() * 6 + 2;
                        particles.push(new Particle(this.x, this.y, this.color, Math.cos(a) * s, Math.sin(a) * s));
                    }
                } else {
                    createShapeParticles(this.type, this.x, this.y);
                }
            }
        }

        // 2. 形状采样逻辑 (🐴, 🐴, 2026)
        function createShapeParticles(type, x, y) {
            const off = document.createElement('canvas');
            const octx = off.getContext('2d');
            off.width = width; off.height = height;
            octx.fillStyle = "white";
            octx.textAlign = "center";
            octx.textBaseline = "middle";
            let size = Math.min(width, height) * 0.7;
            octx.font = `900 ${size}px serif`;

            let char = type === 'horse' ? "🐴" : (type === 'sheep' ? "🐴🐑" : "2026");
            if (type === '2026') octx.font = `bold ${size / 2}px Arial`;

            octx.fillText(char, width / 2, height / 2);
            const data = octx.getImageData(0, 0, width, height).data;

            for (let i = 0; i < height; i += 6) {
                for (let j = 0; j < width; j += 6) {
                    if (data[(i * width + j) * 4 + 3] > 128) {
                        const color = `hsl(${Math.random() * 360}, 100%, 70%)`;
                        // 从中心点向外溅射形成形状
                        const vx = (j - width / 2) / 20 + (Math.random() - 0.5) * 2;
                        const vy = (i - height / 2) / 20 + (Math.random() - 0.5) * 2;
                        particles.push(new Particle(x, y, color, vx, vy, 0.94));
                    }
                }
            }
        }

        // 3. 流程控制
        function updateCountdown() {
            const now = Date.now();
            const diff = targetDate - now;

            if (diff <= 0 && !isCelebration) {
                startNewYear();
                return;
            }

            const d = Math.max(0, Math.floor(diff / 86400000));
            const h = Math.max(0, Math.floor((diff % 86400000) / 3600000));
            const m = Math.max(0, Math.floor((diff % 3600000) / 60000));
            const s = Math.max(0, Math.floor((diff % 60000) / 1000));

            document.getElementById('d').innerText = d.toString().padStart(2, '0');
            document.getElementById('h').innerText = h.toString().padStart(2, '0');
            document.getElementById('m').innerText = m.toString().padStart(2, '0');
            document.getElementById('s').innerText = s.toString().padStart(2, '0');

            if (Math.random() < 0.03) rockets.push(new Rocket(Math.random() * width, Math.random() * height * 0.5));
        }

        async function startNewYear() {
            isCelebration = true;
            ui.style.opacity = '0';
            ui.style.transform = 'scale(1.5) blur(20px)';
            setTimeout(() => ui.style.display = 'none', 1500);

            if (audioStarted) {
                document.getElementById('bgm').play();
                document.getElementById('sfx-firework').play();
            }

            // 跨年震撼开启:依次发射特殊形状
            rockets.push(new Rocket(width / 2, height / 2, 'horse'));
            await sleep(1500);
            rockets.push(new Rocket(width / 2, height / 2, 'sheep'));
            await sleep(1500);
            rockets.push(new Rocket(width / 2, height / 2, '2026'));
            await sleep(1500);

            document.getElementById('blessing-container').style.display = 'flex';
            loopBlessings();
        }

        function loopBlessings() {
            bDisplay.classList.remove('active');
            setTimeout(() => {
                setArtText(blessingList[blessingIdx]);
                bDisplay.classList.add('active');
                blessingIdx = (blessingIdx + 1) % blessingList.length;
                // 每3.5秒换一次词
                setTimeout(loopBlessings, 1500);
            }, 1000);
        }

        function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

        function initAudio() {
            if (audioStarted) return;
            audioStarted = true;
            document.querySelector('.hint').style.display = 'none';
            document.getElementById('bgm').play().then(() => { if (!isCelebration) document.getElementById('bgm').pause() });
        }

        // 4. 主渲染循环
        function render() {
            ctx.fillStyle = 'rgba(5, 5, 5, 0.2)';
            ctx.fillRect(0, 0, width, height);
            ctx.compositeOperation = 'lighter';

            if (!isCelebration) updateCountdown();

            rockets.forEach((r, i) => {
                r.update(); r.draw();
                if (r.exploded) rockets.splice(i, 1);
            });

            particles.forEach((p, i) => {
                p.update(); p.draw();
                if (p.alpha <= 0) particles.splice(i, 1);
            });

            if (isCelebration && Math.random() < 0.15) {
                rockets.push(new Rocket(Math.random() * width, Math.random() * height * 0.6));
            }

            requestAnimationFrame(render);
        }

        render();

        // 测试入口:如需立即查看效果可取消下行注释
        //setTimeout(startNewYear, 1000);
    </script>
</body>

</html>

🧧 2026 开发者专属祝福

时光飞逝,指针即将拨向 2026

愿你在新的马年里:

愿你的 Console 永远干净,Bug 自动退散;

愿你的发量与技术同步增长,

愿生活像 CSS 渐变一样绚丽多彩!


🐴 马年大吉,万事 return true!


创作者:灯把黑夜烧了一个洞

相关推荐
zhenryx2 小时前
React Native 横向滚动指示器组件库(淘宝|京东...&旧版|新版)
javascript·react native·react.js
铅笔侠_小龙虾2 小时前
html+css 实现键盘
前端·css·html
POLITE32 小时前
Leetcode 54.螺旋矩阵 JavaScript (Day 8)
javascript·leetcode·矩阵
licongmingli2 小时前
vue2 基于虚拟dom的下拉选择框,保证大数据不卡顿,仿antd功能和样式
大数据·前端·javascript·vue.js·anti-design-vue
小沐°2 小时前
vue3-父子组件通信
前端·javascript·vue.js
码界奇点2 小时前
基于逆向工程技术的Claude Code智能Agent系统分析与重构研究
javascript·ai·重构·毕业设计·源代码管理
树叶会结冰2 小时前
TypeScript---循环:要学会原地踏步,更要学会跳出舒适圈
前端·javascript·typescript
Zyx20073 小时前
JavaScript 中的 this:作用域陷阱与绑定策略
javascript
2501_946244783 小时前
Flutter & OpenHarmony OA系统底部导航栏组件开发指南
android·javascript·flutter