文字粒子效果

演示页面请看这里

HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>汉字像素排斥</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#000;display:flex;justify-content:center;align-items:center;font-family:monospace}
  #stage{border:1px solid #333}
</style>
</head>
<body>
<canvas id="stage"></canvas>

<script>
/* ========== 配置 ========== */
const CONFIG_TEXT            = '文于止墨';               // 要写什么汉字
const CONFIG_FONT_SIZE       = 120;               // 字体大小(像素)
const CONFIG_GRID_GAP        = 5;                 // 每个“像素块”多大(正方形)
const CONFIG_REPULS_RADIUS   = 300;               // 鼠标排斥半径
const CONFIG_REPULS_STRENGTH = 100;               // 排斥强度 0~1
const CONFIG_SPRING_BACK     = 0.001;              // 弹簧回拉系数
const CONFIG_FONT_FAMILY     = 'serif';           // 字体样式
const CONFIG_REPULS_EXP_FACTOR = 0.1;            // k 值,越大衰减越快

/* ========== 工具 ========== */
const $ = sel => document.querySelector(sel);
const stage = $('#stage');
const ctx   = stage.getContext('2d');

/* ========== 像素实体 ========== */
class PixelGlyphEntity {
  constructor(gridX, gridY) {
    this.originX = gridX * CONFIG_GRID_GAP; // 原始网格坐标
    this.originY = gridY * CONFIG_GRID_GAP;
    this.x       = this.originX;
    this.y       = this.originY;
    this.vx = this.vy = 0;                // 速度
  }
  update(mouseX, mouseY) {
  const cx = this.x + CONFIG_GRID_GAP / 2;
  const cy = this.y + CONFIG_GRID_GAP / 2;
  const dx = cx - mouseX;
  const dy = cy - mouseY;
  const dist = Math.hypot(dx, dy);

  if (dist < CONFIG_REPULS_RADIUS && dist > 0) {
    // 指数衰减:F ∝ e^{-k·d}
    const forceMag = CONFIG_REPULS_STRENGTH * Math.exp(-dist * CONFIG_REPULS_EXP_FACTOR);
    this.vx += (dx / dist) * forceMag;
    this.vy += (dy / dist) * forceMag;
  }

  // 原有弹簧与阻尼保持不变
  this.vx += (this.originX - this.x) * CONFIG_SPRING_BACK;
  this.vy += (this.originY - this.y) * CONFIG_SPRING_BACK;
  this.vx *= 0.85;
  this.vy *= 0.85;
  this.x += this.vx;
  this.y += this.vy;
}

  draw() {
    ctx.fillStyle = '#4fffa4';
    ctx.fillRect(this.x, this.y, CONFIG_GRID_GAP - 1, CONFIG_GRID_GAP - 1);
  }
}

/* ========== 主流程 ========== */
const pixelEntities = [];

function initPixelEntities() {
  // 1. 离屏 canvas 渲染文字
  const off = document.createElement('canvas');
  const offCtx = off.getContext('2d');
  off.width  = off.height = 1024;                 // 足够大即可
  offCtx.fillStyle = '#000';
  offCtx.fillRect(0, 0, off.width, off.height);
  offCtx.font = `bold ${CONFIG_FONT_SIZE}px ${CONFIG_FONT_FAMILY}`;
  offCtx.textAlign = 'center';
  offCtx.textBaseline = 'middle';
  offCtx.fillStyle = '#fff';
  offCtx.fillText(CONFIG_TEXT, off.width/2, off.height/2);

  // 2. 取像素数据
  const imgData = offCtx.getImageData(0, 0, off.width, off.height);
  const { data, width } = imgData;

  // 3. 生成实体
  for (let y = 0; y < off.height; y += CONFIG_GRID_GAP) {
    for (let x = 0; x < off.width; x += CONFIG_GRID_GAP) {
      const idx = (y * width + x) * 4;
      if (data[idx] > 128) {                    // 白色像素
        pixelEntities.push(new PixelGlyphEntity(
          Math.floor(x / CONFIG_GRID_GAP),
          Math.floor(y / CONFIG_GRID_GAP)
        ));
      }
    }
  }

  // 4. 调整舞台大小
  const cols = Math.ceil(off.width  / CONFIG_GRID_GAP);
  const rows = Math.ceil(off.height / CONFIG_GRID_GAP);
  stage.width  = cols * CONFIG_GRID_GAP;
  stage.height = rows * CONFIG_GRID_GAP;
}

/* ========== 鼠标坐标 ========== */
let mouseX = -Infinity, mouseY = -Infinity;
stage.addEventListener('mousemove', e => {
  const rect = stage.getBoundingClientRect();
  mouseX = e.clientX - rect.left;
  mouseY = e.clientY - rect.top;
});
stage.addEventListener('mouseleave', () => {
  mouseX = mouseY = -Infinity;
});

/* ========== 动画循环 ========== */
function animate() {
  ctx.clearRect(0, 0, stage.width, stage.height);
  pixelEntities.forEach(p => {
    p.update(mouseX, mouseY);
    p.draw();
  });
  requestAnimationFrame(animate);
}

initPixelEntities();
animate();
</script>
</body>
</html>

评论

发表回复

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