NEMAWASHI LAB

Radial

SVG

Alternating-colour pie wedges drawn as SVG polygons on concentric rings, with the fill checkerboarded by (slice + ring) % 2. A seeded-sine jitter function nudges each spoke angle proportionally to the slider value, breaking the symmetry without randomising it. Rays, rings, rotation, jitter amount, and both colours are all panel-tuneable; an optional circular clip-path keeps the disc clean.

Radial

24 rays · 3 rings

Controls
Rays24
Rings3
Rotation0
Jitter0.0
Colour A
Colour B

Source

Radial.tsx131 lines
"use client";

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

// Deterministic per-cell jitter (ported from kansei page.tsx seededJitter).
function seededJitter(seed: number): number {
  const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
  return x - Math.floor(x);
}

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

export default function Radial() {
  const [rays, setRays] = useState(24);
  const [rings, setRings] = useState(3);
  const [rotation, setRotation] = useState(0);
  const [jitter, setJitter] = 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 totalSlices = rays * 2;
  const baseAngle = (2 * Math.PI) / totalSlices;

  const polys: React.ReactNode[] = [];
  for (let ring = 0; ring < rings; ring++) {
    const innerR = (R / rings) * ring;
    const outerR = (R / rings) * (ring + 1);
    for (let i = 0; i < totalSlices; i++) {
      const jit =
        jitter > 0 ? (seededJitter(i * 17 + ring * 31) - 0.5) * baseAngle * (jitter / 20) : 0;
      const a1 = i * baseAngle + jit;
      const a2 =
        (i + 1) * baseAngle +
        (jitter > 0 ? (seededJitter((i + 1) * 17 + ring * 31) - 0.5) * baseAngle * (jitter / 20) : 0);
      const fill = (i + ring) % 2 === 0 ? colorA : colorB;
      const x1 = cx + Math.cos(a1) * innerR;
      const y1 = cy + Math.sin(a1) * innerR;
      const x2 = cx + Math.cos(a2) * innerR;
      const y2 = cy + Math.sin(a2) * innerR;
      const x3 = cx + Math.cos(a2) * outerR;
      const y3 = cy + Math.sin(a2) * outerR;
      const x4 = cx + Math.cos(a1) * outerR;
      const y4 = cy + Math.sin(a1) * outerR;
      polys.push(
        ring === 0 ? (
          <polygon
            key={`${ring}-${i}`}
            points={`${cx},${cy} ${x3.toFixed(2)},${y3.toFixed(2)} ${x4.toFixed(2)},${y4.toFixed(2)}`}
            fill={fill}
          />
        ) : (
          <polygon
            key={`${ring}-${i}`}
            points={`${x1.toFixed(2)},${y1.toFixed(2)} ${x2.toFixed(2)},${y2.toFixed(2)} ${x3.toFixed(2)},${y3.toFixed(2)} ${x4.toFixed(2)},${y4.toFixed(2)}`}
            fill={fill}
          />
        ),
      );
    }
  }

  const clipId = `radial-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}>
          {polys}
        </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">Radial</p>
        <p className="text-[12px] tracking-[0.08em] mt-1 text-foreground/30">
          {rays} rays · {rings} rings
        </p>
      </div>

      <DraggablePanel
        isLight={false}
        controls={[
          { label: "Rays", value: rays, set: (v) => setRays(Math.round(v)), min: 3, max: 60, step: 1 },
          { label: "Rings", value: rings, set: (v) => setRings(Math.round(v)), min: 1, max: 8, step: 1 },
          { label: "Rotation", value: rotation, set: setRotation, min: 0, max: 360, step: 1 },
          { label: "Jitter", value: jitter, set: setJitter, min: 0, max: 20, step: 0.5 },
        ]}
      >
        <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