index.jsx
6.8 kB · jsx · 141 lines
1import { useMemo, useRef } from 'react';2import { ready, ink, rgb } from '../../lib/mrly.js';3import { mount, Page, Pick, Slider, Check, Stats, Stat, Note, Group } from '../../lib/app.jsx';4import { Sketch } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';6import { useSeeds, seeded, Picker } from '../../lib/select.jsx';78const m = await ready();9const SIZE = 768;10const PAD = 10;11const SIDES = [2, 3, 5, 7];12const GROWTHS = [['prime', 'MrlyUlam: the primes grow'], ['every', 'MrlySpiral: every number grows']];13const FIRST = { growth: 'every', code: '7', base: 2, side: 2, top: 300, path: true };1415const first = (seeds) => ({ ...FIRST, code: seeded(seeds, 2, FIRST.base, FIRST.code) });1617function inked(grid, color) {18 const [w, h] = [grid.width, grid.height];19 const [r, g, b] = rgb(color);20 const rgba = new Uint8ClampedArray(w * h * 4);21 for (let i = 0; i < w * h; i += 1) {22 if (!grid.types[i]) continue;23 rgba[i * 4] = r;24 rgba[i * 4 + 1] = g;25 rgba[i * 4 + 2] = b;26 rgba[i * 4 + 3] = 255;27 }28 const canvas = document.createElement('canvas');29 canvas.width = w;30 canvas.height = h;31 canvas.getContext('2d').putImageData(new ImageData(rgba, w, h), 0, 0);32 return canvas;33}3435function App() {36 const s = useSeeds();37 const [look, save] = useQuery(first(s));38 const kept = useRef({ cells: null, read: null, art: null });3940 const view = useMemo(() => {41 try {42 const cells = m.snail_cells(look.side, look.top, look.growth);43 const read = JSON.parse(m.snail_read(look.side, look.top, look.growth));44 const art = [];45 for (let k = 1; k <= read.peak; k += 1) {46 const grid = m.two_grid(look.code, look.side, k, 0, look.base);47 art[k] = { lit: inked(grid, ink.blue), plain: inked(grid, ink.dim) };48 }49 kept.current = { cells, read, art };50 return { ...kept.current, error: null };51 } catch (error) {52 return { ...kept.current, error };53 }54 }, [look.growth, look.side, look.top, look.code, look.base]);5556 const draw = (canvas) => {57 const { cells, read, art } = view;58 if (!cells) return;59 canvas.width = SIZE;60 canvas.height = SIZE;61 const ctx = canvas.getContext('2d');62 ctx.imageSmoothingEnabled = false;63 ctx.fillStyle = ink.deep;64 ctx.fillRect(0, 0, SIZE, SIZE);65 const inner = SIZE - 2 * PAD;66 const scale = inner / Math.max(read.width, read.height);67 const at = (x, y) => [68 PAD + (inner - read.width * scale) / 2 + (x - read.low[0]) * scale,69 SIZE - PAD - (inner - read.height * scale) / 2 - (y - read.low[1]) * scale,70 ];71 if (look.path) {72 ctx.strokeStyle = ink.line;73 ctx.lineWidth = 1;74 ctx.beginPath();75 for (let i = 0; i < cells.length; i += 5) {76 const half = cells[i + 2] / 2;77 const [px, py] = at(cells[i] + half, cells[i + 1] + half);78 if (i) ctx.lineTo(px, py);79 else ctx.moveTo(px, py);80 }81 ctx.stroke();82 }83 for (let i = 0; i < cells.length; i += 5) {84 const side = cells[i + 2];85 const level = cells[i + 3];86 const prime = cells[i + 4];87 const wide = side * scale;88 const [px, py] = at(cells[i], cells[i + 1] + side);89 if (level && wide >= 3) {90 ctx.drawImage(prime ? art[level].lit : art[level].plain, px, py, wide, wide);91 } else {92 ctx.globalAlpha = prime ? 1 : 0.5;93 ctx.fillStyle = prime ? ink.blue : ink.dim;94 ctx.fillRect(px, py, Math.max(wide, 1), Math.max(wide, 1));95 ctx.globalAlpha = 1;96 }97 }98 };99100 const read = view.read;101 const tally = read ? read.levels.map((count, k) => `${count} at ${look.side}^${k}`).join(', ') : '';102103 const controls = (104 <>105 <Group name="The winding">106 <Pick label="growth" value={look.growth} options={GROWTHS} onChange={(v) => save({ growth: v })} />107 <Slider label="numbers" value={look.top} min={12} max={2000} onChange={(v) => save({ top: v })} />108 <Check label="the path" checked={look.path} onChange={(v) => save({ path: v })} />109 </Group>110 <Group name="The tile">111 <Picker dimension={2} bases={[2, 3]} code={look.code} base={look.base} seeds={s} onChange={save} />112 <Pick label="base" value={look.side} options={SIDES.map((q) => [q, q])} onChange={(v) => save({ side: +v })} />113 </Group>114 </>115 );116117 return (118 <Page crumb="snail" title="The snail"119 sub="Wind 1, 2, 3 and on around the Ulam spiral, but let a cell that grows carry a whole design instead of a dot, and let it grow with the number. Each tile's side is a power of the base, so every time the count gains a digit the winding widens by that factor and the path curls outward like a shell. Keep only the primes growing and the shell is built of primes; let every number grow and the same shell appears with no prime named anywhere."120 foot={<>The rule, exactly. The level of <code>n</code> is the count of its digits in the base less one, so <code>k(n) = 0</code> below the base, <code>1</code> from the base to <code>base² - 1</code>, and on; a tile at level <code>k</code> is the chosen design grown <code>k</code> times, of side <code>base^k</code>, and level 0 is the bare unit cell. MrlyUlam grows a cell only when <code>n</code> is prime and leaves one and every composite a unit cell; MrlySpiral grows every <code>n</code> by the same law and names no prime. The layout is one deterministic step: tile 1 has its lower-left corner at the origin, and the corner of tile <code>n + 1</code> is the corner of tile <code>n</code> plus the unit step the square winding takes from <code>n</code> to <code>n + 1</code>, scaled by the side of tile <code>n</code>. Tiles overlap wherever the growth outruns the winding, so the drawn area is the sum of the tile squares and counts an overlap twice. This page is an exhibit: it states no theorem, and the two rules above are ours, fixed here and nowhere else. The same numbers on the plain unit grid are the <a href="../ulam">ulam</a> page, and the sieve that decides which of them grow is on the <a href="../primes">primes</a> page.</>}121 controls={controls}>122 <div className="arena">123 <div className="panel">124 <h2>The shell <span>{read && `${read.tiles} numbers, ${read.grown} grown, base ${look.side}`}</span></h2>125 <Sketch draw={draw} deps={[view, look.path]} aria-label="The snail, the whole numbers wound on the square spiral with every grown cell a design tile" />126 </div>127 </div>128 <Stats>129 <Stat label="primes at or below the top">{read?.primes}</Stat>130 <Stat label="tiles grown past a unit cell">{read?.grown}</Stat>131 <Stat label="tiles per level">{tally}</Stat>132 <Stat label="largest tile">{read && `${read.side} by ${read.side}`}</Stat>133 <Stat label="drawn area">{read?.area}</Stat>134 <Stat label="the box">{read && `${read.width} by ${read.height}`}</Stat>135 </Stats>136 <Note error={view.error} />137 </Page>138 );139}140141mount(<App />);