index.jsx

14.7 kB · jsx · 271 lines

1import { Fragment, useEffect, useMemo, useRef } from 'react';2import { ready, ink, fit } from '../../lib/mrly.js';3import { stamp, useQuery } from '../../lib/query.js';4import { mount, Page, Row, Slider, Btn, Note } from '../../lib/app.jsx';5import { Grid, Markup, Sketch } from '../../lib/draw.jsx';6import { useSeeds, roll } from '../../lib/select.jsx';7import { Pins, Terms } from '../../lib/series.jsx';89const m = await ready();10const BUDGET = '500000';11const WINDOW = 6;12const NAMED = 3;13const FLAT = [['low corner', '1'], ['tree', '3'], ['carpet', '7'], ['void', '9'], ['corner and centre', '11'], ['solid', '15']];14const RECORDS = JSON.parse(m.ledger_records());15const FLATCAP = m.level_cap(3, 2, 500000);16const CUBECAP = m.level_cap(3, 3, 60000);17const CUTCAP = m.level_cap(2, 3, 500000);1819const row = (code, d, measure, way, count) => JSON.parse(m.ledger_row(code, d, 2, measure, way, count, BUDGET));20const census = (code, n) => JSON.parse(m.two_census(code, n, 1, 0, 2));21const slice = (n) => JSON.parse(m.slice_census('23', n, 1, 2));22const odds = (k) => Array.from({ length: k }, (_, i) => 2 * i + 1);23const upto = (k) => Array.from({ length: k }, (_, i) => i + 1);24const least = (n) => Math.max(n, NAMED);25const item = (r, label) => ({ label, terms: r.terms, closed: r.closed, capped: r.capped, ledger: true });2627function hex(number, level, view) {28  const side = number ** level;29  return m.hex_svg('23', number, level, 2, view, Math.max(1, Math.round(65 / side)));30}3132function stackOf(canvas, order) {33  const [ctx, w, h] = fit(canvas, 200);34  ctx.strokeStyle = ink.pink;35  for (const [num, den, bright] of JSON.parse(m.farey(order))) {36    const x = 6 + (w - 12) * num / den;37    ctx.globalAlpha = 0.25 + 0.75 * bright / order;38    ctx.beginPath();39    ctx.moveTo(x, h - 6);40    ctx.lineTo(x, h - 6 - (h - 12) * bright / order);41    ctx.stroke();42  }43}4445const CARDS = [46  {47    key: 'sides', title: 'The odd-side law', wide: true, slider: ['side up to', 4, 8, 4], show: (k) => 2 * k - 1,48    say: 'At odd side 2k - 1 an axis splits into k low and k - 1 high positions, so a flat design fills a polynomial in k, and the six designs of the plane read as the polygonal numbers: the low corner the squares, the tree the hexagonal numbers, the carpet the octagonal numbers, before any of them is a fractal.',49    build: (k) => ({50      lines: FLAT.map(([name, code]) => ({51        label: `${name}, code ${code}`,52        art: odds(k).slice(1).map((n) => <Grid key={n} grid={m.two_grid(code, n, 1, 0, 2)} on={ink.blue} className="" />),53        terms: odds(k).slice(1).map((n) => String(census(code, n).fills)),54        closed: m.ledger_closed(code, 2, 2, 'fills', 'side'),55        ledger: true,56      })),57    }),58  },59  {60    key: 'carpet', title: 'The carpet', slider: ['level', 1, FLATCAP, 3],61    say: 'Eight of nine cells survive every level, so the carpet fills 8^level, and its dimension log 8 / log 3 is this count read against the side 3^level.',62    build: (L) => ({ art: <Grid grid={m.two_grid('7', 3, L, 0, 2)} on={ink.blue} className="" />, lines: [item(row('7', 2, 'fills', 'level', least(L)), 'filled cells')] }),63  },64  {65    key: 'voids', title: 'What the carpet drops', slider: ['level', 1, FLATCAP, 3],66    say: 'The cells the carpet leaves empty are the grid less the fill, 9^level - 8^level, the powers of nine racing the powers of eight.',67    build: (L) => ({ art: <Grid grid={m.two_grid('7', 3, L, 0, 2)} on={ink.deep} off={ink.orange} className="" />, lines: [item(row('7', 2, 'voids', 'level', least(L)), 'empty cells')] }),68  },69  {70    key: 'perimeter', title: 'The perimeter of the carpet', slider: ['level', 1, FLATCAP, 3],71    say: 'Two adjacent blocks bury one edge per spanning position, so the perimeter closes as a sum of the powers 8^level and 3^level, the entry the OEIS lists for the carpet at iteration n.',72    build: (L) => ({ art: <Grid grid={m.two_grid('7', 3, L, 0, 2)} on={ink.yellow} className="" />, lines: [item(row('7', 2, 'surface', 'level', least(L)), 'exposed edges')] }),73  },74  {75    key: 'sponge', title: 'The sponge', slider: ['level', 1, CUBECAP, 2],76    say: 'Twenty of twenty-seven subcubes survive every level, so the sponge fills 20^level.',77    build: (L) => ({ art: <Markup svg={hex(3, L, 'iso')} />, lines: [item(row('23', 3, 'fills', 'level', least(L)), 'filled cells')] }),78  },79  {80    key: 'surface', title: 'The surface of the sponge', slider: ['level', 1, CUBECAP, 2],81    say: 'The same burial count in three dimensions: the exposed faces are a sum of the powers 20^level and 8^level, the surface area of the stage-n Menger sponge.',82    build: (L) => ({ art: <Markup svg={hex(3, L, 'pro')} />, lines: [item(row('23', 3, 'surface', 'level', least(L)), 'exposed faces')] }),83  },84  {85    key: 'tile', title: 'The odd sponge tile', slider: ['side', 2, 8, 3], show: (k) => 2 * k - 1,86    say: 'Widen the sponge tile to odd side 2k - 1 and its fills and voids are cubics in k: the divisor count of 240^n read as a solid, and the entry this tree contributed.',87    build: (k) => ({88      art: <Markup svg={hex(2 * k - 1, 1, 'iso')} />,89      lines: [item(row('23', 3, 'fills', 'side', least(k - 1)), 'filled cells'), item(row('23', 3, 'voids', 'side', least(k - 1)), 'empty cells')],90    }),91  },92  {93    key: 'designs', title: 'How many designs', slider: ['base up to', 2, 6, 4],94    say: 'A design is a Boolean function on the corners up to the symmetries of the cube: in base 2 the count by dimension is the irreducible Boolean functions of the record, and by base the rows are the bracelets, the toroidal squares and the toroidal cubes.',95    build: (Q) => {96      const lines = [{ label: 'base 2, by dimension', terms: m.counting_sequence(4), closed: '' }];97      ['a line', 'a square', 'a cube'].forEach((name, i) => {98        const terms = [];99        for (let q = 2; q <= least(Q - 1) + 1; q++) {100          try {101            terms.push(m.baseq_sequence(q, i + 1)[i]);102          } catch {103            break;104          }105        }106        lines.push({ label: `${name}, by base from 2`, terms, closed: '', capped: terms.length < least(Q - 1) });107      });108      const art = <div className="strip">{JSON.parse(m.universe(2)).designs.map((d) => <Grid key={d.code} grid={m.two_grid(d.code, 3, 2, 0, 2)} on={ink.yellow} className="" />)}</div>;109      return { art, lines };110    },111  },112  {113    key: 'classes', title: 'How many fractals', slider: ['dimension', 1, 8, 4],114    say: 'Two base-2 designs draw the same fractal exactly when they fill the same number of corners of each weight, so the distinct fractals of a dimension number the product over the weights of one more than the corners of that weight.',115    build: (D) => {116      const terms = m.classes_sequence(least(D));117      return {118        art: <Pins terms={terms} start={1} height={200} hue={ink.yellow} label="by dimension" style={{ width: '100%' }} />,119        lines: [{ label: 'fill classes, by dimension', terms, closed: '' }],120      };121    },122  },123  {124    key: 'slices', title: 'The middle slice of the sponge', slider: ['level', 1, CUBECAP, 2],125    say: 'Cut the sponge on its middle diagonal plane and count the filled triangles: one index up from the star holes of the record, whose recurrence gives the slice its dimension.',126    build: (L) => ({127      art: <Markup svg={hex(3, L, 'cut')} />,128      lines: [{ label: 'filled triangles, from the unit hexagon', terms: [String(slice(1).fills), ...m.ledger_terms('23', 3, 2, 'triangles', 'level', least(L - 1) + 1, BUDGET)], closed: '', ledger: true }],129    }),130  },131  {132    key: 'vertices', title: 'The vertices of the slice', slider: ['side', 2, 8, 4], show: (k) => 2 * k - 1,133    say: 'The middle slice of the odd cube is a hexagon whose vertex count at side n is the centered hexagonal number 3n(n + 1) + 1, the sequence the corner-and-centre design fills at odd sides; a prime among them is a difference of consecutive cubes, a cuban prime, and the yellow terms are the primes.',134    build: (k) => {135      const terms = odds(least(k)).map((n) => String(slice(n).vertices));136      return {137        art: <Markup svg={hex(2 * k - 1, 1, 'cut')} />,138        lines: [139          { label: 'vertices, by odd side', terms, marks: terms.map((t) => JSON.parse(m.factor(t)).prime), closed: '' },140          item(row('11', 2, 'fills', 'side', least(k - 1)), 'the corner-and-centre fills, by odd side'),141        ],142      };143    },144  },145  {146    key: 'gasket', title: 'The gasket cut', slider: ['level', 1, CUTCAP, 4],147    say: 'Every diagonal plane through the octahedral design at side 2 holds exactly 3^level points at every admissible height, by the uniqueness of the binary expansion of the height, so the cut is a Sierpinski gasket at every depth.',148    build: (L) => {149      const profiles = upto(least(L)).map((j) => JSON.parse(m.diagonal_profile('126', 2, j, 2)));150      const last = profiles[L - 1];151      return {152        art: <Markup svg={m.diagonal_svg('126', 2, L, 2, last.central, Math.max(2, Math.round(180 / last.side)))} />,153        sub: `${last.heights} heights, ${last.constant ? 'all alike' : 'not all alike'}`,154        lines: [{ label: 'points on a diagonal plane, by level', terms: profiles.map((p) => p.max), closed: '' }],155      };156    },157  },158  {159    key: 'farey', title: 'The Farey nodes', slider: ['order', 1, 24, 12],160    say: 'Stack the carpet at every odd scale and the lit nodes are the Farey fractions, each new scale n lighting phi(n) new nodes: the count is one more than the totients summed, and the primes are the scales of most novelty.',161    build: (order) => {162      const stacks = upto(least(order)).map((j) => JSON.parse(m.farey_novelty(j)));163      const last = stacks[order - 1];164      return {165        art: <Sketch className="" role="img" aria-label="The Farey stack" style={{ width: '100%', height: 200 }} draw={(canvas) => stackOf(canvas, order)} deps={[order]} />,166        sub: `${last.lit} nodes lit, ${last.match ? 'as the totients say' : 'against the totients'}, primes ${last.primes.join(' ')}`,167        lines: [{ label: 'nodes lit, by order', terms: stacks.map((f) => String(f.lit)), closed: '' }],168      };169    },170  },171];172173const FIRST = Object.fromEntries(CARDS.map((spec) => [spec.key, spec.slider[3]]));174175function hits(terms) {176  if (terms.length < NAMED) return { html: <span className="dim">three terms name the record</span>, formula: '' };177  const found = JSON.parse(m.ledger_identify(terms.slice(0, WINDOW).join(', ')));178  const formula = found.length ? RECORDS.find((r) => r.id === found[0].id)?.formula : '';179  const html = found.length180    ? found.slice(0, 3).map((r, i) => (181      <Fragment key={r.id}>182        {i ? ' · ' : null}183        <a className="badge" href={`https://oeis.org/${r.id}`} target="_blank" rel="noopener">{r.id}</a> {r.name}, from index {r.shift}184      </Fragment>185    ))186    : 'no curated record holds these terms';187  return { html, formula: formula && formula !== 'none' ? formula : '' };188}189190function Line({ it }) {191  const found = hits(it.terms);192  const forms = [it.closed, found.formula].filter((form, i, all) => form && all.indexOf(form) === i);193  return (194    <div className="entry">195      {it.art ? <div className="strip">{it.art}</div> : null}196      <div className="text">197        {it.label ? <><span className="dim">{it.label}</span><br /></> : null}198        <Terms terms={it.terms} marks={it.marks} capped={it.capped} tight />199        {forms.map((form, i) => <Fragment key={form}>{i ? ' · ' : null}<span className="mono">{form}</span></Fragment>)}200        {forms.length ? ' · ' : null}201        {found.html}202        {it.ledger ? <> · <a href={`../sequences/?q=${encodeURIComponent(it.terms.join(', '))}`}>open in the ledger</a></> : null}203      </div>204    </div>205  );206}207208function Card({ spec, index, value, onChange, cardRef }) {209  const [label, min, max] = spec.slider;210  const made = useMemo(() => {211    try {212      const built = spec.build(value);213      return { art: built.art, sub: built.sub, lines: built.lines.map((it, i) => <Line key={i} it={it} />), error: null };214    } catch (error) {215      return { lines: [], error };216    }217  }, [value]);218219  return (220    <section className={`panel tour${spec.wide ? ' wide' : ''}`} ref={cardRef} aria-label={spec.title}>221      <div className="pic" role="img" aria-label={spec.title}>{made.art}</div>222      <div className="body">223        <h2>{index + 1} · {spec.title} <span>{made.sub ?? ''}</span></h2>224        <p className="story">{spec.say}</p>225        <Row><Slider label={label} value={value} min={min} max={max} show={spec.show ? spec.show(value) : value} onChange={onChange} /></Row>226        <div>{made.lines}</div>227        <Note error={made.error} />228      </div>229    </section>230  );231}232233function App() {234  const s = useSeeds();235  const [pick, set] = useQuery(FIRST);236  const cards = useRef({});237238  const shuffle = (seed) => {239    const [at] = roll(seed, [[0, CARDS.length - 1]]);240    const spec = CARDS[at];241    const [value] = roll(seed, [[spec.slider[1], spec.slider[2]]]);242    set({ [spec.key]: value });243    cards.current[spec.key]?.scrollIntoView({ behavior: 'smooth', block: 'center' });244  };245246  useEffect(() => {247    stamp(pick);248    if (s.get()) shuffle(s.get());249  }, []);250251  const controls = (252    <Row>253      <Btn onClick={() => shuffle(s.next())}>Randomize</Btn>254      <span className="dim">picks a card and a step for it</span>255    </Row>256  );257258  return (259    <Page crumb="tour" title="A dozen sequences the designs write" controls={controls}260      sub="A design is a rule on the corners of a square or a cube. Grown level by level or widened side by side it counts something, and the count is an integer sequence the OEIS already holds or has just learnt. Every card draws the design live, reads its terms from the crates, and names the record; slide, and the picture and the terms grow together."261      foot={<>A card is one design, one measure and one axis, the same row the <a href="../sequences/">ledger</a> lists, and where a line is such a row the link under its terms opens it with the terms prefilled; the counts of designs, of fractals, of slice vertices, of gasket points and of Farey nodes are not rows of the ledger and carry no link. The level axis grows the fractal at the smallest side the base allows, the side axis holds level one and widens the odd side. The record after the terms is found by the crate from the terms alone, as a window of the entry's own first terms, and the number after it is the entry's index of the first term shown. Every number on this page is computed in Rust; the page only draws. The ledger these cards read from is the <a href="/research/sequences/">sequences</a> note, and the census behind it is the shelf paper <a href="/papers/sequence-census/">the sequence census</a>.</>}>262      <div>263        {CARDS.map((spec, i) => (264          <Card key={spec.key} spec={spec} index={i} value={pick[spec.key]} cardRef={(el) => { cards.current[spec.key] = el; }} onChange={(v) => { s.drop(); set({ [spec.key]: v }); }} />265        ))}266      </div>267    </Page>268  );269}270271mount(<App />);