index.jsx
9.1 kB · jsx · 182 lines
1import { useEffect, useMemo, useRef, useState } from 'react';2import { ready, ink, blit } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Slider, Check, Btn, Stats, Stat, Note, Group } from '../../lib/app.jsx';4import { Sketch } from '../../lib/draw.jsx';5import { useQuery, stamp } from '../../lib/query.js';6import { useSeeds, roll } from '../../lib/select.jsx';7import { board, bars, axis, tag } from '../../lib/chart.js';89const m = await ready();10const SIZE = 768;11const NORMS = 60;12const RINGS = [['gaussian', 'gaussian, a + bi'], ['eisenstein', 'eisenstein, a + bω']];13const COLOURS = [['class', 'by class'], ['norm', 'by norm'], ['plain', 'plain']];14const FIRST = { ring: 'gaussian', radius: 40, colour: 'class', units: true, composites: true };15const CLICK = new URLSearchParams(location.search).get('click');1617const shuffle = (seed) => {18 const [ring, radius, colour] = roll(seed, [[0, RINGS.length - 1], [20, 120], [0, COLOURS.length - 1]]);19 return { ring: RINGS[ring][0], radius, colour: COLOURS[colour][0] };20};2122const first = (seeds) => (seeds.get() ? { ...FIRST, ...shuffle(seeds.get()) } : FIRST);2324function App() {25 const s = useSeeds();26 const [look, save] = useQuery(first(s));27 const [picked, setPicked] = useState(null);28 const [error, setError] = useState(null);29 const kept = useRef({ pixels: null, census: null, weights: null, fates: null, peak: null });3031 const view = useMemo(() => {32 try {33 const pixels = m.ring_pixels(look.ring, look.radius, look.colour, look.composites, SIZE);34 const census = JSON.parse(m.ring_census(look.ring, look.radius));35 const weights = m.ring_weights(look.ring, NORMS);36 const fates = m.ring_fates(look.ring, NORMS);37 const peak = m.ring_peak(look.ring, NORMS);38 kept.current = { pixels, census, weights, fates, peak };39 return { ...kept.current, error: null };40 } catch (error) {41 return { ...kept.current, error };42 }43 }, [look.ring, look.radius, look.colour, look.composites]);4445 const set = (patch) => {46 save(patch);47 if ('units' in patch) stamp({ units: patch.units ? null : 0 });48 if ('composites' in patch) stamp({ composites: patch.composites ? null : 0 });49 setPicked(null);50 setError(null);51 };5253 const name = (a, b) => {54 const unit = look.ring === 'gaussian' ? 'i' : 'ω';55 const size = Math.abs(b) === 1 ? '' : Math.abs(b);56 if (b === 0) return `${a}`;57 if (a === 0) return `${b < 0 ? '-' : ''}${size}${unit}`;58 return `${a} ${b < 0 ? '-' : '+'} ${size}${unit}`;59 };6061 const verdict = (p) => {62 const norm = `norm ${p.norm}`;63 const shown = p.factors.map(([q, e]) => (e > 1 ? `${q}^${e}` : q)).join(' · ');64 const [ca, cb] = p.conjugate;65 if (p.class === 'split') return `prime: ${p.norm} splits as (${name(p.a, p.b)})(${name(ca, cb)})`;66 if (p.class === 'inert') return `prime: ${p.factors[0][0]} stays prime in the plane`;67 if (p.class === 'ramified') return `prime: ${p.norm} ramifies, a unit times a square`;68 if (p.class === 'unit') return 'a unit, norm 1';69 if (p.class === 'zero') return 'the origin';70 return `composite, ${norm} = ${shown}`;71 };7273 const sheet = (canvas) => {74 if (!view.pixels) return;75 blit(canvas, view.pixels);76 if (!picked) return;77 const ctx = canvas.getContext('2d');78 const ring = (x, y, color, width, dash = []) => {79 ctx.strokeStyle = color;80 ctx.lineWidth = width;81 ctx.setLineDash(dash);82 ctx.beginPath();83 ctx.arc(x, y, picked.span / 2 + 3, 0, Math.PI * 2);84 ctx.stroke();85 ctx.setLineDash([]);86 };87 if (look.units && picked.norm > 1) {88 for (const [, , x, y] of picked.associates.slice(1)) ring(x, y, ink.fg, 1.5);89 const [, , x, y] = picked.conjugate;90 ring(x, y, ink.pink, 1.5, [4, 3]);91 }92 ring(picked.px, picked.py, ink.fg, 3);93 };9495 const chart = (canvas) => {96 if (!view.weights) return;97 const b = board(canvas, 220);98 const values = Array.from(view.weights).slice(1);99 const colour = (k) => [ink.dim, ink.blue, ink.orange, ink.pink][view.fates[k + 1]];100 bars(b, values, { color: colour });101 values.forEach((v, k) => {102 if (v || view.fates[k + 1] !== 2) return;103 b.ctx.fillStyle = ink.orange;104 b.ctx.fillRect(b.x(k / values.length) + 1, b.floor - 3, Math.max(1, b.wide / values.length - 2), 3);105 });106 axis(b, values.map((_, k) => [(k + 0.5) / values.length, k + 1]).filter(([, n]) => n % 10 === 0));107 tag(b, `peak r(${view.peak[0]}) = ${view.peak[1]}`, ink.fg);108 tag(b, 'blue split · orange inert · pink ramified', ink.dim, 'right');109 };110111 const hit = (x, y) => {112 if (!view.pixels) return;113 setError(null);114 try {115 setPicked(JSON.parse(m.ring_at(look.ring, look.radius, x, y, SIZE)));116 } catch (error) {117 setError(error);118 }119 };120121 useEffect(() => {122 if (CLICK) hit(...CLICK.split(',').map(Number));123 }, []);124125 const census = view.census;126127 const controls = (128 <>129 <Group name="The window">130 <Pick label="ring" value={look.ring} options={RINGS} onChange={(v) => set({ ring: v })} />131 <Slider label="radius" value={look.radius} min={5} max={200} onChange={(v) => set({ radius: v })} />132 <Pick label="colour" value={look.colour} options={COLOURS} onChange={(v) => set({ colour: v })} />133 <Check label="faint composites" checked={look.composites} onChange={(v) => set({ composites: v })} />134 </Group>135 <Group name="A clicked point">136 <Check label="show its units" checked={look.units} onChange={(v) => set({ units: v })} />137 </Group>138 <Group name="Seed">139 <Btn onClick={() => set(shuffle(s.next()))}>Randomize</Btn>140 </Group>141 </>142 );143144 return (145 <Page crumb="gaussian" title="Primes in the plane"146 sub="Give the whole numbers a square root of minus one and the points a + bi have primes of their own; paint them and a four-armed snowflake appears. On the hexagonal numbers a + bω it grows six arms. The colour says what became of an ordinary prime when it entered the plane: split into a point and its mirror image, stayed prime on an axis, or ramified into a square. Click a point for its norm, its class and its unit rotations."147 foot={<>The norm of <code>a + bi</code> is <code>a² + b²</code>, the norm of <code>a + bω</code> is <code>a² - ab + b²</code>, and a norm multiplies like a length squared. A point is prime when its norm is an ordinary prime, or when it is a unit times an ordinary prime that stays prime in the plane: <code>3 mod 4</code> on the square lattice, <code>2 mod 3</code> on the hexagonal one. An ordinary prime that is a norm has split into a point and its conjugate, except the one prime that ramifies, 2 or 3, whose point is a unit times a square. The units, 4 or 6 of them, turn every prime into its associates, and with the mirror give the picture its symmetry. The bars count the points of each norm: on the square lattice <code>r(n) = 4 (d₁ - d₃)</code>, silent exactly where an inert prime divides <code>n</code> to an odd power, summing to <code>4 ζ(s) L(s, χ₋₄)</code>; on the hexagonal lattice the weights sum to <code>6 ζ(s) L(s, χ₋₃)</code>. These are the zeta functions of the two rings, whose values at 2 are the coprime densities of the two lattices and whose weights ring the profiles of the <a href="../spin">spin</a> page. Every point is classified and painted in Rust, the norms sieved once per window. What base 3 hides, and why its constant is not pi, is in <a href="/research/bases/">the bases note</a>.</>}148 controls={controls}>149 <div className="arena">150 <div className="panel">151 <h2>The window <span>{census && `${look.ring}, reach ${look.radius}, norms to ${census.top}, ${census.units} units`}</span></h2>152 <Sketch draw={sheet} deps={[view, picked, look.units]} aria-label="The window, the primes of the ring around the origin" onClick={(event) => {153 const box = event.currentTarget.getBoundingClientRect();154 hit((event.clientX - box.left) * SIZE / box.width, (event.clientY - box.top) * SIZE / box.height);155 }} />156 </div>157 <div className="panel">158 <h2>The ring weights <span>{census && `norms 1 to ${NORMS}`}</span></h2>159 <Sketch draw={chart} deps={[view]} className="bars" role="img" aria-label="The ring weights, one bar per norm" />160 <Stats>161 <Stat label="points">{census?.points}</Stat>162 <Stat label="primes">{census?.primes}</Stat>163 <Stat label="split">{census?.split}</Stat>164 <Stat label="inert">{census?.inert}</Stat>165 <Stat label="ramified">{census?.ramified}</Stat>166 <Stat label="density">{census && `${(census.density * 100).toFixed(2)}%`}</Stat>167 <Stat label="symmetry">{census && `${census.symmetry}-fold`}</Stat>168 </Stats>169 <Stats>170 <Stat label="clicked">{picked && `${name(picked.a, picked.b)} at ${picked.a}, ${picked.b}`}</Stat>171 <Stat label="norm">{picked?.norm}</Stat>172 <Stat label="class">{picked?.class}</Stat>173 <Stat label="verdict">{picked && verdict(picked)}</Stat>174 </Stats>175 </div>176 </div>177 <Note error={error ?? view.error} />178 </Page>179 );180}181182mount(<App />);