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