
最近又捣鼓了一个好玩的效果,这里赶紧分享给大家。
简单来说就是可以把输入的文字,像放烟花一样在空中绽放。
其实这个想法早就有过,之前也搞过一次;效果不是很好;最近请了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>









