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
Source
"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>
);
}