index.jsx

10.6 kB · jsx · 211 lines

1import { useMemo } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Slider, Stats, Stat, Note, Group } from '../../lib/app.jsx';4import { Pixels, Sketch } from '../../lib/draw.jsx';5import { board, tag } from '../../lib/chart.js';6import { useQuery } from '../../lib/query.js';7import { useSeeds, seeded, Picker } from '../../lib/select.jsx';89const m = await ready();10const RMAX = 242;11const LEAVES = 220;1213function widths(nodes) {14  const out = [];15  for (let i = 0; i < nodes.length; i += 5) {16    const j = nodes[i];17    while (out.length <= j) out.push(0);18    out[j] += 1;19  }20  return out;21}2223function fits(read) {24  let root = 0;25  for (let j = 0; j <= read.depth; j += 1) if (read.levels[0].boxes <= LEAVES * read.levels[j].boxes) root = j;26  return root;27}2829function places(boxes, nodes) {30  const starts = [];31  let total = 0;32  for (const wide of boxes) {33    starts.push(total);34    total += wide;35  }36  const px = new Float64Array(total);37  const leaves = boxes[0];38  for (let k = 0; k < leaves; k += 1) px[k] = (leaves - k - 0.5) / leaves;39  for (let j = 1; j < boxes.length; j += 1) {40    const wide = boxes[j];41    const sum = new Float64Array(wide);42    const held = new Float64Array(wide);43    for (let k = 0; k < boxes[j - 1]; k += 1) {44      const parent = nodes[5 * (starts[j - 1] + k) + 3];45      if (parent >= wide) continue;46      sum[parent] += px[starts[j - 1] + k];47      held[parent] += 1;48    }49    for (let k = 0; k < wide; k += 1) px[starts[j] + k] = held[k] ? sum[k] / held[k] : 0;50  }51  return { starts, px };52}5354function App() {55  const s = useSeeds();56  const [q, set] = useQuery({ code: seeded(s, 2, 2, '7'), r: 40, at: 2, zoom: -1, box: 0 });5758  const view = useMemo(() => {59    try {60      const code = q.code.trim();61      const read = JSON.parse(m.shell_read(code, 3, 2, q.r));62      const at = Math.max(0, Math.min(q.at, read.depth));63      const root = q.zoom < 0 ? fits(read) : Math.max(0, Math.min(q.zoom, read.depth));64      const whole = root === read.depth;65      const pick = Math.max(0, Math.min(q.box, read.levels[root].boxes - 1));66      const nodes = m.shell_nodes(code, 3, 2, q.r, root, pick);67      const boxes = widths(nodes);68      return {69        read, at, nodes, boxes, root, pick, whole,70        art: m.shell_pixels(code, 3, 2, q.r, at, whole ? undefined : root, whole ? undefined : pick),71        name: m.name_of(code, 2, 2),72        layout: places(boxes, nodes),73        error: null,74      };75    } catch (error) {76      return { error };77    }78  }, [q.code, q.r, q.at, q.zoom, q.box]);7980  const draw = (canvas) => {81    const { read, nodes, layout, boxes, at, whole } = view;82    if (!read) return;83    const b = board(canvas, 340, { left: 44, right: 16, top: 14, bottom: 14 });84    const { starts, px } = layout;85    const depth = boxes.length - 1;86    const row = (j) => b.y(depth ? j / depth : 0.5);87    if (at <= depth) {88      b.ctx.lineWidth = 1;89      b.ctx.strokeStyle = ink.pink;90      b.ctx.setLineDash([3, 4]);91      b.ctx.beginPath();92      b.ctx.moveTo(b.x(0), row(at));93      b.ctx.lineTo(b.x(1), row(at));94      b.ctx.stroke();95      b.ctx.setLineDash([]);96    }97    const paths = [new Path2D(), new Path2D()];98    for (let j = 0; j < depth; j += 1) {99      for (let k = 0; k < boxes[j]; k += 1) {100        const node = 5 * (starts[j] + k);101        const parent = nodes[node + 3];102        if (parent >= boxes[j + 1]) continue;103        const path = paths[nodes[node + 4]];104        path.moveTo(b.x(px[starts[j] + k]), row(j));105        path.lineTo(b.x(px[starts[j + 1] + parent]), row(j + 1));106      }107    }108    b.ctx.lineWidth = 1;109    b.ctx.strokeStyle = ink.blue;110    b.ctx.stroke(paths[0]);111    b.ctx.strokeStyle = ink.yellow;112    b.ctx.stroke(paths[1]);113    const dot = Math.max(1, Math.min(3, b.wide / boxes[0]));114    for (let j = 0; j <= depth; j += 1) {115      for (let k = 0; k < boxes[j]; k += 1) {116        b.ctx.fillStyle = nodes[5 * (starts[j] + k) + 4] ? ink.yellow : ink.blue;117        b.ctx.fillRect(b.x(px[starts[j] + k]) - dot / 2, row(j) - dot / 2, dot, dot);118      }119    }120    if (!whole) {121      b.ctx.strokeStyle = ink.green;122      b.ctx.lineWidth = 1.5;123      b.ctx.beginPath();124      b.ctx.arc(b.x(px[starts[depth]]), row(depth), 5, 0, Math.PI * 2);125      b.ctx.stroke();126    }127    b.ctx.fillStyle = ink.dim;128    b.ctx.textAlign = 'right';129    for (let j = 0; j <= depth; j += 1) b.ctx.fillText(`j=${j}`, b.left - 8, row(j) + 4);130    b.ctx.textAlign = 'left';131    if (at <= depth) {132      const count = whole ? `${boxes[at]}` : `${boxes[at]} of ${read.levels[at].boxes}`;133      tag(b, `${count} boxes at level ${at}`, ink.pink, 'right', b.x(1), row(at) - 8);134    }135  };136137  const read = view.read;138  const rows = read ? read.levels : [];139  const roots = read ? read.levels[view.root].boxes : 1;140141  const controls = (142    <>143      <Group name="Design">144        <Picker dimension={2} code={q.code} seeds={s} onChange={set} />145      </Group>146      <Group name="Circle">147        <Slider label="radius r" value={q.r} min={1} max={RMAX} onChange={(v) => set({ r: v })} />148        <Slider label="level j" value={view.at ?? 0} min={0} max={read ? read.depth : 1} onChange={(v) => set({ at: v })} />149      </Group>150      <Group name="Zoom">151        <Slider label="root" value={q.zoom} min={-1} max={read ? read.depth : 1}152          show={q.zoom < 0 ? `fit j=${view.root ?? 0}` : view.whole ? 'whole tree' : `j=${view.root ?? q.zoom}`}153          onChange={(v) => set({ zoom: v, box: 0 })} />154        <Slider label="box" value={view.pick ?? 0} min={0} max={Math.max(0, roots - 1)}155          show={`${(view.pick ?? 0) + 1}/${roots}`} onChange={(v) => set({ box: v })} />156      </Group>157    </>158  );159160  return (161    <Page crumb="shell" title="A circle on a carpet is a tree"162      sub={<>Draw the circle of radius <code>r</code> cells about the corner and keep the cells it crosses. Zoom out by threes: the crossed cells fall into crossed boxes, those into fewer boxes, and at last into one. That is a rooted tree, its leaves the crossed cells, and the carpet keeps only the leaves whose path never sat in a centre seat. Drag <code>r</code> and both panels move together. A wide radius gives more leaves than a panel has pixels, so the zoom picks one box of one level and draws only the branch hanging under it, ringed in the picture and at the top of the tree, with every count in that branch still exact.</>}163      controls={controls}164      foot={<>The picture is the design at the least level that holds the circle: its cells are the faint ground, a crossed cell the design fills is yellow, a crossed cell it drops is blue, and the boxes of the chosen level are outlined in pink so you can count them against the row of the table. The tree draws the same boxes, one row per level, an edge from every box to its parent, and the same two inks; the dashed rule marks the chosen level. Every box, every count and every colour comes from <code>mrlymath::shape::crossing_tree</code> through wasm, walked once along the arc in exact integers with no square root taken twice. The zoom is the same walk rooted lower down: a box's children are contiguous among its own level, so one branch is one range a level and nothing is dropped or thinned to make it fit, which a leaf cap could not promise. The <code>root</code> slider rests on <code>fit</code>, the deepest branch that still fits the panel, until you move it, and its top step is the tree's own root, which is the whole tree again. The table stays the whole circle, so the row beside the drawn level says how many of its boxes the branch carries. The same circle counted radius by radius instead of level by level is <a href="../crop">crop</a>, and the whole count with its proofs is on <a href="/research/crop/">the crop page</a>.</>}>165      <p><span className="chip proved">Proved</span> The circle crosses exactly <code>2r + 1</code> cells of the whole grid at every integer <code>r &gt;= 1</code>, and the level-<code>j</code> boxes carrying a crossed cell are the whole grid's crossing shell at the real radius <code>r / 3^j</code>, so there are <code>2 floor(r / 3^j) + 1</code> of them. Summing the children over one level gives the branching identity, so a box of level <code>j + 1</code> has <code>3 + (2k - 2) / (2Q + 1)</code> children on average with <code>Q = floor(r / 3^(j+1))</code> and <code>floor(r / 3^j) = 3Q + k</code>: exactly three at every level where <code>floor(r / 3^j)</code> is <code>1 mod 3</code>.</p>166      <div className="arena">167        <div className="panel">168          <h2>The circle on the design <span>{read && `level ${read.depth}, side ${read.side}`}</span></h2>169          {view.art && <Pixels data={view.art} role="img" aria-label="The design with the crossed cells lit and one level's boxes outlined" />}170        </div>171        <div className="panel">172          <h2>The crossing shell as a tree <span>{read && (view.whole ? `depth ${read.depth}, ${read.leaves} leaves` : `box ${view.pick + 1} of level ${view.root}, ${view.boxes[0]} of ${read.leaves} leaves`)}</span></h2>173          <Sketch draw={draw} deps={[view]} className="bars" role="img" aria-label="The crossing shell drawn as a rooted tree, surviving leaves yellow and pruned ones blue" />174        </div>175      </div>176      <Stats>177        <Stat label="design">{view.name}</Stat>178        <Stat label="radius">{q.r}</Stat>179        <Stat label="depth">{read?.depth}</Stat>180        <Stat label="leaves 2r + 1">{read?.leaves}</Stat>181        <Stat label="kept leaves C(r)">{read?.live}</Stat>182        <Stat label="pruned">{read && read.leaves - read.live}</Stat>183        <Stat label="counts exact">{read && (read.exact ? 'yes' : 'no')}</Stat>184        <Stat label="boxes without a parent">{read?.orphans}</Stat>185      </Stats>186      <div className="panel">187        <h2>Every level, against the identity <span>click a row to outline that level</span></h2>188        <div className="scroll">189          <table>190            <thead><tr><th>level j</th><th>boxes</th><th>2 floor(r / 3^j) + 1</th><th>kept</th><th>children per box</th><th>reading</th></tr></thead>191            <tbody>192              {rows.map((row) => (193                <tr key={row.level} className={row.level === view.at ? 'on' : undefined} onClick={() => set({ at: row.level })}>194                  <td className="num">{row.level}</td>195                  <td className="num">{row.boxes}</td>196                  <td className="num">{row.want}</td>197                  <td className="num">{row.live}</td>198                  <td className="num">{row.level ? row.branch.toFixed(6) : '-'}</td>199                  <td className="num">{row.level ? (row.three ? 'exactly 3' : '') : `${row.boxes} crossed cells`}</td>200                </tr>201              ))}202            </tbody>203          </table>204        </div>205      </div>206      <Note error={view.error} />207    </Page>208  );209}210211mount(<App />);