粒子动态聚集组成文字

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>

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注