index.jsx

12.0 kB · jsx · 211 lines

1import { useMemo } from 'react';2import { ready, ink, fit } from '../../lib/mrly.js';3import { mount, Page, Group, Row, Pick, Slider, Btn, Stats, Stat, Note } from '../../lib/app.jsx';4import { Sketch } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';67const m = await ready();89const MENU = JSON.parse(m.radix_menu());10const KOCH = MENU.presets[0];11const PAD = 16;12const TALL = 520;1314const PLACE =15  'A design places its copies by hand today: the base is a whole number, the digits are cells of a base by base box, and every cell sits where its row and column put it. The place dial hands that job to a ring of the plane. Pick a ring, `Z[i]` on the square lattice or `Z[w]` on the hexagonal, pick a base `b` of norm `q = N(b)` inside it, and pick a digit `d` for some of the `q` residue classes modulo `b`. Each digit is a place map `phi_d(x) = (u_d x + d) / b`, a similarity of ratio `1 / sqrt(q)` turned by a unit `u_d`, and the design is the set those maps hold fixed. A word `d_1 ... d_level` lands on `sum_(j=1..level) (prod_(i<j) u_(d_i)) d_j b^(-j)`, which is what the dots below are.';1617const GLUE =18  'Accept and place do not interfere: the words are counted before any of them is drawn, so a design of `fill` digits writes `fill^level` words at that level whatever the base and whatever the twists. What the twists can do is send two words to one point. That is the third slot, the glue, and it is never chosen: it is what the place maps do to the accepted words. Watch the distinct count fall below the fill and the design fold onto itself.';1920const CODE =21  'A digit set is one representative per chosen class, read against the canonical system: the `q` representatives of least norm, ties broken by argument. The code below is one bit a class in that order, so it names which classes are in and never which representatives stand for them. Move a digit by a multiple of `b` and the code does not move while the design does, `phi_(d + b m)(x) = phi_d(x) + m`, so a code alone names a design only when every digit is canonical. The Koch preset and the carpet preset are both off the canonical system, which is why the chips print the digit and not only the class.';2223const TWIST =24  'A twist is a unit of the ring, one per digit: four on the square lattice, six on the hexagonal, so every twist is a rotation of order `1, 2, 3, 4` or `6` and nothing else, the crystallographic restriction reading off the trace. Turning a digit keeps the word count and the similarity ratio and moves only where its copy lands, which is the whole difference between a blob and a curve: the Koch preset is the four-digit design at base `3` on the hexagonal lattice with two of its copies turned, one by a sixth of a turn and one back.';2526const attempt = (fn) => {27  try {28    return { ...fn(), error: null };29  } catch (error) {30    return { error };31  }32};3334const symbol = (ring) => (ring === 'gaussian' ? 'i' : 'w');3536const spell = (z, ring) => {37  if (z.c === 0) return `${z.a}`;38  const tail = Math.abs(z.c) === 1 ? symbol(ring) : `${Math.abs(z.c)}${symbol(ring)}`;39  if (z.a === 0) return z.c < 0 ? `-${tail}` : tail;40  return `${z.a} ${z.c < 0 ? '-' : '+'} ${tail}`;41};4243const write = (list) => [list.map((d) => `${d.a}:${d.c}`).join('_'), list.map((d) => d.u).join('_')];4445const residues = (ring, a, c) => JSON.parse(m.radix_read(ring, a, c, '0:0', '0', 1)).residues;4647const full = (ring, a, c) => write(residues(ring, a, c).map((z) => ({ ...z, u: 0 })));4849function App() {50  const [q, set] = useQuery({ ring: KOCH.ring, a: KOCH.a, c: KOCH.c, d: KOCH.digits, t: KOCH.twists, level: KOCH.level, line: KOCH.line });5152  const cap = useMemo(() => attempt(() => ({ cap: m.radix_cap(q.d) })), [q.d]);53  const level = Math.max(1, Math.min(cap.cap ?? 1, Math.round(q.level) || 1));54  const read = useMemo(() => attempt(() => ({ card: JSON.parse(m.radix_read(q.ring, q.a, q.c, q.d, q.t, level)) })), [q.ring, q.a, q.c, q.d, q.t, level]);55  const drawn = useMemo(() => attempt(() => ({ points: m.radix_points(q.ring, q.a, q.c, q.d, q.t, level) })), [q.ring, q.a, q.c, q.d, q.t, level]);5657  const card = read.card;58  const points = drawn.points;59  const ringCard = MENU.rings.find((row) => row.name === q.ring);6061  const chosen = card ? card.digits.map((d, i) => ({ a: d.a, c: d.c, u: card.twists[i], cls: d.class })) : [];6263  const lay = (list) => {64    const sorted = [...list].sort((one, two) => one.cls - two.cls);65    const [d, t] = write(sorted);66    set({ d, t, level: sorted.length ? Math.min(level, m.radix_cap(d)) : level });67  };6869  const toggle = (index) => {70    if (chosen.some((d) => d.cls === index)) lay(chosen.filter((d) => d.cls !== index));71    else lay([...chosen, { ...card.residues[index], u: 0, cls: index }]);72  };7374  const turn = (index, unit) => lay(chosen.map((d) => (d.cls === index ? { ...d, u: unit } : d)));7576  const rebase = (ring, a, c) => {77    const [d, t] = full(ring, a, c);78    set({ ring, a, c, d, t, level: Math.min(level, m.radix_cap(d)) });79  };8081  const preset = (name) => {82    const row = MENU.presets.find((one) => one.name === name);83    if (row) set({ ring: row.ring, a: row.a, c: row.c, d: row.digits, t: row.twists, level: row.level, line: row.line });84  };8586  const draw = (canvas) => {87    const [ctx, w, h] = fit(canvas, TALL);88    ctx.clearRect(0, 0, w, h);89    if (!points || points.length < 2) return;90    let [lowX, lowY, highX, highY] = [Infinity, Infinity, -Infinity, -Infinity];91    for (let i = 0; i < points.length; i += 2) {92      lowX = Math.min(lowX, points[i]);93      highX = Math.max(highX, points[i]);94      lowY = Math.min(lowY, points[i + 1]);95      highY = Math.max(highY, points[i + 1]);96    }97    const span = Math.max(highX - lowX, highY - lowY, 1e-9);98    const scale = Math.min(w - 2 * PAD, h - 2 * PAD) / span;99    const X = (x) => w / 2 + (x - (lowX + highX) / 2) * scale;100    const Y = (y) => h / 2 - (y - (lowY + highY) / 2) * scale;101    const count = points.length / 2;102    ctx.strokeStyle = ink.blue;103    ctx.fillStyle = ink.blue;104    if (q.line) {105      ctx.lineWidth = count > 4096 ? 0.6 : 1.2;106      ctx.lineJoin = 'round';107      ctx.beginPath();108      ctx.moveTo(X(points[0]), Y(points[1]));109      for (let i = 2; i < points.length; i += 2) ctx.lineTo(X(points[i]), Y(points[i + 1]));110      ctx.stroke();111      return;112    }113    const dot = Math.max(1, Math.min(5, Math.round(0.6 * scale * span / Math.sqrt(count))));114    for (let i = 0; i < points.length; i += 2) ctx.fillRect(X(points[i]) - dot / 2, Y(points[i + 1]) - dot / 2, dot, dot);115  };116117  const controls = (118    <>119      <Group name="The design">120        <Pick label="preset" value="" options={[['', 'pick one'], ...MENU.presets.map((row) => [row.name, row.label])]} onChange={preset} />121        <Pick label="ring" value={q.ring}122          options={MENU.rings.map((row) => [row.name, row.name === 'gaussian' ? 'Z[i], the square lattice' : 'Z[w], the hexagonal lattice'])}123          onChange={(name) => { const row = MENU.rings.find((one) => one.name === name); rebase(name, row.bases[0].a, row.bases[0].c); }} />124        <Pick label="base b" value={`${q.a}:${q.c}`}125          options={(ringCard?.bases ?? []).map((b) => [`${b.a}:${b.c}`, `${spell(b, q.ring)}, norm ${b.norm}`])}126          onChange={(value) => { const [a, c] = value.split(':').map(Number); rebase(q.ring, a, c); }} />127      </Group>128      <Group name="The depth">129        <Slider label={`level 1 to ${cap.cap ?? 1}`} value={level} min={1} max={cap.cap ?? 1} onChange={(value) => set({ level: value })} />130        <Pick label="drawn as" value={q.line ? 'line' : 'dots'} options={[['dots', 'dots, one a word'], ['line', 'a polyline through the words']]}131          onChange={(value) => set({ line: value === 'line' })} />132      </Group>133      <Group name="The digits">134        <Btn onClick={() => rebase(q.ring, q.a, q.c)}>Every residue</Btn>135        <Btn onClick={() => lay(chosen.map((d) => ({ ...d, u: 0 })))}>Drop the twists</Btn>136      </Group>137    </>138  );139140  return (141    <Page crumb="radix" title="The place dial"142      sub="A design writes words of digits and then has to put them somewhere. Today the somewhere is a box of cells at a whole-number scale. Turn the place dial and the scale becomes an element of a ring of the plane, the digits become residues modulo that element, and each one carries a unit twist: the word count never moves, the picture becomes a dragon, a gasket, a snowflake or a tile, and two words can land on one point."143      controls={controls}144      foot={<>Every residue, count, dimension and point on this page comes from the crates through wasm; the page only draws. The level is capped so one drawing stays under 2^16 points. The preset names are the standard ones of the literature: what the crate pins is the arithmetic of each quintuple, never the naming. Nearby: <a href="../memory">the memory dial</a> thins the words the same design accepts, <a href="../gaussian">the plane primes</a> walks the same two rings, <a href="../tile">the tile</a> is what the untwisted whole-number base does.</>}>145146      <div className="panel">147        <h2>the design at level {level} <span>{card ? `${card.fill} words, ${card.distinct} distinct points` : 'the place maps at work'}</span></h2>148        <Note error={cap.error ?? read.error ?? drawn.error} />149        <Sketch draw={draw} deps={[points, q.line]} role="img" aria-label={`the level ${level} points of the radix design at base ${q.a}, ${q.c}`} />150        {card ? (151          <Stats>152            <Stat label="base">{`${spell({ a: card.a, c: card.c }, card.ring)}, norm ${card.q}`}</Stat>153            <Stat label="card F">{card.size}</Stat>154            <Stat label="fill">{card.fill}</Stat>155            <Stat label="distinct">{card.distinct}</Stat>156            <Stat label="dimension">{card.dimension.toFixed(6)}</Stat>157            <Stat label="code">{card.code}</Stat>158            <span className="chip proved">the fill is `card F ^ L` at every level, twists and all</span>159            {card.glued ? <span className="chip verified">the place maps glue: fewer points than words</span> : <span className="chip verified">no glue: one point a word</span>}160          </Stats>161        ) : null}162        <p className="sub">{PLACE}</p>163      </div>164165      <div className="arena">166        <div className="panel">167          <h2>the digits <span>{card ? `${card.size} of the ${card.q} classes modulo the base` : 'one representative a class'}</span></h2>168          {card ? (169            <div className="ribbon tight">170              {card.residues.map((z, index) => {171                const held = chosen.find((d) => d.cls === index);172                return (173                  <span key={index} role="button" tabIndex={0} className={held ? 'yellow' : undefined}174                    onClick={() => toggle(index)} onKeyDown={(event) => { if (event.key === 'Enter') toggle(index); }}>175                    <b>{spell(held ?? z, card.ring)}</b>176                  </span>177                );178              })}179            </div>180          ) : null}181          {card ? (182            <Row>183              {chosen.map((d) => (184                <Pick key={d.cls} label={`twist on ${spell(d, card.ring)}`} value={String(d.u)}185                  options={card.units.map((u, index) => [String(index), spell(u, card.ring)])}186                  onChange={(value) => turn(d.cls, Number(value))} />187              ))}188            </Row>189          ) : null}190          {card ? (191            <Stats>192              <Stat label="canonical">{card.canonical ? 'every digit' : 'not every digit'}</Stat>193              <span className={`chip ${card.canonical ? 'verified' : 'proved'}`}>194                {card.canonical ? 'every digit is the least-norm representative of its class, so the code names this design' : 'a digit is off the canonical system, so the code names the classes and not this design'}195              </span>196            </Stats>197          ) : null}198          <p className="sub">{CODE}</p>199        </div>200201        <div className="panel">202          <h2>the twists <span>{card ? `${card.units.length} units on this lattice` : 'a rotation a digit'}</span></h2>203          <p className="sub">{TWIST}</p>204          <p className="sub">{GLUE}</p>205        </div>206      </div>207    </Page>208  );209}210211mount(<App />);