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