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