import { useEffect, useMemo, useRef, useState } from 'react'; import { ready, ink, blit } from '../../lib/mrly.js'; import { mount, Page, Row, Pick, Slider, Check, Btn, Stats, Stat, Note, Group } from '../../lib/app.jsx'; import { Sketch } from '../../lib/draw.jsx'; import { useQuery, stamp } from '../../lib/query.js'; import { useSeeds, roll } from '../../lib/select.jsx'; const m = await ready(); const SIZE = 768; const LABELS = 1000; const LATTICES = ['square', 'hex']; const MARKS = [['prime', 'primes'], ['twin', 'twin primes'], ['squarefree', 'squarefree'], ['mobius', 'Mobius sign']]; const FIRST = { lattice: 'square', side: 201, mark: 'prime', a: 4, b: -2, c: 41, faint: true }; const CLICK = new URLSearchParams(location.search).get('click'); const shuffle = (seed) => { const [lattice, side, a, b, c] = roll(seed, [[0, LATTICES.length - 1], [21, 401], [1, 4], [-20, 20], [2, 97]]); return { lattice: LATTICES[lattice], side: side | 1, a, b, c: m.prime_from(c) }; }; const first = (seeds) => (seeds.get() ? { ...FIRST, ...shuffle(seeds.get()) } : FIRST); function Num({ label, value, min, max, onChange }) { const [text, setText] = useState(String(value)); useEffect(() => { if (+text !== value) setText(String(value)); }, [value]); return ( ); } function App() { const s = useSeeds(); const [look, save] = useQuery(first(s)); const [picked, setPicked] = useState(null); const [error, setError] = useState(null); const kept = useRef({ pixels: null, poly: null, centres: null }); const view = useMemo(() => { try { const pixels = m.spiral_pixels(look.lattice, look.side, look.a, look.b, look.c, look.mark, look.faint, SIZE); const poly = JSON.parse(m.spiral_polynomial(look.lattice, look.side, look.a, look.b, look.c)); const centres = poly.top <= LABELS ? m.spiral_centers(look.lattice, look.side, SIZE) : null; kept.current = { pixels, poly, centres }; return { ...kept.current, error: null }; } catch (error) { return { ...kept.current, error }; } }, [look.lattice, look.side, look.mark, look.a, look.b, look.c, look.faint]); const set = (patch) => { save(patch); if ('faint' in patch) stamp({ faint: patch.faint ? null : 0 }); setPicked(null); setError(null); }; const draw = (canvas) => { const { pixels, centres } = view; if (!pixels) return; blit(canvas, pixels); const ctx = canvas.getContext('2d'); if (centres) { const mono = getComputedStyle(document.body).getPropertyValue('--mono'); const px = Math.min(14, SIZE / look.side * 0.42); ctx.font = `${px}px ${mono}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = ink.bg; for (let n = 1; 2 * n <= centres.length; n++) { const [x, y] = [centres[2 * n - 2], centres[2 * n - 1]]; const light = pixels.rgba[(Math.floor(y) * SIZE + Math.floor(x)) * 4 + 1] > 100; ctx.fillStyle = light ? ink.bg : ink.dim; ctx.fillText(n, x, y + 1); } } if (picked) { ctx.strokeStyle = ink.fg; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(picked.px, picked.py, picked.span / 2 + 3, 0, Math.PI * 2); ctx.stroke(); } }; const hit = (x, y) => { if (!view.pixels) return; setError(null); try { setPicked(JSON.parse(m.spiral_at(look.lattice, look.side, x, y, SIZE))); } catch (error) { setError(error); } }; useEffect(() => { if (CLICK) hit(...CLICK.split(',').map(Number)); }, []); const poly = view.poly; const shown = poly ? poly.values.slice(0, 8).join(' ') + (poly.values.length > 8 ? ' …' : '') : ''; const legend = poly ? `${look.a} k² ${look.b < 0 ? '-' : '+'} ${Math.abs(look.b)} k ${look.c < 0 ? '-' : '+'} ${Math.abs(look.c)}: ${shown}` : ''; const controls = ( <> set({ lattice: v })} /> set({ side: v })} /> set({ mark: v })} /> set({ faint: v })} /> set({ a: v })} /> set({ b: v })} /> set({ c: v })} /> set(shuffle(s.next()))}>Randomize ); return ( On the square lattice ring k holds 8k cells and ends at the odd square (2k + 1)² on the diagonal below right, so a straight line through the spiral picks one number per ring and reads a quadratic in k: a diagonal has a = 4, a line through the centre a = 1 or 2. Euler's m² - m + 41 is prime for m from 0 to 40; at m = 2k it is 4k² - 2k + 41, the line lit at the start, prime for its first 21 values and then at 1763 = 41 · 43 it breaks. On the hexagonal lattice ring r holds 6r cells and ends at the centered hexagonal number 3r² + 3r + 1, so its straight lines read quadratics with a = 3. Every cell is sieved and painted in Rust: yellow for the mark, orange where the quadratic lands on a prime, blue where it lands on a composite, pink for a Mobius value of minus one. The same primes are sieved, counted and found by the carpet stack on the primes page.} controls={controls}>

The sheet {legend}

{ const box = event.currentTarget.getBoundingClientRect(); hit((event.clientX - box.left) * SIZE / box.width, (event.clientY - box.top) * SIZE / box.height); }} />
{poly?.top} {poly?.primes} {poly && `${(poly.density * 100).toFixed(2)}%`} {poly && `${poly.hits} of ${poly.count}`} {poly && `${(poly.share * 100).toFixed(1)}%`} {poly && (poly.count === 0 ? 'off the sheet' : poly.streak === poly.count ? `all ${poly.count} prime` : `${poly.streak} primes, then ${poly.values[poly.streak]}`)} {picked && `${picked.n} on ring ${picked.ring} at ${picked.x}, ${picked.y}`} {picked && (picked.n === 1 ? 'one, neither prime nor composite' : picked.prime ? 'prime' : 'composite')} {picked && (picked.factors.length ? picked.factors.map(([p, e]) => (e > 1 ? `${p}^${e}` : p)).join(' · ') : 'none')}
); } mount();