NEMAWASHI LAB

Burst

SVG

Exponentially spaced concentric rings where each annulus is split into alternating triangles rather than rectangular sectors. The growth factor k = 1 + (angular step) is chosen so that radial height roughly equals arc width at every radius, producing near-congruent 45-45-90 triangles that shrink towards the centre in a starburst. Each annular band flips the diagonal direction based on (slice + ring) parity, giving a woven optical texture.

Burst

16 rays

Controls
Rays16
Rotation0
Colour A
Colour B

Source

Burst.tsx108 lines
"use client";

import { useId, useState } from "react";
import ScrambleLink from "../ScrambleLink";
import DraggablePanel from "../DraggablePanel";
import { PanelColor, PanelToggle } from "../_kansei/controls";

const R = 280; // viewBox radius — SVG is a 560×560 square scaled by CSS

export default function Burst() {
  const [rays, setRays] = useState(16);
  const [rotation, setRotation] = useState(0);
  const [clip, setClip] = useState(true);
  const [colorA, setColorA] = useState("#cc2010");
  const [colorB, setColorB] = useState("#f5f0e8");

  const uid = useId().replace(/:/g, "");
  const cx = R;
  const cy = R;
  const size = R * 2;
  const N = rays * 2; // angular divisions
  const step = (2 * Math.PI) / N;

  const pt = (r: number, a: number) =>
    `${(cx + Math.cos(a) * r).toFixed(2)},${(cy + Math.sin(a) * r).toFixed(2)}`;

  // Exponential ring spacing: growth k = 1 + Δθ makes radial height = arc width
  // at every radius → congruent 45-45-90 triangles.
  const k = 1 + step;
  const rMin = R * 0.018;
  const rings = Math.ceil(Math.log(R / rMin) / Math.log(k));

  const tris: React.ReactNode[] = [];
  for (let ring = 0; ring < rings; ring++) {
    const r1 = rMin * Math.pow(k, ring);
    const r2 = Math.min(rMin * Math.pow(k, ring + 1), R);
    for (let i = 0; i < N; i++) {
      const a0 = i * step;
      const a1 = (i + 1) * step;
      if (ring === 0) {
        tris.push(
          <polygon key={`${ring}-${i}`} points={`${cx},${cy} ${pt(r2, a0)} ${pt(r2, a1)}`} fill={i % 2 === 0 ? colorA : colorB} />,
        );
      } else if ((i + ring) % 2 === 0) {
        tris.push(
          <polygon key={`${ring}-${i}-a`} points={`${pt(r1, a0)} ${pt(r1, a1)} ${pt(r2, a1)}`} fill={colorA} />,
          <polygon key={`${ring}-${i}-b`} points={`${pt(r1, a0)} ${pt(r2, a1)} ${pt(r2, a0)}`} fill={colorB} />,
        );
      } else {
        tris.push(
          <polygon key={`${ring}-${i}-a`} points={`${pt(r1, a0)} ${pt(r1, a1)} ${pt(r2, a0)}`} fill={colorB} />,
          <polygon key={`${ring}-${i}-b`} points={`${pt(r1, a1)} ${pt(r2, a1)} ${pt(r2, a0)}`} fill={colorA} />,
        );
      }
    }
  }

  const clipId = `burst-clip-${uid}`;

  return (
    <div className="fixed inset-0 flex items-center justify-center select-none" style={{ background: colorB }}>
      <svg viewBox={`0 0 ${size} ${size}`} className="w-[86vmin] h-[86vmin] z-[1]" xmlns="http://www.w3.org/2000/svg">
        {clip && (
          <defs>
            <clipPath id={clipId}>
              <circle cx={cx} cy={cy} r={R} />
            </clipPath>
          </defs>
        )}
        <rect width={size} height={size} fill={colorB} />
        <g transform={`rotate(${rotation}, ${cx}, ${cy})`} clipPath={clip ? `url(#${clipId})` : undefined}>
          {tris}
        </g>
      </svg>

      <div
        className="fixed bottom-8 left-8 pointer-events-none z-10 font-[family-name:var(--font-flux)]"
        style={{ fontVariationSettings: "'wght' 300, 'SRIF' 100" }}
      >
        <p className="text-[12px] tracking-[0.08em] uppercase text-foreground/50">Burst</p>
        <p className="text-[12px] tracking-[0.08em] mt-1 text-foreground/30">{rays} rays</p>
      </div>

      <DraggablePanel
        isLight={false}
        controls={[
          { label: "Rays", value: rays, set: (v) => setRays(Math.round(v)), min: 4, max: 40, step: 1 },
          { label: "Rotation", value: rotation, set: setRotation, min: 0, max: 360, step: 1 },
        ]}
      >
        <PanelColor label="Colour A" value={colorA} set={setColorA} isLight={false} />
        <PanelColor label="Colour B" value={colorB} set={setColorB} isLight={false} />
        <PanelToggle label="Clip circle" value={clip} set={setClip} isLight={false} />
      </DraggablePanel>

      <div className="fixed top-8 left-8 z-10">
        <ScrambleLink
          from="NEMAWASHI LAB"
          to="← BACK"
          href="/lab"
          className="text-[14px] tracking-[0.08em] uppercase pointer-events-auto font-[family-name:var(--font-flux)] text-foreground/60"
          style={{ fontVariationSettings: "'wght' 600, 'SRIF' 400" }}
        />
      </div>
    </div>
  );
}

NEMAWASHI — Kotaro Abe