Each of N radial pizza slices is triangulated by a skewed grid: two unit vectors along the slice edges define lattice coordinates, and every cell within the triangular sector is subdivided into two triangles whose fill alternates by (i + j) parity. The result is a tiled pinwheel where the tessellation density is independent of the number of slices. Slice count, row density, rotation, and dual colours are adjustable.
Pizza
8 slices · 4 rows
Source
"use client";
import { useState } from "react";
import ScrambleLink from "../ScrambleLink";
import DraggablePanel from "../DraggablePanel";
import { PanelColor } from "../_kansei/controls";
const R = 280; // viewBox radius — SVG is a 560×560 square scaled by CSS
export default function Pizza() {
const [slices, setSlices] = useState(8);
const [density, setDensity] = useState(4);
const [rotation, setRotation] = useState(0);
const [colorA, setColorA] = useState("#cc2010");
const [colorB, setColorB] = useState("#f5f0e8");
const cx = R;
const cy = R;
const size = R * 2;
const N = slices;
const sectorAngle = (2 * Math.PI) / N;
const rows = density;
const cellSize = R / rows;
// Grid coord (i, j) within slice s → SVG point string
const pt = (i: number, j: number, s: number) => {
const base = s * sectorAngle;
const ux = Math.cos(base);
const uy = Math.sin(base);
const vx = Math.cos(base + sectorAngle);
const vy = Math.sin(base + sectorAngle);
const x = cx + i * cellSize * ux + j * cellSize * vx;
const y = cy + i * cellSize * uy + j * cellSize * vy;
return `${x.toFixed(2)},${y.toFixed(2)}`;
};
const tris: React.ReactNode[] = [];
for (let s = 0; s < N; s++) {
for (let i = 0; i < rows; i++) {
for (let j = 0; j < rows - i; j++) {
const p00 = pt(i, j, s);
const p10 = pt(i + 1, j, s);
const p01 = pt(i, j + 1, s);
if (i + j < rows - 1) {
const p11 = pt(i + 1, j + 1, s);
const flip = (i + j) % 2 === 0;
tris.push(
<polygon key={`${s}-${i}-${j}-a`} points={`${p00} ${p10} ${p11}`} fill={flip ? colorA : colorB} />,
<polygon key={`${s}-${i}-${j}-b`} points={`${p00} ${p11} ${p01}`} fill={flip ? colorB : colorA} />,
);
} else {
const fill = (i + j) % 2 === 0 ? colorA : colorB;
tris.push(<polygon key={`${s}-${i}-${j}`} points={`${p00} ${p10} ${p01}`} fill={fill} />);
}
}
}
}
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">
<rect width={size} height={size} fill={colorB} />
<g transform={`rotate(${rotation}, ${cx}, ${cy})`}>{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">Pizza</p>
<p className="text-[12px] tracking-[0.08em] mt-1 text-foreground/30">
{slices} slices · {density} rows
</p>
</div>
<DraggablePanel
isLight={false}
controls={[
{ label: "Slices", value: slices, set: (v) => setSlices(Math.round(v)), min: 3, max: 16, step: 1 },
{ label: "Density", value: density, set: (v) => setDensity(Math.round(v)), min: 2, max: 10, 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} />
</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>
);
}