index.jsx
9.9 kB · jsx · 201 lines
1import { useEffect, useMemo, useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Slider, Btn, Stats, Stat, Note } from '../../lib/app.jsx';4import { Pixels, Sketch } from '../../lib/draw.jsx';5import { board, bars, axis, tag } from '../../lib/chart.js';6import { mix } from '../../lib/series.jsx';7import { useQuery, stamp } from '../../lib/query.js';8import { useSeeds, roll, Ramp, Cropper, cropOf } from '../../lib/select.jsx';910const m = await ready();11const NAMES = [...m.moire_names()];12const SIZES = [128, 256, 384, 512];1314const drawn = (seed) => {15 const [preset, limit] = roll(seed, [[0, NAMES.length - 1], [1, 41]]);16 return { preset: NAMES[preset], limit: limit | 1 };17};1819const heat = (r) => {20 const t = Math.min(1, Math.sqrt(Math.max(0, r)));21 return t < 0.5 ? mix(ink.fg, ink.blue, t * 2) : mix(ink.blue, ink.deep, t * 2 - 1);22};2324const shown = (r) => (r === 0 ? '0' : r.toFixed(9));2526function Heat({ scales, grid, clear, row, mate }) {27 const draw = (canvas) => {28 const n = scales.length;29 const gutter = 42;30 const free = Math.max(60, canvas.clientWidth - gutter - 12);31 const size = n ? Math.max(4, Math.min(26, free / n)) : 0;32 const b = board(canvas, Math.round(52 + n * size), { left: gutter, right: 12, top: 44, bottom: 8 });33 if (!n) {34 tag(b, 'no odd scale reaches 3 yet', ink.dim);35 return;36 }37 const every = Math.max(1, Math.ceil(n / Math.max(1, Math.floor(free / 24))));38 scales.forEach((a, i) => {39 const y = b.roof + i * size;40 grid[i].forEach((r, j) => {41 b.ctx.fillStyle = heat(r);42 b.ctx.fillRect(gutter + j * size, y, Math.max(1, size - 1), Math.max(1, size - 1));43 });44 if (i % every) return;45 const hue = a === row ? ink.yellow : clear[i] ? ink.green : ink.dim;46 tag(b, String(a), hue, 'right', gutter - 6, y + size / 2 + 4);47 tag(b, String(a), hue, 'center', gutter + i * size + size / 2, b.roof - 10);48 });49 const k = scales.indexOf(row);50 const j = scales.indexOf(mate);51 b.ctx.lineWidth = 1;52 b.ctx.strokeStyle = ink.dim;53 b.ctx.beginPath();54 b.ctx.moveTo(gutter, b.roof);55 b.ctx.lineTo(gutter + n * size, b.roof + n * size);56 b.ctx.stroke();57 if (k >= 0) {58 b.ctx.strokeStyle = ink.yellow;59 b.ctx.strokeRect(gutter - 1.5, b.roof + k * size - 1.5, n * size + 2, size + 2);60 }61 if (j >= 0) {62 b.ctx.strokeStyle = ink.pink;63 b.ctx.strokeRect(gutter + j * size - 1.5, b.roof - 1.5, size + 2, n * size + 2);64 }65 tag(b, 'white where the correlation is exactly zero; left of the diagonal a row clear to its end is prime, in green', ink.dim);66 };67 return <Sketch className="bars" draw={draw} deps={[grid, clear, row, mate]} role="img" aria-label="The r-matrix" />;68}6970function Strip({ witness, mate, onPick }) {71 const n = witness.scales.length;72 const draw = (canvas) => {73 const b = board(canvas, 170);74 if (!n) {75 axis(b);76 tag(b, `scale ${witness.n} has no earlier odd scale`, ink.dim);77 return;78 }79 bars(b, witness.row, {80 peak: Math.max(witness.max, 1e-12),81 color: (i, v) => (witness.scales[i] === mate ? ink.yellow : v === 0 ? ink.line : ink.blue),82 });83 const step = Math.max(1, Math.ceil(n / 14));84 axis(b, witness.scales.map((s, i) => [(i + 0.5) / n, String(s)]).filter((_, i) => i % step === 0));85 witness.row.forEach((v, i) => {86 if (v !== 0 && witness.scales[i] !== mate) return;87 b.ctx.fillStyle = witness.scales[i] === mate ? ink.yellow : ink.line;88 b.ctx.fillRect(b.x(i / n) + 1, b.floor - 3, Math.max(1, b.wide / n - 2), 3);89 });90 tag(b, `scale ${witness.n} against every earlier odd scale, click to pick one`, ink.dim);91 tag(b, witness.prime ? 'every bar exactly zero' : `largest ${witness.max.toFixed(6)} at scale ${witness.at}`, witness.prime ? ink.green : ink.yellow, 'right');92 };93 const seek = (f) => onPick(witness.scales[Math.max(0, Math.min(n - 1, Math.floor(f * n)))]);94 return <Sketch className="bars" draw={draw} deps={[witness, mate]} onSeek={n ? seek : undefined} role="img" aria-label="The row of the chosen carpet against every earlier odd scale" />;95}9697function App() {98 const s = useSeeds();99 const [pick, setPick] = useState({ preset: NAMES[0], limit: 9, size: 256, ...(s.get() ? drawn(s.get()) : null) });100 const [look, setLook] = useState({ ramp: 'fire', levels: 16, invert: false });101 const [crop, setCrop] = useQuery({ crop: '', 'crop-r': 16, 'crop-anti': false });102 const [law, setLaw] = useQuery({ scale: 9, mate: 3 });103 const [playing, setPlaying] = useState(false);104 const shownField = useRef(null);105106 let error = null;107 try {108 const c = cropOf(crop);109 let pixels;110 if (c.active) {111 const field = m.field_crop(m.moire_field(pick.preset, pick.limit, pick.size), pick.size, 2, c.shape, c.rnum, c.rden, c.anti);112 let low = Infinity, high = -Infinity;113 for (const v of field) if (!Number.isNaN(v)) { low = Math.min(low, v); high = Math.max(high, v); }114 pixels = m.paint_span(field, pick.size, low, high, look.ramp, look.levels, look.invert);115 } else {116 pixels = m.moire(pick.preset, pick.limit, pick.size, look.ramp, look.levels, look.invert);117 }118 shownField.current = { pixels, scales: Array.from(m.odd_scales(pick.limit)).join(' '), size: pick.size };119 } catch (fault) {120 error = fault;121 }122123 const scales = useMemo(() => Array.from(m.odd_scales(pick.limit)).filter((v) => v >= 3), [pick.limit]);124 const grid = useMemo(() => scales.map((a) => scales.map((b) => m.moire_correlation(a, b))), [scales]);125 const clear = useMemo(() => scales.map((a) => JSON.parse(m.carpet_witness(a)).prime), [scales]);126 const row = scales.filter((v) => v <= law.scale).at(-1) ?? scales[0] ?? 0;127 const witness = useMemo(() => (row ? JSON.parse(m.carpet_witness(row)) : null), [row]);128 const mate = witness && witness.scales.includes(law.mate) ? law.mate : (witness?.scales.at(-1) ?? 0);129 const value = witness && mate ? witness.row[witness.scales.indexOf(mate)] : null;130131 const step = () => setPick((old) => ({ ...old, limit: old.limit >= 41 ? 1 : old.limit + 2 }));132133 useEffect(() => {134 if (!playing) return;135 const timer = setInterval(step, 350);136 return () => clearInterval(timer);137 }, [playing]);138139 const view = shownField.current;140141 const controls = (142 <>143 <section>144 <h3>The stack</h3>145 <Row>146 <Pick label="preset" value={pick.preset} options={NAMES} onChange={(v) => setPick({ ...pick, preset: v })} />147 <Slider label="scales up to" value={pick.limit} min={1} max={41} step={2} onChange={(v) => setPick({ ...pick, limit: v })} />148 <Btn onClick={() => { setPlaying(!playing); if (!playing) step(); }}>{playing ? 'Stop' : 'Play the scales'}</Btn>149 <Btn onClick={() => setPick({ ...pick, ...drawn(s.next()) })}>Randomize</Btn>150 <Pick label="size" value={pick.size} options={SIZES.map((v) => [v, v])} onChange={(v) => setPick({ ...pick, size: +v })} />151 </Row>152 </section>153 <section>154 <h3>Colour</h3>155 <Row>156 <Ramp value={look} onChange={(patch) => setLook({ ...look, ...patch })} />157 </Row>158 </section>159 <section>160 <h3>Crop</h3>161 <Row>162 <Cropper value={crop} onChange={(patch) => { setCrop(patch); if (!({ ...crop, ...patch }).crop) stamp({ crop: null, 'crop-r': null, 'crop-anti': null }); }} />163 </Row>164 </section>165 <section>166 <h3>The r-matrix</h3>167 <Row>168 <Slider label="carpet" value={row || 3} min={3} max={Math.max(3, scales.at(-1) ?? 3)} step={2} onChange={(v) => setLaw({ scale: v })} />169 </Row>170 </section>171 </>172 );173174 return (175 <Page crumb="moire" title="Stack one design over its own scales"176 sub="One design sampled at scale 1, 3, 5, and so on, the layers stacked into one field. Stacking is where the interference comes from: each new scale adds a finer grid on top of the coarse ones."177 controls={controls}178 foot={<>The heatmap sums the parity of the low corner over the odd scales, the weave folds the same layers to their parity, the hive samples on the hexagonal lattice, and the carpet keeps eight corners of nine in base 3. The field is quantized into levels and painted through a ramp; the pixels arrive already colored. The r-matrix reads a second law off the same stack: two parity carpets at odd scales correlate to exactly zero when the scales are coprime and to a strictly positive number when the scales share a factor, so a scale whose earlier row is all white is prime, the detector the <a href="../primes">primes</a> page stacks, with the closed form and its proof in the paper <a href="/papers/moire-correlation-laws/">moire correlation laws</a>, and the stack that counts pi in <a href="/research/pi/">pi out of the stack</a>. Every number in the panel is computed in Rust; the page only draws.</>}>179 {view && <Pixels data={view.pixels} style={{ maxWidth: 640 }} role="img" aria-label="The moire field" />}180 <Stats>181 <Stat label="scales">{view?.scales}</Stat>182 <Stat label="pixels">{view && `${view.size} by ${view.size}`}</Stat>183 </Stats>184 <div className="panel" style={{ marginTop: 22 }}>185 <h2>The r-matrix <span>the correlation of the flat carpet layers at every pair of odd scales</span></h2>186 <Heat scales={scales} grid={grid} clear={clear} row={row} mate={mate} />187 {witness && <Strip witness={witness} mate={mate} onPick={(v) => setLaw({ mate: v })} />}188 <Stats>189 <Stat label="scale m">{row || 'none'}</Stat>190 <Stat label="scale n">{mate || 'none earlier'}</Stat>191 <Stat label="r">{value === null ? 'none' : shown(value)}</Stat>192 <Stat label="row of m">{witness ? (witness.prime ? 'clear against every earlier scale' : `largest ${shown(witness.max)} at scale ${witness.at}`) : 'none'}</Stat>193 <Stat label="matrix">{`${scales.length} by ${scales.length}`}</Stat>194 </Stats>195 </div>196 <Note error={error} />197 </Page>198 );199}200201mount(<App />);