Components / KATAYABURI / Gravity Playground

Gravity Playground beta

The catalog, dropped: chips of parts fall, bounce, pile and can be thrown across the stage.

Preview

Grab a chip and throw it. The pile is not load-bearing.

<div class="k-grav" data-grav role="img" aria-label="Chips named after catalog parts, falling and piling under gravity">
  <span class="k-grav-chip">Button</span><span class="k-grav-chip">Modal</span><span class="k-grav-chip">Toast</span><span class="k-grav-chip hot">QR Code</span><span class="k-grav-chip">Sidebar</span><span class="k-grav-chip hot">Dock</span><span class="k-grav-chip">Calendar</span><span class="k-grav-chip">Meteor</span><span class="k-grav-chip">Kanban</span><span class="k-grav-chip">Aurora</span>
</div>
<p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">Grab a chip and throw it. The pile is not load-bearing.</p>
/* KATA Gravity Playground — KATAYABURI. The catalog, dropped. */
.k-grav{position:relative;width:100%;background:var(--k-zinc-950);border-radius:14px;overflow:hidden;height:264px;font-family:var(--k-font);touch-action:none}
.k-grav-chip{position:absolute;left:0;top:0;padding:8px 14px;border-radius:99px;background:var(--k-zinc-800);color:var(--k-on-color);font-size:13px;font-weight:600;white-space:nowrap;cursor:grab;border:1px solid var(--k-zinc-700);user-select:none;-webkit-user-select:none}
.k-grav-chip.hot{background:var(--k-accent);border-color:var(--k-accent)}
.k-grav-chip:active{cursor:grabbing}
// KATA Gravity Playground — circles in chip clothing; grab and fling.
document.querySelectorAll('[data-grav]').forEach(function (stage) {
  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var bodies = [];
  stage.querySelectorAll('.k-grav-chip').forEach(function (el, i) {
    var r = Math.max(el.offsetWidth, el.offsetHeight) / 2;
    bodies.push({ el: el, x: 30 + (i * 67) % Math.max(120, stage.clientWidth - 90), y: 16 + (i % 4) * 34, vx: 0, vy: 0, r: r, held: false });
  });
  var drag = null, last = null;
  bodies.forEach(function (b) {
    b.el.style.left = '0px'; b.el.style.top = '0px';
    b.el.addEventListener('pointerdown', function (e) {
      drag = b; last = { x: e.clientX, y: e.clientY, t: performance.now() };
      b.held = true; b.vx = 0; b.vy = 0;
      b.el.setPointerCapture(e.pointerId);
    });
    b.el.addEventListener('pointermove', function (e) {
      if (drag !== b) return;
      var now = performance.now();
      var rect = stage.getBoundingClientRect();
      b.x = e.clientX - rect.left; b.y = e.clientY - rect.top;
      var dt = Math.max(1, now - last.t);
      b.vx = (e.clientX - last.x) / dt * 16; b.vy = (e.clientY - last.y) / dt * 16;
      last = { x: e.clientX, y: e.clientY, t: now };
    });
    function release() { if (drag === b) { drag = null; b.held = false; } }
    b.el.addEventListener('pointerup', release);
    b.el.addEventListener('pointercancel', release);
  });
  function tick() {
    var W = stage.clientWidth, H = stage.clientHeight;
    bodies.forEach(function (b) {
      if (!b.held) {
        b.vy += .45;
        b.x += b.vx; b.y += b.vy;
        b.vx *= .999; b.vy *= .999;
        if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx) * .5; }
        if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx) * .5; }
        if (b.y < b.el.offsetHeight / 2) { b.y = b.el.offsetHeight / 2; b.vy = Math.abs(b.vy) * .5; }
        if (b.y > H - b.el.offsetHeight / 2) { b.y = H - b.el.offsetHeight / 2; b.vy = -Math.abs(b.vy) * .42; b.vx *= .92; if (Math.abs(b.vy) < .6) b.vy = 0; }
      }
      b.el.style.transform = 'translate(' + (b.x - b.el.offsetWidth / 2) + 'px,' + (b.y - b.el.offsetHeight / 2) + 'px)';
    });
    for (var i = 0; i < bodies.length; i++) for (var j = i + 1; j < bodies.length; j++) {
      var a = bodies[i], c = bodies[j];
      var dx = c.x - a.x, dy = c.y - a.y;
      var rr = (a.el.offsetWidth + c.el.offsetWidth) / 4.4;
      var d2 = dx * dx + dy * dy;
      if (d2 > 0 && d2 < rr * rr) {
        var d = Math.sqrt(d2), push = (rr - d) / 2;
        var nx = dx / d, ny = dy / d;
        if (!a.held) { a.x -= nx * push; a.y -= ny * push; }
        if (!c.held) { c.x += nx * push; c.y += ny * push; }
      }
    }
    requestAnimationFrame(tick);
  }
  if (reduced) {
    bodies.forEach(function (b, i) { b.x = 40 + i * 62; b.y = stage.clientHeight - 24; b.el.style.transform = 'translate(' + (b.x - b.el.offsetWidth / 2) + 'px,' + (b.y - b.el.offsetHeight / 2) + 'px)'; });
  } else { requestAnimationFrame(tick); }
});

Installation

CLI

node kata-ui.mjs add gravity-playground

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

Every chip is a part name with a small physics body: gravity, wall bounces, cheap collisions, and a pointer that can grab and fling. It is a toy — the kind a conference booth runs on a big screen. Put it anywhere the brand can afford to be caught playing.

Accessibility

A toy, honestly labeled: the chips are inert text to assistive technology (the same names exist in the Components index, in order). Under prefers-reduced-motion the chips rest in a neat pile at the floor — gravity, but polite.

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-gravclass
.k-grav-chipclass
.hotclass
data-gravdata hook
aria-labelARIA

Related parts