Components / KATAYABURI / Kaleidoscope

Kaleidoscope beta

Draw once, get ten drawings: a pointer sketch mirrored into a living mandala.

Preview

It draws alone until you take the pen. Then it is your fault, ten times.

<div class="k-kal" data-kal role="img" aria-label="A kaleidoscope drawing stage">
  <canvas aria-hidden="true"></canvas>
  <div class="k-kal-bar"><button class="k-kal-btn" type="button" data-kal-clear>Clear</button></div>
</div>
<p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">It draws alone until you take the pen. Then it is your fault, ten times.</p>
/* KATA Kaleidoscope — KATAYABURI. Draw once, get ten. */
.k-kal{background:var(--k-zinc-950);border-radius:14px;padding:12px;font-family:var(--k-font)}
.k-kal canvas{display:block;width:100%;height:262px;border-radius:8px;touch-action:none;cursor:crosshair}
.k-kal-bar{display:flex;justify-content:center;margin-top:10px}
.k-kal-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 14px;border-radius:99px;cursor:pointer}
.k-kal-btn:hover{color:var(--k-on-color);border-color:var(--k-zinc-500)}
// KATA Kaleidoscope — one stroke, ten reflections.
document.querySelectorAll('[data-kal]').forEach(function (stage) {
  var canvas = stage.querySelector('canvas');
  var ctx = canvas.getContext('2d');
  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var SEG = 10, W, H, cx, cy, drawing = false, prev = null, auto = !reduced, t = 0;
  var paper = getComputedStyle(stage).getPropertyValue('--k-on-color').trim() || '#ffffff';
  var accent = getComputedStyle(stage).getPropertyValue('--k-accent').trim() || '#0e9594';
  function size() {
    W = canvas.clientWidth; H = canvas.clientHeight;
    canvas.width = W; canvas.height = H;
    cx = W / 2; cy = H / 2;
    ctx.fillStyle = '#09090b'; ctx.fillRect(0, 0, W, H);
  }
  function stroke(a, b, color) {
    ctx.strokeStyle = color; ctx.lineWidth = 2.4; ctx.lineCap = 'round';
    for (var i = 0; i < SEG; i++) {
      var ang = (Math.PI * 2 / SEG) * i;
      var flip = i % 2 === 1;
      [false, true].forEach(function (mirror) {
        var s = (flip ? -1 : 1) * (mirror ? -1 : 1);
        ctx.beginPath();
        ctx.moveTo(cx + rot(a, ang, s).x, cy + rot(a, ang, s).y);
        ctx.lineTo(cx + rot(b, ang, s).x, cy + rot(b, ang, s).y);
        ctx.stroke();
      });
    }
  }
  function rot(p, ang, sx) {
    var dx = (p.x - cx) * sx, dy = p.y - cy;
    return { x: dx * Math.cos(ang) - dy * Math.sin(ang), y: dx * Math.sin(ang) + dy * Math.cos(ang) };
  }
  function autoPen() {
    if (!auto) return;
    t += .016;
    var p = { x: cx + Math.sin(t * 1.9) * W * .3, y: cy + Math.sin(t * 2.7) * H * .32 };
    if (prev) stroke(prev, p, Math.floor(t * 2) % 2 ? paper : accent);
    prev = p;
    requestAnimationFrame(autoPen);
  }
  function pos(e) { var r = canvas.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; }
  canvas.addEventListener('pointerdown', function (e) { auto = false; drawing = true; prev = pos(e); canvas.setPointerCapture(e.pointerId); });
  canvas.addEventListener('pointermove', function (e) { if (!drawing) return; var p = pos(e); stroke(prev, p, paper); prev = p; });
  canvas.addEventListener('pointerup', function () { drawing = false; });
  stage.querySelector('[data-kal-clear]').addEventListener('click', function () { ctx.fillStyle = '#09090b'; ctx.fillRect(0, 0, W, H); });
  size();
  window.addEventListener('resize', size);
  if (!reduced) requestAnimationFrame(autoPen);
});

Installation

CLI

node kata-ui.mjs add kaleidoscope

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

The stage draws by itself until you touch it — a slow lissajous pen — then your strokes take over, mirrored across ten segments. The Clear button is the only adult in the room. Screenshots of it make excellent album covers and terrible documentation.

Accessibility

A drawing toy: the canvas carries a plain label, and nothing on any sane page may depend on what gets drawn. Under prefers-reduced-motion the pen rests and the stage waits, blank and patient, for a hand.

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-kalclass
.k-kal-barclass
.k-kal-btnclass
data-kaldata hook
data-kal-cleardata hook
aria-labelARIA
aria-hiddenARIA

Related parts