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