最近又捣鼓了一个好玩的效果,这里赶紧分享给大家。
简单来说就是可以把输入的文字,像放烟花一样在空中绽放。
其实这个想法早就有过,之前也搞过一次;效果不是很好;最近请了AI小助理,和小助理一起努努力,又搞了现在这版;感觉效果挺好的。
下面介绍下。
一、3 秒玩转:输入→发射→看烟花绽放
输入框敲入想说的话(如「某同学我爱你」),点击「确定」; 文字秒变彩色粒子,升空→爆炸→飘散,背景还有随机彩蛋烟花; 点击「分享」生成专属链接,朋友打开直接看你的定制烟花!
二、技术亮点
像素级文字拆解
每个笔画化作独立粒子,爆炸时拼成清晰字符轮廓; 三阶段动画
上升→爆炸→飘散,粒子带物理模拟效果(减速、旋转); 多设备适配
手机 / 电脑都可以看; encode处理
分享的url上做了encode对文字处理;从url上看不出啥文字; 随机元素
烟花颜色随机、位置随机。
三、怎么玩?
表白
输入情话,让 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>