index.jsx
8.7 kB · jsx · 184 lines
1import { useMemo } from 'react';2import { ready, ink, rgb, fit } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Slider, Check, Stats, Stat, Note } from '../../lib/app.jsx';4import { Sketch } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';67const m = await ready();89const CAPS = JSON.parse(m.apollonian_caps());10const ROOTS = CAPS.roots.map((name) => [name, name === 'strip' ? 'the strip (0, 0, 2, 2)' : `bounded (${name.split(',').join(', ')})`]);11const INKS = [['size', 'by curvature'], ['ford', 'the Ford circles apart'], ['one', 'one ink']];12const LOW = 5;13const HIGH = Math.round(Math.log2(CAPS.curvature));14const PAD = 16;15const BOX = 620;16const BAND = 160;1718function App() {19 const [q, setQ] = useQuery({ root: 'strip', power: 11, order: 32, stack: true, fill: true, ink: 'size' });20 const cap = 2 ** Math.min(HIGH, Math.max(LOW, q.power));2122 const view = useMemo(() => {23 const clock = performance.now();24 try {25 const read = JSON.parse(m.apollonian_read(q.root, cap, q.order));26 return {27 read,28 circles: m.apollonian(q.root, cap),29 roots: m.apollonian_root(q.root),30 marks: m.apollonian_touches(q.root, cap),31 nodes: read.strip ? JSON.parse(m.farey(q.order)) : [],32 ms: performance.now() - clock,33 };34 } catch (fault) {35 return { fault };36 }37 }, [q.root, cap, q.order]);3839 const read = view.read;40 const shadow = read?.shadow;41 const stacked = q.stack && read?.strip;4243 const draw = (canvas) => {44 if (!read) return;45 const [x0, y0, x1, y1] = read.frame;46 const wide = canvas.clientWidth;47 const scale = Math.min(wide - 2 * PAD, BOX) / Math.max(x1 - x0, y1 - y0);48 const deep = Math.round(scale * (y1 - y0)) + 2 * PAD;49 const band = stacked ? BAND : 0;50 const [ctx, w] = fit(canvas, deep + band);51 ctx.clearRect(0, 0, w, deep + band);52 const ox = (w - scale * (x1 - x0)) / 2 - x0 * scale;53 const X = (x) => ox + x * scale;54 const line = deep - PAD;55 const Y = (y) => line - (y - y0) * scale;56 const six = [ink.blue, ink.teal, ink.green, ink.yellow, ink.orange, ink.pink];57 const colour = (k, den) => {58 if (q.ink === 'one') return ink.fg;59 if (q.ink === 'ford') return den > 0 ? ink.blue : ink.dim;60 return six[Math.floor(Math.log2(Math.abs(k))) % 6];61 };6263 ctx.save();64 ctx.beginPath();65 ctx.rect(X(x0), Y(y1), scale * (x1 - x0), scale * (y1 - y0));66 ctx.clip();67 if (read.strip) {68 ctx.strokeStyle = ink.dim;69 ctx.lineWidth = 1.5;70 for (const edge of [y0, y1]) {71 ctx.beginPath();72 ctx.moveTo(X(x0), Y(edge));73 ctx.lineTo(X(x1), Y(edge));74 ctx.stroke();75 }76 }77 const paint = (data, seed) => {78 for (let i = 0; i < data.length; i += 5) {79 const [cx, cy, r, k, den] = [data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4]];80 const tone = seed ? ink.fg : colour(k, den);81 const px = r * scale;82 ctx.beginPath();83 ctx.arc(X(cx), Y(cy), px, 0, Math.PI * 2);84 if (q.fill) {85 ctx.globalAlpha = 0.15;86 ctx.fillStyle = tone;87 ctx.fill();88 ctx.globalAlpha = 1;89 }90 ctx.strokeStyle = tone;91 ctx.lineWidth = Math.min(1.3, Math.max(0.4, px / 12));92 ctx.stroke();93 }94 };95 paint(view.roots, true);96 paint(view.circles, false);97 ctx.restore();9899 if (read.strip) {100 ctx.strokeStyle = ink.blue;101 ctx.lineWidth = 1;102 for (let i = 0; i < view.marks.length; i += 4) {103 const at = X(view.marks[i]);104 ctx.beginPath();105 ctx.moveTo(at, line - 4);106 ctx.lineTo(at, line + 4);107 ctx.stroke();108 }109 }110111 if (!stacked) return;112 const pale = rgb(ink.fg).join(', ');113 const tall = band - 34;114 for (const [num, den, bright] of view.nodes) {115 const at = X(num / den);116 ctx.strokeStyle = `rgba(${pale}, ${0.14 + 0.7 * bright / q.order})`;117 ctx.lineWidth = bright > q.order / 3 ? 1.5 : 0.7;118 ctx.beginPath();119 ctx.moveTo(at, line + 8);120 ctx.lineTo(at, line + 8 + tall * bright / q.order);121 ctx.stroke();122 }123 ctx.fillStyle = ink.dim;124 ctx.font = '11px ui-monospace, monospace';125 ctx.fillText('0', X(0) - 3, line + band - 8);126 ctx.textAlign = 'right';127 ctx.fillText('1', X(1) + 3, line + band - 8);128 ctx.textAlign = 'left';129 };130131 const controls = (132 <>133 <section>134 <h3>The packing</h3>135 <Row>136 <Pick label="root quadruple" value={q.root} options={ROOTS} onChange={(v) => setQ({ root: v })} />137 <Slider label="curvature cap T" value={q.power} min={LOW} max={HIGH} show={cap} onChange={(v) => setQ({ power: v })} />138 <Pick label="ink" value={q.ink} options={INKS} onChange={(v) => setQ({ ink: v })} />139 <Check label="fill the discs" checked={q.fill} onChange={(v) => setQ({ fill: v })} />140 </Row>141 </section>142 <section>143 <h3>The stack</h3>144 <Row>145 <Check label="lay the Farey stack under the line" checked={q.stack} onChange={(v) => setQ({ stack: v })} />146 <Slider label="depth Q" value={q.order} min={2} max={CAPS.order} onChange={(v) => setQ({ order: v })} />147 </Row>148 </section>149 </>150 );151152 const census = read && `root (${read.curvatures.join(', ')}) N(T) = ${read.circles} circles to curvature T = ${read.cap}, the root quadruple excluded ${read.quads} quadruples, ${read.broken} broken, ${read.strayed} strayed`;153 const exponent = read?.exponent !== null && read?.exponent !== undefined154 ? `local exponent log(N(${read.cap})/N(${read.from}))/log 4 = ${read.exponent.toFixed(4)}, read on that one octave pair and nowhere finer`155 : '';156 const identified = read && (read.strip157 ? `line-tangent circles ${read.line}, of them Ford circles ${read.ford}, off-Ford ${read.line - read.ford}`158 : 'no line in this root, so no Ford circles and no stack to shadow');159 const agreed = read && read.strip && shadow && (shadow.covered160 ? `the stack at Q = ${shadow.order}: nodes lit in the open period ${shadow.nodes}, tangency points carrying them ${shadow.touched}, missed ${shadow.missed}, off-Ford below ${shadow.reach} ${shadow.offford} brightness ${shadow.bright} against Q(Q + 1)/2 = ${shadow.want}`161 : `the packing stops at ${read.cap} and the stack at Q = ${shadow.order} needs curvature 2 Q^2 = ${shadow.reach}: raise T before reading the agreement`);162163 return (164 <Page crumb="apollonian" title="The Apollonian gasket"165 sub="Four mutually tangent circles obey Descartes, and the second circle in a curvilinear triangle is the first reflected, k' = 2(k1 + k2 + k3) - k4, with no square root in it. Write a circle as the integer triple (k, kx, ky) and that reflection moves all three coordinates at once, so an integer root quadruple grows a whole packing in exact integers. The strip root is two lines a unit apart: the circles it grows that rest on the lower line are exactly the Ford circles, the circle over a reduced a/b carrying curvature 2 b squared and resting at a/b. Lay the Farey stack under that line and the bars stand at the tangency points, one bar to a circle, lit floor(Q/b) times."166 controls={controls}167 foot={<>The growth, the six exact invariants it checks on every quadruple, the census, the Ford test, the tangency fractions and the agreement with the stack are computed in Rust; the page strokes the circles it is handed at the centres and radii the crate gives. The stack is the one <a href="../farey">the Farey demo</a> builds, and its bars are placed from the same numerator and denominator the tangency points carry, so a bar and its tick land on one pixel. The census exponent is printed as a ratio of two counts over one octave pair and never fitted; the dimension it is reaching for, and why no design of any base has it, are in <a href="/research/apollonian/">the Apollonian note</a>. The desk's other gasket is the Sierpinski one and shares nothing with this but the name.</>}>168 <Sketch draw={draw} deps={[view, q.ink, q.fill, stacked, q.order]} role="img" aria-label="An integral Apollonian packing drawn over the Farey stack of its tangency points" />169 <Stats>170 <Stat label="circles drawn">{read?.drawn}</Stat>171 <Stat label="census N(T)">{read?.circles}</Stat>172 <Stat label="Ford circles">{read && (read.strip ? read.ford : '-')}</Stat>173 <Stat label="nodes lit">{read && (read.strip ? shadow.nodes : '-')}</Stat>174 <Stat label="brightness">{read && (read.strip ? shadow.bright : '-')}</Stat>175 <Stat label="broken">{read?.broken}</Stat>176 <Stat label="draw">{view.ms !== undefined && `${view.ms.toFixed(0)} ms`}</Stat>177 </Stats>178 {read && <pre>{[census, identified, agreed, exponent].filter(Boolean).join('\n')}</pre>}179 <Note error={view.fault} />180 </Page>181 );182}183184mount(<App />);