JavaScript+canvas实现一个文字烟花的效果

www.jswusn.com JS 2025-05-29 11:33:07 5次浏览

  最近又捣鼓了一个好玩的效果,这里赶紧分享给大家。

  简单来说就是可以把输入的文字,像放烟花一样在空中绽放。

  其实这个想法早就有过,之前也搞过一次;效果不是很好;最近请了AI小助理,和小助理一起努努力,又搞了现在这版;感觉效果挺好的。

  下面介绍下。


一、3 秒玩转:输入→发射→看烟花绽放

  • 输入框敲入想说的话(如「某同学我爱你」),点击「确定」;
  • 文字秒变彩色粒子,升空→爆炸→飘散,背景还有随机彩蛋烟花;
  • 点击「分享」生成专属链接,朋友打开直接看你的定制烟花!


二、技术亮点

  1. 像素级文字拆解

    每个笔画化作独立粒子,爆炸时拼成清晰字符轮廓;
  2. 三阶段动画

    上升→爆炸→飘散,粒子带物理模拟效果(减速、旋转);
  3. 多设备适配

    手机 / 电脑都可以看;
  4. encode处理

    分享的url上做了encode对文字处理;从url上看不出啥文字;
  5. 随机元素

    烟花颜色随机、位置随机。

三、怎么玩?

  • 表白

    输入情话,让 TA 见证文字炸成星空;
  • 节日祝福

    春节发「恭喜发财」,朋友生日发「xxx生日快乐」,特效随文字变色;

四、下面老规矩放源码;直接复制去玩。

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>文字烟花</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <style>
        html,
        body {
            margin: 0;
            padding: 0;
            width: 100vw;
            height: 100vh;
            overflow: hidden;
            background: #000;
            font-size: 16px;
            -webkit-tap-highlight-color: transparent;
        }

        #ui {
            position: absolute;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            z-index: 10;
            background: rgba(20, 24, 40, 0.85);
            padding: 10px 8vw 18px 8vw;
            border-radius: 24px 24px 0 0;
            box-shadow: 0 -2px 24px #000a;
            color: #fff;
            font-size: 1rem;
            display: flex;
            flex-direction: column;
            gap: 8px;
            align-items: center;
            border: 1.5px solid rgba(80, 120, 255, 0.18);
            backdrop-filter: blur(2px);
            width: 100vw;
            max-width: 280px;
            min-width: 0;
        }

        #textInput {
            font-size: 1.1rem;
            padding: 10px 16px;
            border-radius: 18px;
            border: 1.5px solid #3a4a7a;
            outline: none;
            background: rgba(30, 40, 70, 0.85);
            color: #fff;
            transition: border 0.2s, box-shadow 0.2s;
            box-shadow: 0 2px 8px #0004 inset;
            width: 64vw;
            max-width: 340px;
            min-width: 0;
        }

        #launchBtn,
        #shareBtn {
            font-size: 1.1rem;
            padding: 10px 0;
            border-radius: 18px;
            border: none;
            background: linear-gradient(90deg, #3a8dde 0%, #7ecfff 100%);
            color: #fff;
            cursor: pointer;
            font-weight: bold;
            letter-spacing: 2px;
            box-shadow: 0 2px 12px #3a8dde44;
            transition: background 0.2s, box-shadow 0.2s;
            width: 38vw;
            max-width: 150px;
            min-width: 80px;
            margin: 0 2vw;
        }

        #launchBtn:hover,
        #shareBtn:hover {
            background: linear-gradient(90deg, #7ecfff 0%, #3a8dde 100%);
            box-shadow: 0 4px 18px #7ecfff66;
        }

        #canvas {
            display: block;
            width: 100vw;
            height: 100vh;
            touch-action: none;
        }

        @media (max-width: 600px) {
            #ui {
                padding: 8px 2vw 12px 2vw;
                max-width: 100vw;
                border-radius: 18px 18px 0 0;
            }

            #textInput {
                font-size: 1rem;
                padding: 8px 8px;
                width: 80vw;
                max-width: 98vw;
            }

            #launchBtn,
            #shareBtn {
                font-size: 1rem;
                padding: 8px 0;
                width: 38vw;
                max-width: 120px;
                min-width: 60px;
            }
        }
    </style>
