HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Particle Text</title>
<style>
</style>
</head>
<body>
<div id="container-1"></div>
<div id="container-2"></div>
<script >
/**
* 单个粒子类
* 负责记录自身位置、颜色、速度及绘制逻辑
*/
class Particle {
constructor(effect, x, y) {
this.effect = effect;
// 目标位置 (文字组成后的位置)
this.targetX = x;
this.targetY = y;
// 初始分散位置 (随机分布在画布内)
this.x = Math.random() * this.effect.width;
this.y = Math.random() * this.effect.height;
// 粒子基础属性
// 速度系数:根据外部配置调整初始飞行速度
const speedMulti = this.effect.options.speed || 1;
this.vx = (Math.random() - 0.5) * 1.5 * speedMulti;
this.vy = (Math.random() - 0.5) * 1.5 * speedMulti;
this.size = this.effect.options.particleSize || 2;
// 组装后的抖动效果参数
this.angle = Math.random() * Math.PI * 2;
this.angleSpeed = 0.05 + Math.random() * 0.05;
// 颜色选择
const colors = this.effect.options.colors || ["#FFFFFF"];
this.color = colors[Math.floor(Math.random() * colors.length)];
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
update() {
if (this.effect.isHovering) {
// --- 组装状态:飞向目标文字位置 ---
const dx = this.targetX - this.x;
const dy = this.targetY - this.y;
// 缓动系数:速度越快,ease 越大
const baseEase = 0.08;
const speedMulti = this.effect.options.speed || 1;
const ease = baseEase * speedMulti;
this.x += dx * ease;
this.y += dy * ease;
// 接近目标时增加微小的抖动,保持"活跃"感
const distSq = dx * dx + dy * dy;
if (distSq < 100) {
this.angle += this.angleSpeed;
this.x += Math.sin(this.angle) * 0.5;
this.y += Math.cos(this.angle) * 0.5;
}
} else {
// --- 分散状态:自由漂浮 ---
this.x += this.vx;
this.y += this.vy;
// 边界反弹
if (this.x < 0 || this.x > this.effect.width) this.vx *= -1;
if (this.y < 0 || this.y > this.effect.height) this.vy *= -1;
}
}
}
/**
* 粒子文字特效控制器类
* 负责管理 Canvas、事件监听、动画循环
*/
class ParticleTextEffect {
constructor(container, options = {}) {
this.container = container;
// 默认配置与用户配置合并
this.options = {
text: "TEXT",
colors: ["#ffffff"],
speed: 1, // 移动速度系数 (0.5 - 3)
particleSize: 2, // 粒子半径
particleGap: 5, // 采样间隔:越小粒子越多 (3 - 10)
fontFamily: "Verdana, sans-serif",
bgOverlay: "rgba(0, 0, 0, 0.2)", // 尾迹效果强度
...options,
};
this.canvas = document.createElement("canvas");
this.ctx = this.canvas.getContext("2d");
this.particles = [];
this.isHovering = false;
this.animationId = null;
this.resizeObserver = null;
this.width = 0;
this.height = 0;
this._initDOM();
this._initEvents();
// 启动
this.resize();
this.animate();
}
// 初始化 DOM 结构
_initDOM() {
this.container.style.position = "relative";
this.container.style.overflow = "hidden";
this.canvas.style.display = "block";
this.canvas.style.width = "100%";
this.canvas.style.height = "100%";
this.container.appendChild(this.canvas);
// 添加提示标签 (可选)
const label = document.createElement("div");
label.innerText = "点击或移动鼠标至上方";
Object.assign(label.style, {
position: "absolute",
bottom: "10px",
left: "50%",
transform: "translateX(-50%)",
color: "rgba(255,255,255,0.3)",
fontFamily: "sans-serif",
fontSize: "12px",
pointerEvents: "none",
userSelect: "none",
});
this.container.appendChild(label);
}
// 初始化事件监听
_initEvents() {
// 鼠标事件
this.canvas.addEventListener(
"mouseenter",
() => (this.isHovering = true)
);
this.canvas.addEventListener(
"mouseleave",
() => (this.isHovering = false)
);
// 触摸事件
this.canvas.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
this.isHovering = true;
},
{ passive: false }
);
this.canvas.addEventListener(
"touchend",
() => (this.isHovering = false)
);
// 监听容器大小变化 (替代 window resize)
this.resizeObserver = new ResizeObserver(() => {
// 防抖处理
if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => this.resize(), 100);
});
this.resizeObserver.observe(this.container);
}
// 扫描文字像素并创建粒子
_createParticles() {
this.particles = [];
// 1. 创建离屏 Canvas 绘制文字
const offCanvas = document.createElement("canvas");
offCanvas.width = this.width;
offCanvas.height = this.height;
const offCtx = offCanvas.getContext("2d");
if (!offCtx) return;
offCtx.fillStyle = "white";
// 动态字体大小:根据容器宽度调整
const fontSize = Math.min(
this.width / (this.options.text.length * 0.6),
this.height / 2
);
offCtx.font = `900 ${fontSize}px ${this.options.fontFamily}`;
offCtx.textAlign = "center";
offCtx.textBaseline = "middle";
offCtx.fillText(this.options.text, this.width / 2, this.height / 2);
// 2. 获取像素数据
const imageData = offCtx.getImageData(0, 0, this.width, this.height);
const gap = this.options.particleGap; // 采样间隔
// 3. 遍历像素
for (let y = 0; y < this.height; y += gap) {
for (let x = 0; x < this.width; x += gap) {
// Alpha 通道索引
const index = (y * this.width + x) * 4 + 3;
const alpha = imageData.data[index];
if (alpha > 128) {
this.particles.push(new Particle(this, x, y));
}
}
}
// 如果没有生成粒子(例如容器太小或文字为空),生成一些默认粒子
if (this.particles.length === 0) {
for (let i = 0; i < 50; i++) {
this.particles.push(
new Particle(this, this.width / 2, this.height / 2)
);
}
}
}
resize() {
this.width = Math.floor(this.container.clientWidth);
this.height = Math.floor(this.container.clientHeight);
if (this.width === 0 || this.height === 0) return;
this.canvas.width = this.width;
this.canvas.height = this.height;
this._createParticles();
}
animate() {
if (!this.ctx) return;
// 绘制半透明背景层以形成拖尾效果
this.ctx.fillStyle = this.options.bgOverlay;
this.ctx.fillRect(0, 0, this.width, this.height);
this.particles.forEach((p) => {
p.update();
p.draw(this.ctx);
});
this.animationId = requestAnimationFrame(() => this.animate());
}
// 销毁实例,清理内存
destroy() {
cancelAnimationFrame(this.animationId);
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
this.container.innerHTML = ""; // 清空容器
}
}
</script>
<script>
// --- 实例 1:蓝色系,文字 "COOL",速度慢,粒子大且稀疏 ---
const box1 = document.getElementById("container-1");
const effect1 = new ParticleTextEffect(box1, {
text: "ICE",
colors: ["#A5B4FC", "#818CF8", "#6366F1", "#FFFFFF"],
speed: 0.8, // 较慢的聚合速度
particleSize: 4, // 大粒子
particleGap: 6, // 采样间隔大,粒子数量较少
});
// --- 实例 2:红色系,文字 "HOT",速度快,粒子小且密集 ---
const box2 = document.getElementById("container-2");
const effect2 = new ParticleTextEffect(box2, {
text: "FIRE",
colors: ["#FF4500", "#FFA500", "#FFD700", "#FF0000"],
speed: 2.0, // 快速聚合
particleSize: 1.5, // 小粒子
particleGap: 3, // 采样间隔小,粒子非常密集
});
</script>
</body>
</html>


发表回复