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