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 />);