Components / KATAYABURI / Particle Word

Particle Word beta

A word made of a thousand dots that scatter when you touch them — the KATAYABURI flagship.

Preview

Run the cursor through the word. It forgives you, slowly.

<div class="k-pw" data-pw data-word="KATA" role="img" aria-label="The word KATA, made of particles">
  <canvas aria-hidden="true"></canvas>
  <div class="k-pw-bar">
    <button class="k-pw-btn on" type="button" data-pw-word="KATA">KATA</button>
    <button class="k-pw-btn" type="button" data-pw-word="型破り">型破り</button>
    <button class="k-pw-btn" type="button" data-pw-word="2026">2026</button>
  </div>
</div>
<p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">Run the cursor through the word. It forgives you, slowly.</p>
/* KATA Particle Word — KATAYABURI. A word, weather included. */
.k-pw{background:var(--k-zinc-950);border-radius:14px;padding:14px;font-family:var(--k-font)}
.k-pw canvas{display:block;width:100%;height:230px}
.k-pw-bar{display:flex;gap:8px;justify-content:center;margin-top:10px}
.k-pw-btn{border:1px solid var(--k-zinc-700);background:none;color:var(--k-zinc-300);font-family:inherit;font-size:12.5px;font-weight:550;padding:6px 13px;border-radius:99px;cursor:pointer}
.k-pw-btn:hover{color:var(--k-on-color);border-color:var(--k-zinc-500)}
.k-pw-btn.on{background:var(--k-on-color);color:var(--k-zinc-950);border-color:var(--k-on-color)}
// KATA Particle Word — samples a word into points; each point gets
// a spring home and a healthy fear of the cursor.
document.querySelectorAll('[data-pw]').forEach(function (stage) {
  var canvas = stage.querySelector('canvas');
  var ctx = canvas.getContext('2d');
  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var W = 0, H = 0, parts = [], mouse = { x: -9999, y: -9999 };
  var accent = getComputedStyle(stage).getPropertyValue('--k-accent').trim() || '#0e9594';
  var paper = getComputedStyle(stage).getPropertyValue('--k-on-color').trim() || '#ffffff';
  function targets(word) {
    var off = document.createElement('canvas');
    off.width = W; off.height = H;
    var o = off.getContext('2d');
    o.fillStyle = '#fff';
    o.font = '700 ' + Math.min(150, W / Math.max(2.2, word.length * 0.62)) + 'px Inter, sans-serif';
    o.textAlign = 'center'; o.textBaseline = 'middle';
    o.fillText(word, W / 2, H / 2 + 4);
    var data = o.getImageData(0, 0, W, H).data;
    var pts = [], step = 5;
    for (var y = 0; y < H; y += step) for (var x = 0; x < W; x += step)
      if (data[(y * W + x) * 4 + 3] > 128) pts.push({ x: x, y: y });
    if (pts.length > 1500) { pts = pts.filter(function (_, i) { return i % 2 === 0; }); }
    return pts;
  }
  function seed(word) {
    var pts = targets(word);
    while (parts.length < pts.length) parts.push({ x: Math.random() * W, y: Math.random() * H, vx: 0, vy: 0, c: Math.random() < .22 ? accent : paper });
    parts.length = pts.length;
    parts.forEach(function (p, i) { p.hx = pts[i].x; p.hy = pts[i].y; if (reduced) { p.x = p.hx; p.y = p.hy; } });
  }
  function frame() {
    ctx.clearRect(0, 0, W, H);
    parts.forEach(function (p) {
      var dx = mouse.x - p.x, dy = mouse.y - p.y;
      var d2 = dx * dx + dy * dy;
      if (d2 < 4900) { var d = Math.sqrt(d2) || 1; var f = (70 - d) / 70 * 2.6; p.vx -= dx / d * f; p.vy -= dy / d * f; }
      p.vx += (p.hx - p.x) * 0.028;
      p.vy += (p.hy - p.y) * 0.028;
      p.vx *= 0.86; p.vy *= 0.86;
      p.x += p.vx; p.y += p.vy;
      ctx.fillStyle = p.c;
      ctx.fillRect(p.x, p.y, 2.2, 2.2);
    });
    if (!reduced) requestAnimationFrame(frame);
  }
  function resize() {
    W = canvas.clientWidth; H = canvas.clientHeight;
    canvas.width = W; canvas.height = H;
    seed(stage.getAttribute('data-word') || 'KATA');
    if (reduced) frame();
  }
  stage.querySelectorAll('[data-pw-word]').forEach(function (b) {
    b.addEventListener('click', function () {
      stage.querySelectorAll('[data-pw-word]').forEach(function (x) { x.classList.remove('on'); });
      b.classList.add('on');
      stage.setAttribute('data-word', b.getAttribute('data-pw-word'));
      seed(b.getAttribute('data-pw-word'));
    });
  });
  canvas.addEventListener('pointermove', function (e) {
    var r = canvas.getBoundingClientRect();
    mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top;
  });
  canvas.addEventListener('pointerleave', function () { mouse.x = -9999; mouse.y = -9999; });
  resize();
  window.addEventListener('resize', resize);
  if (!reduced) requestAnimationFrame(frame);
});

Installation

CLI

node kata-ui.mjs add particle-word

Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.

Manual

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Usage

KATAYABURI is the shelf where the mold breaks on purpose. This one samples a word into points and gives every point a spring home and a fear of cursors. Change the word with the buttons, or the data-word attribute. It proves nothing, sells nothing, and is the part people will remember — which, occasionally, is the job.

Accessibility

The word also exists as plain text for assistive technology; the canvas is aria-hidden theatre. Under prefers-reduced-motion the dots assemble instantly and stay put — the word, without the weather.

Anatomy

The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.

.k-pwclass
.k-pw-barclass
.k-pw-btnclass
.onclass
data-pwdata hook
data-worddata hook
data-pw-worddata hook
aria-labelARIA
aria-hiddenARIA

Related parts