</head>

<body>
<div id="ui">
    <input id="textInput" type="text" placeholder="输入你想要的文字" maxlength="100" style="font-size:18px;padding:4px 8px;" />
    <div style="display:flex;gap:12px;justify-content:center;margin-top:6px;">
        <button id="launchBtn" style="font-size:18px;">确定</button>
        <button id="shareBtn" style="font-size:18px;background:linear-gradient(90deg,#3a8dde 0%,#7ecfff 100%);color:#fff;cursor:pointer;font-weight:bold;letter-spacing:2px;box-shadow:0 2px 12px #3a8dde44;border:none;padding:8px 28px;border-radius:18px;transition:background 0.2s,box-shadow 0.2s;">分享</button>
    </div>
</div>
<canvas id="canvas"></canvas>
<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    let W = window.innerWidth, H = window.innerHeight;
    function resize() {
        W = window.innerWidth;
        H = window.innerHeight;
        canvas.width = W;
        canvas.height = H;
    }
    window.addEventListener('resize', resize);
    resize();
    // 粒子类
    class Particle {
        constructor(x, y, color, vx, vy, size, alpha, fade, gravity = 0.02) {
            this.x = x;
            this.y = y;
            this.color = color;
            this.vx = vx;
            this.vy = vy;
            this.size = size;
            this.alpha = alpha;
            this.fade = fade;
            this.gravity = gravity;
        }
        update() {
            this.x += this.vx;
            this.y += this.vy;
            this.vy += this.gravity;
            this.alpha -= this.fade;
        }
        draw(ctx) {
            ctx.save();
            ctx.globalAlpha = Math.max(this.alpha, 0);
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
            ctx.fillStyle = this.color;
            ctx.fill();
            ctx.restore();
        }
        isAlive() {
            return this.alpha > 0;
        }
    }

    // 烟花上升类
    class AscendFirework {
        constructor(sx, sy, tx, ty, color, textChar) {
            this.x = sx;
            this.y = sy;
            this.tx = tx;
            this.ty = ty;
            this.color = color;
            this.textChar = textChar;
            this.vx = (tx - sx) / 36;
            this.vy = (ty - sy) / 36;
            this.age = 0;
            this.exploded = false;
        }
        update() {
            if (!this.exploded) {
                this.x += this.vx;
                this.y += this.vy;
                this.age++;
                if ((Math.abs(this.x - this.tx) < 2 && Math.abs(this.y - this.ty) < 2) || this.age > 40) {
                    this.exploded = true;
                    return true;
                }
            }
            return false;
        }
        draw(ctx) {
            if (!this.exploded) {
                ctx.save();
                ctx.beginPath();
                ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
                ctx.fillStyle = this.color;
                ctx.shadowColor = this.color;
                ctx.shadowBlur = 12;
                ctx.fill();
                ctx.restore();
            }
        }
    }

    // 文字烟花粒子
    function createTextParticles(text, x, y, color) {
        const offCanvas = document.createElement('canvas');
        const offCtx = offCanvas.getContext('2d');
        const fontSize = 144;
        offCanvas.width = fontSize * text.length;
        offCanvas.height = fontSize * 1.2;
        offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
        offCtx.font = `bold ${fontSize}px cursive`;
        offCtx.textAlign = 'center';
        offCtx.textBaseline = 'middle';
        offCtx.fillStyle = '#fff';
        offCtx.fillText(text, offCanvas.width / 2, offCanvas.height / 2);
        const imageData = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height);
        const particles = [];
        for (let i = 0; i < imageData.width; i += 6) {
            // 步长由4改为6,减少粒子
            for (let j = 0; j < imageData.height; j += 6) {
                const idx = (j * imageData.width + i) * 4;
                if (imageData.data[idx + 3] > 128) {
                    const tx = x - offCanvas.width / 2 + i;
                    const ty = y - offCanvas.height / 2 + j;
                    const sx = x;
                    const sy = y;
                    const duration = 32;
                    const vx = (tx - sx) / duration;
                    const vy = (ty - sy) / duration;
                    const p = new Particle(sx, sy, color, vx, vy, 2.2, 1, 0.012 + Math.random() * 0.01, 0);
                    p.tx = tx;
                    p.ty = ty;
                    p.explodeFrame = 0;
                    p.state = 'explode';
                    p.holdFrame = 0;
                    p.disperseFrame = 0;
                    particles.push(p);
                }
            }
        }
        return particles;
    }

    // 烟花爆炸粒子
    function createFirework(x, y, color, count = 40, speed = 4, size = 2) {
        const particles = [];
        for (let i = 0; i < count; i++) {
            const angle = (Math.PI * 2) * (i / count);
            const vx = Math.cos(angle) * (Math.random() * speed * 0.7 + speed * 0.3);
            const vy = Math.sin(angle) * (Math.random() * speed * 0.7 + speed * 0.3);
            particles.push(new Particle(x, y, color, vx, vy, size, 1, 0.015 + Math.random() * 0.01));
        }
        return particles;
    }

    // 随机颜色
    function randomColor() {
        const colors = ['#ff5252', '#ffd740', '#40c4ff', '#69f0ae', '#fff176', '#b388ff', '#ff80ab', '#fff'];
        return colors[Math.floor(Math.random() * colors.length)];
    }

    // 背景烟花
    let bgFireworks = [];
    let bgAscendFireworks = [];
    function launchBgFirework() {
        const tx = Math.random() * W * 0.9 + W * 0.05;
        const ty = Math.random() * H * 0.4 + H * 0.1;
        const sx = tx + (Math.random() - 0.5) * 80;
        const sy = H;
        const color = randomColor();
        bgAscendFireworks.push(new AscendFirework(sx, sy, tx, ty, color, null));
    }
    setInterval(() => {
        if (bgAscendFireworks.length < 8)
            launchBgFirework();
    }, 700);

    // 文字烟花流程
    let textQueue = [];
    let textParticles = [];
    let isTextFirework = false;
    let textCurrentChar = '';
    let textCurrentColor = '';
    let textLoopStr = '';
    let textAnimating = false;
    let textAscendFireworks = [];
    function launchTextFirework(str) {
        textLoopStr = str;
        textQueue = str.split('');
        textParticles = [];
        isTextFirework = true;
        textCurrentChar = '';
        textCurrentColor = '';
        textAnimating = false;
        textAscendFireworks = [];
        nextTextFirework();
    }
    function nextTextFirework() {
        if (textQueue.length === 0) {
            textAnimating = true;
            return;
        }
        textCurrentChar = textQueue.shift();
        // 目标位置在上下70%-80%和左右33%-66%区域内随机
        const x = W * (0.33 + 0.33 * Math.random());
        const y = H * (0.1 + 0.1 * Math.random());
        textCurrentColor = randomColor();
        const sx = x + (Math.random() - 0.5) * 80;
        const sy = H;
        textAscendFireworks.push(new AscendFirework(sx, sy, x, y, textCurrentColor, textCurrentChar));
    }

    // 在动画主循环中增加粒子拖影效果
    function animate() {
        // 拖影效果:用半透明黑色覆盖
        ctx.globalAlpha = 0.22;
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, W, H);
        ctx.globalAlpha = 1;
        // 背景烟花上升
        for (let i = bgAscendFireworks.length - 1; i >= 0; i--) {
            const fw = bgAscendFireworks[i];
            fw.draw(ctx);
            if (fw.update()) {
                bgFireworks.push(...createFirework(fw.x, fw.y, fw.color, 30 + Math.random() * 20, 2.5 + Math.random() * 1.5, 1.5 + Math.random()));
                bgAscendFireworks.splice(i, 1);
            }
        }
        // 绘制背景烟花爆炸
        bgFireworks = bgFireworks.filter(p => p.isAlive());
        for (const p of bgFireworks) {
            p.update();
            p.draw(ctx);
        }
        // 文字烟花上升
        for (let i = textAscendFireworks.length - 1; i >= 0; i--) {
            const fw = textAscendFireworks[i];
            fw.draw(ctx);
            if (fw.update()) {
                textParticles.push(...createTextParticles(fw.textChar, fw.x, fw.y, fw.color));
                textAscendFireworks.splice(i, 1);
                setTimeout(nextTextFirework, 300);
            }
        }
        // 文字烟花动画
        textParticles = textParticles.filter(p => p.isAlive());
        let allDisappear = true;
        for (const p of textParticles) {
            if (p.tx !== undefined && p.ty !== undefined) {
                if (p.state === 'explode') {
                    // 爆炸阶段粒子带有更高透明度拖影
                    ctx.save();
                    ctx.globalAlpha = 0.7;
                    p.x += p.vx;
                    p.y += p.vy;
                    p.explodeFrame++;
                    p.draw(ctx);
                    ctx.restore();
                    if (p.explodeFrame >= 32) {
                        p.x = p.tx;
                        p.y = p.ty;
                        p.state = 'hold';
                    } else {
                        allDisappear = false;
                    }
                } else if (p.state === 'hold') {
                    p.holdFrame++;
                    p.alpha = 1;
                    p.draw(ctx);
                    if (p.holdFrame > 38) {
                        const angle = Math.random() * Math.PI * 2;
                        const speed = 3 + Math.random() * 2.5;
                        p.vx = Math.cos(angle) * speed;
                        p.vy = Math.sin(angle) * speed;
                        p.state = 'disperse';
                    } else {
                        allDisappear = false;
                    }
                } else if (p.state === 'disperse') {
                    p.x += p.vx;
                    p.y += p.vy;
                    p.vx *= 0.96;
                    p.vy *= 0.96;
                    p.alpha *= 0.94;
                    p.disperseFrame++;
                    p.draw(ctx);
                    if (p.alpha > 0.05) allDisappear = false;
                }
            } else {
                p.update();
                p.draw(ctx);
                allDisappear = false;
            }
        }
        // 所有字动画完全消失后再自动循环
        if (isTextFirework && textAnimating && allDisappear) {
            setTimeout(() => {
                if (textLoopStr) {
                    textQueue = textLoopStr.split('');
                    textParticles = [];
                    isTextFirework = true;
                    textCurrentChar = '';
                    textCurrentColor = '';
                    textAnimating = false;
                    textAscendFireworks = [];
                    nextTextFirework();
                }
            }, 1200);
            isTextFirework = false;
            textAnimating = false;
        }
        requestAnimationFrame(animate);
    }
    animate();

    // 简单base64加密
    function encodeText(str) {
        return btoa(unescape(encodeURIComponent(str)));
    }

    function decodeText(str) {
        try {
            return decodeURIComponent(escape(atob(str)));
        } catch(e) {
            return '';
        }
    }

    // 交互
    const input = document.getElementById('textInput');
    const btn = document.getElementById('launchBtn');
    const shareBtn = document.getElementById('shareBtn');
    btn.onclick = () => {
        const val = input.value.trim();
        if (!val) return;
        launchTextFirework(val);
        // 更新URL参数(加密)
        const url = new URL(window.location.href);
        url.searchParams.set('text', encodeText(val));
        window.history.replaceState(null, '', url.toString());
    };
    input.addEventListener('keydown', e => {
        if (e.key === 'Enter') btn.click();
    });
    shareBtn.onclick = () => {
        const val = input.value.trim();
        const url = new URL(window.location.href);
        if (val) {
            url.searchParams.set('text', encodeText(val));
        } else {
            url.searchParams.delete('text');
        }
        navigator.clipboard.writeText(url.toString()).then(() => {
            shareBtn.textContent = '已复制!';
            setTimeout(()=>{shareBtn.textContent='分享';},1200);
        });
    };
    // 页面加载时自动播放默认或URL参数文字(解密)
    const urlTextRaw = new URLSearchParams(window.location.search).get('text');
    let urlText = '';
    if (urlTextRaw) {
        urlText = decodeText(urlTextRaw);
    }
    if (urlText) {
        input.value = urlText;
        launchTextFirework(urlText);
    } else {
        launchTextFirework('苏州网站建设我爱你');
    }
    
</script>
</body>

</html>


上一篇:没有了!

JS

下一篇:原来可以这样写JavaScript!ES2025新语法糖

技术分享

苏南名片

  • 联系人:吴经理
  • 电话:152-1887-1916
  • 邮箱:message@jswusn.com
  • 地址:江苏省苏州市相城区

热门文章

Copyright © 2018-2025 jswusn.com 版权所有

技术支持:苏州网站建设  苏ICP备18036849号