index.jsx
5.8 kB · jsx · 109 lines
1import { useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Stats, Stat, Note, Group } from '../../lib/app.jsx';4import { Markup, Sketch } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';6import { useSeeds, seeded, Picker } from '../../lib/select.jsx';7import { board, bars, line, axis, tag } from '../../lib/chart.js';89const m = await ready();10const TOP = 16;11const TALLY = [12 ['boundary', 'boundary'], ['edges', 'edges'], ['interior', 'interior'], ['vertices', 'vertices'], ['euler', 'euler'],13 ['fills', 'filled'], ['voids', 'empty'], ['components', 'pieces'], ['holes', 'holes'], ['giant', 'largest piece'],14];1516const capOf = (fractal) => (fractal ? m.level_cap(fractal, 1, 100) : m.level_cap(1, 1, 1));1718function App() {19 const s = useSeeds();20 const [pick, set] = useQuery({ code: seeded(s, 3, 2, '23'), k: 2, level: 1 });21 const [fractal, setFractal] = useState(0);22 const shown = useRef(null);2324 const lid = capOf(fractal);25 const level = Math.min(pick.level, lid);26 const k = Math.min(TOP, Math.max(1, pick.k));27 const code = pick.code.trim();2829 let error = null, art = null, split = null;30 try {31 const rows = JSON.parse(m.slice_series(code, TOP));32 const number = fractal || rows[k - 1].n;33 const tally = JSON.parse(m.slice_census(code, number, level, 2));34 shown.current = {35 rows, tally, here: fractal ? 0 : k,36 name: m.name_of(code, 3, 2),37 reading: fractal ? `tile ${number}` : `k ${k}, n ${number}`,38 };39 if (level === 1) split = JSON.parse(m.slice_partition(number));40 const svg = m.hex_svg(code, number, level, 2, 'cut', Math.max(1, Math.round(360 / tally.side)));41 if (svg.length > 4000000) throw new Error('that drawing is larger than this page serves; lower the level.');42 art = svg;43 } catch (fault) {44 error = fault;45 }4647 const chart = (canvas) => {48 const view = shown.current;49 if (!view) return;50 const b = board(canvas, 220);51 const rows = view.rows, n = rows.length;52 const peak = rows.reduce((a, r) => Math.max(a, r.fills), 1);53 const crest = rows.reduce((a, r) => Math.max(a, r.components, r.holes), 1);54 bars(b, rows.map((r) => r.fills), { peak, color: (i) => (rows[i].k === view.here ? ink.yellow : ink.blue) });55 for (const [key, color] of [['components', ink.green], ['holes', ink.pink]]) {56 line(b, rows.map((r, i) => [(i + 0.5) / n, r[key] / crest]), color, { dots: 2.5 });57 }58 axis(b, rows.map((r, i) => [(i + 0.5) / n, r.k]));59 const next = tag(b, `filled triangles, peak ${peak}`, ink.blue);60 tag(b, `holes, peak ${crest}`, ink.pink, 'left', tag(b, 'pieces', ink.green, 'left', next + 16) + 16);61 };6263 const toFractal = (value) => {64 setFractal(value);65 if (pick.level > capOf(value)) set({ level: capOf(value) });66 };6768 const onSeek = (frac) => {69 toFractal(0);70 set({ k: Math.min(TOP, Math.max(1, Math.floor(frac * TOP) + 1)) });71 };7273 const view = shown.current;7475 const controls = (76 <>77 <Group name="Design">78 <Picker dimension={3} code={pick.code} seeds={s} onChange={set} />79 </Group>80 <Group name="Section">81 <label>side <input type="range" min={1} max={TOP} value={k} disabled={fractal !== 0} onChange={(e) => set({ k: +e.target.value })} /><span className="num">{view?.reading}</span></label>82 <Pick label="fractal" value={fractal} options={[[0, 'off, odd side'], [3, 'base 3 tile'], [5, 'base 5 tile']]} onChange={(value) => toFractal(+value)} />83 <label>level <input type="range" min={1} max={lid} value={level} disabled={fractal === 0} onChange={(e) => set({ level: +e.target.value })} /><span className="num">{level}</span></label>84 </Group>85 </>86 );8788 return (89 <Page crumb="slices" title="The middle plane of a cube is a hexagon of triangles" controls={controls}90 sub={<>Cut the cube of odd side <code>side = 2k-1</code> through its centre, square to the main diagonal, and the section is a regular hexagon tiled by <code>6 side^2</code> unit triangles. A design's parity rule fills some of them: carpet and net split the hexagon between them with nothing left over, and the carpet's own section alternates as <code>k</code> grows, falling into many separate pieces at odd <code>k</code> and closing into one piece pierced by holes at even <code>k</code>. Drag the chart to move the side.</>}91 foot={<>The mesh is the crate's triangular section of the cube it builds from the code, and every count is read off that mesh; the closed form beside the triangle count is the polynomial in <code>k</code>, evaluated in Rust and never fitted here. Pieces come from the adjacency network of filled triangles and holes from the Euler number of the filled sub-mesh, so the alternation in the chart is counted, not drawn. The mesh census, the closed forms beside it and which designs pierce the hexagon with holes are in <a href="/research/slices/">the slices note</a>.</>}>92 <Sketch draw={chart} deps={[view]} onSeek={onSeek} className="bars" role="img" aria-label="Filled triangles, pieces and holes by side, drag to move the side" />93 <Stats>94 <Stat label="name">{view?.name}</Stat>95 <Stat label="side">{view?.tally.side}</Stat>96 <Stat label="triangles">{view?.tally.triangles}</Stat>97 <Stat label="closed form">{view?.tally.closed.triangles}</Stat>98 {TALLY.map(([key, label]) => <Stat key={key} label={label}>{view?.tally[key]}</Stat>)}99 </Stats>100 <Stats>101 {split && <><Stat label="carpet">{split.carpet}</Stat><Stat label="net">{split.net}</Stat><Stat label="together">{split.together}</Stat><Stat label="hexagon">{split.hexagon}</Stat><Stat label="partition">{split.exact ? 'exact' : 'broken'}</Stat></>}102 </Stats>103 <Markup svg={art ?? ''} role="img" aria-label="The middle slice" />104 <Note error={error} />105 </Page>106 );107}108109mount(<App />);