index.jsx
27.1 kB · jsx · 523 lines
1import { useEffect, useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { board, line, axis, tag } from '../../lib/chart.js';4import { faces } from '../../lib/stage.js';5import { mount, Page, Row, Pick, Slider, Check, Btn, Stat, Note } from '../../lib/app.jsx';6import { Grid, Pixels, Sketch } from '../../lib/draw.jsx';7import { Stage } from '../../lib/stage.jsx';8import { Picker, useSeeds, roll } from '../../lib/select.jsx';9import { useQuery, stamp, share } from '../../lib/query.js';1011const m = await ready();12const MAX_SLOTS = 6;13const PLANE = 243;14const SOLID = 128;15const CUBES = 150000;1617const WORDS = {18 doctest: { view: 'plane', compare: 'swap', chart: 'exponent', letters: [['7', 2, 3], ['14', 2, 7], ['9', 2, 5]] },19 periodic: { view: 'plane', compare: 'double', chart: 'exponent', letters: [['7', 2, 3], ['9', 2, 5]] },20 staircase: { view: 'nest', compare: 'double', chart: 'stair', letters: [['7', 2, 3], ['7', 2, 3], ['7', 2, 5], ['7', 2, 3], ['7', 2, 5], ['7', 2, 7]] },21 order: { view: 'plane', compare: 'swap', chart: 'exponent', letters: [['3', 2, 2], ['6', 2, 2]] },22 morse: { view: 'nest', compare: 'swap', chart: 'exponent', letters: [['3', 2, 2], ['7', 2, 2], ['7', 2, 2], ['3', 2, 2], ['7', 2, 2], ['3', 2, 2]] },23 collide: { view: 'plane', compare: 'collide', chart: 'exponent', letters: [['9', 2, 2], ['273', 3, 3]] },24 sponge: { view: 'solid', compare: 'swap', chart: 'stair', letters: [['23', 2, 3], ['9', 2, 3]] },25};2627const NAMES = [28 ['doctest', 'the constructor doctest word'],29 ['periodic', 'a pair, and the same pair doubled'],30 ['staircase', 'the staircase, three blocks'],31 ['order', 'the minimal order pair, swapped'],32 ['morse', 'Thue-Morse over the gasket and the domino'],33 ['collide', 'one tile, two words'],34 ['sponge', 'a solid word'],35];3637const COLLISION = [38 { codes: ['9', '273'], numbers: [2, 3], bases: [2, 3] },39 { codes: ['273', '9'], numbers: [3, 2], bases: [3, 2] },40];4142const SAYS = {43 double: 'Block reduction: a word repeated is not a word, it is a letter. The doubled word is the ordinary self-similar theory of its one-period composite, at side the product of the sides and fill the product of the fills, so the side and the fill square and the dimension does not move. Proved, research/magic.md.',44 swap: 'Order is the object. Side, fill, density and the main-diagonal count are functions of the letter multiset alone, so a swap moves none of them; components, Euler characteristic, holes and boundary are order-sensitive, so a swap moves the piece count. Proved, research/magic.md and research/connectivity.md.',45 collide: 'Factorisation in the tile monoid is not unique. I(2) x I(3) and I(3) x I(2) are the same side-6 tile spelt by two different words, and the identity (nm - 1) - x = (n - 1 - i) m + (m - 1 - j) is symmetric in the two sides, so the collision happens at every pair of sides. Both sides here are prime, so all four letters are irreducible. Proved, cited rather than claimed, research/magic.md. A base-3 code whose digit set is not a parity rule is a tile and not a design, and inherits nothing from the design census.',46};4748const CHARTS = {49 exponent:50 'Over the 15 plane codes at side two, where both letters occur with strictly positive frequency, the component exponent exists, depends on the letter frequencies alone, and equals the fill exponent on 104 of the 105 letter pairs; the periodic control at the same frequencies lands on the same limit, so a difference against the prediction refutes the prediction and not stationarity. The constant-word functional Phi(f) = (f_6 + f_9) log 2 is then refuted on 78 of the 105 letter pairs and exact on 27. Proved, research/connectivity.md. The interior-frequency hypothesis rides inside the statement: at a boundary frequency three words over one pair give rates 0, log 2 and no limit at all.',51 outside:52 'These two letters lie outside the alphabet the closed forms cover, the 15 plane codes at side two, so the curves below are exact counts of the drawn word and no rate is claimed for them. Switch to a pair of side-2 letters, or to the Thue-Morse preset, for the proved reading. Research/connectivity.md.',53 stair:54 'The staircase stacks prefixes, so the letter in place j occurs n - j + 1 times in the first n blocks and the dimension is the occurrence-weighted average of the per-letter dimensions. It is not monotone: it dips at the second block because the base-5 carpet is less dense than the base-3 carpet, then climbs, and its limit is the ambient dimension. Proved, research/magic.md; the five printed values are Verified, lab/py/slice-ladder-controls.',55};5657const FIRST = { view: 'plane', compare: 'swap', chart: 'exponent', schedule: 'thue-morse', length: 64, blocks: 5 };5859const attempt = (fn) => {60 try {61 return { ...fn(), error: null };62 } catch (error) {63 return { error };64 }65};6667const side = (value) => Math.min(16, Math.max(2, +value || 2));6869function token(dimension, codes, numbers, bases) {70 try {71 return m.magic_name(codes, numbers, bases, dimension);72 } catch {73 return '';74 }75}7677function keyed(dimension, codes, numbers, bases) {78 try {79 return m.magic_key(codes, numbers, bases, dimension);80 } catch {81 return '';82 }83}8485function repeats(dimension, codes, numbers, bases) {86 const seen = new Map();87 for (let i = 0; i < codes.length; i++) {88 const key = `${m.name_of(codes[i], dimension, bases[i])} at side ${numbers[i]}`;89 seen.set(key, (seen.get(key) ?? 0) + 1);90 }91 const doubled = [...seen].filter(([, count]) => count > 1);92 if (!doubled.length) return '';93 return `There is no level control: the word length is the depth, and a repeated letter is how a level is spelt. This word repeats ${doubled.map(([key, count]) => `${key} ${count} times`).join(', ')}.`;94}9596function flags(census) {97 return [98 census.constant && <span key="constant" className="chip proved">constant</span>,99 census.periodic && <span key="periodic" className="chip proved">periodic</span>,100 census.composite && <span key="composite" className="chip proved">composite at base {census.residue_base}</span>,101 census.native && <span key="native" className="chip verified">native</span>,102 ].filter(Boolean);103}104105function nesting(codes, numbers, bases, taken) {106 const full = m.magic_grid(codes.slice(0, taken), numbers.slice(0, taken), bases.slice(0, taken));107 const size = full.width;108 const field = new Float32Array(size * size);109 for (let depth = 1; depth <= taken; depth++) {110 const grid =111 depth === 1112 ? m.two_grid(codes[0], numbers[0], 1, 0, bases[0])113 : m.magic_grid(codes.slice(0, depth), numbers.slice(0, depth), bases.slice(0, depth));114 const step = size / grid.width;115 for (let r = 0; r < size; r++) {116 const row = Math.floor(r / step) * grid.width;117 for (let c = 0; c < size; c++) {118 if (grid.types[row + Math.floor(c / step)]) field[r * size + c] += 1;119 }120 }121 }122 return m.sheet(field, size, 'fire', taken, false);123}124125function tile(word, budget) {126 const taken = m.magic_cap(word.numbers, word.dimension, budget);127 if (taken < 2) throw new Error('the first two letters already pass the page budget; lower a side.');128 const grid = m.magic_grid(word.codes.slice(0, taken), word.numbers.slice(0, taken), word.bases.slice(0, taken));129 const census = JSON.parse(m.magic_census(word.codes, word.numbers, word.dimension, word.bases));130 return { word, taken, grid, census, name: token(word.dimension, word.codes, word.numbers, word.bases) };131}132133function firstSlots(params, dimension) {134 const list = [];135 if (params.has('w')) {136 try {137 const read = JSON.parse(m.magic_parse(params.get('w')));138 for (const [i, code] of read.codes.entries()) list.push({ code, base: read.bases[i], number: read.numbers[i] });139 for (let i = 0; i < MAX_SLOTS; i++) stamp({ [`l${i}code`]: null, [`l${i}base`]: null, [`l${i}n`]: null });140 } catch {141 list.length = 0;142 }143 } else {144 for (let i = 0; i < MAX_SLOTS; i++) {145 if (!params.has(`l${i}code`)) break;146 list.push({ code: params.get(`l${i}code`), base: +(params.get(`l${i}base`) ?? 2), number: +(params.get(`l${i}n`) ?? 3) });147 }148 }149 if (list.length >= 2) return list;150 return WORDS[dimension === 3 ? 'sponge' : 'doctest'].letters.map(([code, base, number]) => ({ code, base, number }));151}152153function App() {154 const shared = useSeeds();155 const [pick, set] = useQuery(FIRST);156 const [slots, setSlots] = useState(() => firstSlots(new URLSearchParams(location.search), pick.view === 'solid' ? 3 : 2));157 const [preset, setPreset] = useState('doctest');158 const [art, setArt] = useState(false);159 const sheetRef = useRef(null);160 const [spin, setSpin] = useState(true);161 const [iso, setIso] = useState(false);162 const [probe, setProbe] = useState('4');163 const [shout, setShout] = useState(null);164165 const dimension = pick.view === 'solid' ? 3 : 2;166 const codes = slots.map((slot) => slot.code.trim());167 const numbers = slots.map((slot) => side(slot.number));168 const bases = slots.map((slot) => (dimension === 2 ? slot.base : 2));169 const name = token(dimension, codes, numbers, bases);170 const key = keyed(dimension, codes, numbers, bases);171 const sig = `${pick.view}|${JSON.stringify(slots)}`;172173 useEffect(() => {174 const values = { w: key || null };175 for (let i = 0; i < MAX_SLOTS; i++) {176 values[`l${i}code`] = i < slots.length ? codes[i] : null;177 values[`l${i}base`] = i < slots.length && dimension === 2 ? bases[i] : null;178 values[`l${i}n`] = i < slots.length ? numbers[i] : null;179 }180 stamp(values);181 }, [sig]);182183 useEffect(() => {184 setShout(null);185 }, [sig, pick.compare, pick.chart, pick.schedule, pick.length, pick.blocks, art, spin, iso, probe]);186187 const budget = dimension === 3 ? SOLID : PLANE;188 const word = attempt(() => {189 const taken = m.magic_cap(numbers, dimension, budget);190 if (taken < 2) throw new Error(`the first two letters already pass side ${budget}; lower a side.`);191 return { taken, census: JSON.parse(m.magic_census(codes, numbers, dimension, bases)) };192 });193 const cut = (list) => list.slice(0, word.taken);194 const shape =195 pick.view === 'solid' && !word.error196 ? attempt(() => {197 const count = Number(m.word_count(cut(codes), cut(numbers), 3, cut(bases)));198 if (count > CUBES) throw new Error(`${count} cubes is more than this page draws; drop a letter or lower a side.`);199 return { buffer: m.magic_faces(cut(codes), cut(numbers), cut(bases)) };200 })201 : { buffer: null, error: null };202203 const pair = attempt(() => {204 if (dimension !== 2 && pick.compare !== 'collide') throw new Error('the pair panel is a plane reading; switch the view off solid.');205 const plain = { codes, numbers, bases, dimension };206 let left = plain;207 let right = plain;208 if (pick.compare === 'double') {209 right = { codes: codes.concat(codes), numbers: numbers.concat(numbers), bases: bases.concat(bases), dimension };210 } else if (pick.compare === 'swap') {211 const order = (list) => [list[1], list[0], ...list.slice(2)];212 right = { codes: order(codes), numbers: order(numbers), bases: order(bases), dimension };213 } else {214 left = { ...COLLISION[0], dimension: 2 };215 right = { ...COLLISION[1], dimension: 2 };216 }217 const a = tile(left, PLANE);218 const b = tile(right, PLANE);219 const same = a.grid.width === b.grid.width && a.grid.types.every((byte, i) => byte === b.grid.types[i]);220 const square = (text) => (BigInt(text) * BigInt(text)).toString();221 const reads = [];222 if (pick.compare === 'double') {223 reads.push(['side squares', b.census.side === square(a.census.side)]);224 reads.push(['fill squares', b.census.fill === square(a.census.fill)]);225 reads.push(['dimension unmoved', Math.abs(a.census.dimension - b.census.dimension) < 1e-12]);226 } else if (pick.compare === 'swap') {227 reads.push(['side equal', a.census.side === b.census.side]);228 reads.push(['fill equal', a.census.fill === b.census.fill]);229 reads.push(['pieces equal', a.census.components === b.census.components]);230 } else {231 reads.push(['same tile', same]);232 reads.push(['same word', false]);233 }234 return { a, b, reads };235 });236237 const chart = attempt(() => {238 if (pick.chart === 'stair') {239 const read = JSON.parse(m.magic_staircase(pick.blocks));240 const rows = read.rows.map((row) => row.dimension);241 const at = (i) => (rows.length > 1 ? i / (rows.length - 1) : 0);242 const low = Math.min(read.constant, ...rows);243 const high = Math.max(read.constant, ...rows);244 const pad = (high - low) * 0.25 || 0.001;245 const options = {246 low: low - pad,247 high: high + pad,248 marks: [[read.constant, ink.blue, `the constant word ${read.constant.toFixed(9)}`]],249 lines: [[rows.map((value, i) => [at(i), value]), ink.yellow, 1.8]],250 labels: [[0, 'one block'], [1, `${pick.blocks} blocks`]],251 };252 const stats = (253 <>254 <span><span className="swatch" style={{ background: ink.yellow }}></span> staircase dimension</span>255 {read.rows.map((row) => <span key={row.blocks}>{row.blocks} blocks, {row.length} letters <b>{row.dimension.toFixed(9)}</b></span>)}256 <span>dips at the second block <b>{String(read.rows[1].dimension < read.rows[0].dimension)}</b></span>257 </>258 );259 return { options, stats, say: CHARTS.stair };260 }261 if (dimension !== 2) throw new Error('the component exponent is a plane reading; switch the view off solid.');262 const read = JSON.parse(m.magic_rates(codes, numbers, bases, pick.schedule, pick.length));263 const total = read.length;264 const at = (i) => (total > 1 ? i / (total - 1) : 0);265 const component = read.rows.map((row, i) => [at(i), row[0]]);266 const fill = read.rows.map((row, i) => [at(i), row[1]]);267 const control = read.control.map((value, i) => [at(i), value]);268 const values = read.rows.flat().concat(read.control, [read.phi, read.limit]);269 const options = {270 low: Math.min(0, ...values),271 high: Math.max(...values) * 1.08,272 marks: [273 [read.limit, ink.green, `interior exponent ${read.limit.toFixed(9)}`],274 [read.phi, ink.pink, `Phi(f) ${read.phi.toFixed(4)}`],275 ],276 lines: [[fill, ink.blue, 1], [control, ink.orange, 1.2], [component, ink.yellow, 1.8]],277 labels: [[0, 'length 1'], [1, `length ${total}`]],278 };279 const last = read.rows[total - 1];280 const stats = (281 <>282 <span><span className="swatch" style={{ background: ink.yellow }}></span> {read.schedule} component rate <b>{last[0].toFixed(9)}</b></span>283 <span><span className="swatch" style={{ background: ink.orange }}></span> periodic control <b>{read.control[total - 1].toFixed(9)}</b></span>284 <span><span className="swatch" style={{ background: ink.blue }}></span> fill rate <b>{last[1].toFixed(9)}</b></span>285 <span><span className="swatch" style={{ background: ink.green }}></span> interior exponent <b>{read.limit.toFixed(15)}</b></span>286 <span><span className="swatch" style={{ background: ink.pink }}></span> Phi(f) <b>{read.phi.toFixed(9)}</b></span>287 <span>letters <b>{read.letters.join(' and ')}</b></span>288 <span>lengths <b>1 to {total}</b></span>289 <span className={`chip ${read.alphabet ? 'proved' : 'conjecture'}`}>{read.alphabet ? 'inside the closed-form alphabet' : 'outside the closed-form alphabet'}</span>290 </>291 );292 return { options, stats, say: read.alphabet ? CHARTS.exponent : CHARTS.outside };293 });294295 const patch = (i, values) => setSlots(slots.map((slot, k) => (k === i ? { ...slot, ...values } : slot)));296297 const load = (key) => {298 setPreset(key);299 setSlots(WORDS[key].letters.map(([code, base, number]) => ({ code, base, number })));300 set({ view: WORDS[key].view, compare: WORDS[key].compare, chart: WORDS[key].chart, ...(key === 'morse' ? { schedule: 'thue-morse' } : {}) });301 };302303 const randomize = () => {304 const seed = shared.next();305 const drawn = m.random_codes(dimension, 2, seed, slots.length);306 const sides = roll(seed, slots.map(() => [2, 16]));307 setSlots(slots.map((slot, i) => ({ code: drawn[i], base: 2, number: sides[i] })));308 };309310 const swap = (i) => {311 const next = [...slots];312 const at = (i + 1) % next.length;313 [next[i], next[at]] = [next[at], next[i]];314 setSlots(next);315 };316317 const png = () => {318 if (pick.view === 'solid') {319 setShout(new Error('the solid view saves no picture, and nothing in the crates writes OBJ.'));320 return;321 }322 const canvas = sheetRef.current;323 if (!canvas) return;324 const link = document.createElement('a');325 link.download = `${key || 'word'}.png`;326 link.href = canvas.toDataURL('image/png');327 link.click();328 };329330 const member = () => {331 const text = probe.trim();332 if (!text) return '';333 try {334 return String(m.word_member(codes, numbers, dimension, bases, text));335 } catch (error) {336 return String(error.message ?? error);337 }338 };339340 const sheet = () => {341 if (word.error || pick.view === 'solid') return null;342 if (pick.view === 'nest') return <Pixels data={nesting(codes, numbers, bases, word.taken)} canvasRef={sheetRef} role="img" aria-label="the word" />;343 return <Grid grid={m.magic_grid(cut(codes), cut(numbers), cut(bases))} on={ink.yellow} canvasRef={sheetRef} role="img" aria-label="the word" />;344 };345346 const onStage = (live) => {347 if (pick.view !== 'solid' || !shape.buffer) {348 live.clear();349 return;350 }351 live.show(faces(shape.buffer, ink.blue, 1));352 live.project(iso ? 'iso' : 'eye');353 live.spin = spin ? 0.004 : 0;354 };355356 const drawChart = (canvas) => {357 if (!chart.options) return;358 const b = board(canvas, 240, { left: 46, right: 16 });359 const { low, high, marks, lines, labels } = chart.options;360 const span = high - low || 1;361 const at = (value) => (value - low) / span;362 axis(b, labels, { wall: true });363 for (const [value, color, text] of marks) {364 line(b, [[0, at(value)], [1, at(value)]], color, { width: 1, dash: [4, 4] });365 tag(b, text, color, 'right', b.x(1), b.y(at(value)) - 4);366 }367 for (const [points, color, width] of lines) {368 line(b, points.map(([x, y]) => [x, at(y)]), color, { width, dots: points.length < 12 ? 3 : 0 });369 }370 b.ctx.fillStyle = ink.dim;371 b.ctx.fillText(high.toFixed(3), 2, b.roof + 4);372 b.ctx.fillText(low.toFixed(3), 2, b.floor);373 };374375 const readout = () => {376 const census = word.census;377 const members = m.word_count(codes, numbers, dimension, bases);378 const agrees = members === census.fill;379 let profile = [];380 try {381 profile = m.word_profile(codes, numbers, dimension, bases).map(Number);382 } catch {383 profile = [];384 }385 const peak = profile.length ? Math.max(...profile) : 0;386 const head = Number(members) <= 4096 ? m.word_members(codes, numbers, dimension, bases).slice(0, 6).join(', ') : '';387 return (388 <>389 {name && <Stat label="name">{name}</Stat>}390 <Stat label="side">{census.side}</Stat>391 <Stat label="cells">{census.cells}</Stat>392 <Stat label="filled">{census.fill}</Stat>393 <Stat label="empty">{census.voids}</Stat>394 <Stat label="density">{census.ratio.toFixed(4)}</Stat>395 <Stat label="dimension">{census.dimension.toFixed(4)}</Stat>396 {census.components ? <span>pieces <b>{census.components}</b> <span className="dim">{census.counted}</span></span> : null}397 <span>press members <b>{String(members)}</b> <span className={`chip ${agrees ? 'verified' : 'refuted'}`}>{agrees ? 'agrees with the fill' : 'differs from the fill'}</span></span>398 {profile.length ? <span>diagonal profile <b>{profile.length}</b> heights, peak <b>{peak}</b> at <b>{profile.indexOf(peak)}</b></span> : null}399 {head ? <Stat label="first members">{head}</Stat> : null}400 {flags(census)}401 </>402 );403 };404405 const scale = () => {406 const census = word.census;407 const drawing = word.taken < codes.length408 ? <>Drawing <b>{word.taken} of {codes.length} letters</b>, the box cover of the whole word at side {census.letters.slice(0, word.taken).reduce((a, l) => a * l.number, 1)}. </>409 : null;410 return <>{drawing}The readout is a product over the letters, so it outruns the raster on purpose: every number above is exact at the full length. {repeats(dimension, codes, numbers, bases)} <a href="../moire">Moire</a> stacks one design over its scales instead.</>;411 };412413 const pane = (one) => (414 <div>415 <Grid grid={one.grid} on={ink.yellow} role="img" aria-label={one.name || 'the word'} />416 <div className="stats">417 <span>{one.name}</span>418 <Stat label="side">{one.census.side}</Stat>419 <Stat label="filled">{one.census.fill}</Stat>420 <Stat label="dimension">{one.census.dimension.toFixed(4)}</Stat>421 {one.census.components ? <Stat label="pieces">{one.census.components}</Stat> : null}422 {one.taken < one.word.codes.length ? <span className="dim">drawn {one.taken} of {one.word.codes.length} letters</span> : null}423 </div>424 </div>425 );426427 const picture = (428 <section key="picture">429 <h3>The picture</h3>430 <Row>431 <Check label="art" checked={art} onChange={setArt} />432 <Btn onClick={png}>{pick.view === 'solid' ? 'PNG (plane only)' : 'PNG'}</Btn>433 </Row>434 </section>435 );436437 const controls = art ? picture : (438 <>439 <section>440 <h3>The word</h3>441 <Row>442 <Pick label="preset" value={preset} options={NAMES} onChange={load} />443 <Btn onClick={randomize}>Randomize</Btn>444 <Pick label="view" value={pick.view} options={[['plane', 'plane'], ['nest', 'nesting'], ['solid', 'solid']]} onChange={(v) => set({ view: v })} />445 {pick.view === 'solid' && <Check label="spin" checked={spin} onChange={setSpin} />}446 {pick.view === 'solid' && <Check label="iso" checked={iso} onChange={setIso} />}447 </Row>448 </section>449 <section>450 <h3>The letters</h3>451 {slots.map((slot, i) => (452 <Row key={i}>453 <span className="badge">letter {i + 1}</span>454 <span className="set">455 <Picker dimension={dimension} bases={dimension === 2 ? [2, 3] : [2]} code={slot.code} base={bases[i]} seeds={shared} button={false} onChange={(values) => patch(i, values)} />456 </span>457 <label>side <input type="number" value={slot.number} min={2} max={16} onChange={(e) => patch(i, { number: e.target.value })} /></label>458 <button disabled={slots.length < 2} onClick={() => swap(i)}>swap next</button>459 <button disabled={slots.length < 3} onClick={() => setSlots(slots.filter((one, k) => k !== i))}>remove</button>460 </Row>461 ))}462 <Row>463 <button disabled={slots.length >= MAX_SLOTS} onClick={() => setSlots([...slots, { ...slots[slots.length - 1] }])}>add a letter</button>464 <label>is <input type="text" value={probe} onChange={(e) => setProbe(e.target.value)} /> a member <b className="num">{member()}</b></label>465 <span className="badge dim">{name}</span>466 </Row>467 </section>468 <section>469 <h3>The pair</h3>470 <Row>471 <Pick label="compare" value={pick.compare} onChange={(v) => set({ compare: v })}472 options={[['swap', 'the first two letters swapped'], ['double', 'the same word doubled'], ['collide', 'I(2) x I(3) beside I(3) x I(2)']]} />473 </Row>474 </section>475 <section>476 <h3>The chart</h3>477 <Row>478 <Pick label="chart" value={pick.chart} options={[['exponent', 'the component exponent'], ['stair', 'the staircase dimension']]} onChange={(v) => set({ chart: v })} />479 {pick.chart === 'exponent' && <Pick label="schedule" value={pick.schedule} options={[['thue-morse', 'Thue-Morse'], ['periodic', 'periodic'], ['constant', 'constant']]} onChange={(v) => set({ schedule: v })} />}480 {pick.chart === 'exponent' && <Slider label="length" value={pick.length} min={2} max={98} onChange={(v) => set({ length: v })} />}481 {pick.chart === 'stair' && <Slider label="blocks" value={pick.blocks} min={2} max={8} onChange={(v) => set({ blocks: v })} />}482 </Row>483 </section>484 {picture}485 </>486 );487488 return (489 <Page bare={art} crumb="words" title="One design per level, and the order of the letters counts"490 sub="One design per level. Same letters in a different order, a different set. Build a word letter by letter, first letter outermost, and every number below comes back out of the crates as a product over the letters."491 controls={controls}492 foot={<>Side, fill, density and dimension are products over the letters, so the readout is exact at any length even where the raster is not: the plane draws to side 243 and the cube to side 128, and a shorter render is labelled with the count of letters it drew because it is the box cover of the whole word at that scale and never a shallower word. Words with two or more letters only; a word of one repeated letter is the ordinary fractal under another name. Links: <a href="../sponge">the sponge</a> grows one cube design level by level, <a href="../moire">moire</a> stacks one design over its scales. The grammar of a word, the families that collapse back into one fractal and the ones that do not are in <a href="/research/magic/">the magic words note</a>, and what a swap costs is the paper <a href="/papers/order-sensitivity-of-kronecker-words/">order sensitivity of Kronecker words</a>.</>}>493 <div className="arena">494 <div className="panel">495 <h2>the word <span>{name}</span></h2>496 {sheet()}497 <Stage onStage={onStage} deps={[pick.view, shape.buffer, iso, spin]} hidden={pick.view !== 'solid'} role="img" aria-label="the word in the cube" />498 <div className="stats" hidden={art}>{word.error ? null : readout()}</div>499 <div className="stats" hidden={art}>{word.error ? null : word.census.letters.map((letter, i) => (500 <span key={i} className="badge">{i + 1} <b>{letter.name}</b> side {letter.number} fill {letter.fill} dimension {letter.dimension.toFixed(4)}{letter.native ? ' native' : ''}</span>501 ))}</div>502 <Note error={shout ?? word.error ?? shape.error} />503 <p className="sub" hidden={art}>{word.error ? null : scale()}</p>504 <p className="sub">The first letter alone grows level by level in <a href={`../sponge${share({ code: codes[0], base: bases[0], number: numbers[0], level: 3 })}`}>the sponge</a>.</p>505 </div>506 <div className="panel" hidden={art}>507 <h2>the pair <span>{pick.compare === 'collide' ? 'one tile, two words' : `the word beside ${pick.compare === 'double' ? 'itself doubled' : 'its swap'}`}</span></h2>508 <div className="arena">509 {pair.error ? null : pane(pair.a)}510 {pair.error ? null : pane(pair.b)}511 </div>512 <Note error={pair.error}>{pair.error ? null : pair.reads.map(([text, value], i) => <span key={i}>{text} <b>{String(value)}</b>{' '}</span>)}</Note>513 <p className="sub">{SAYS[pick.compare]}</p>514 </div>515 </div>516 <Sketch draw={drawChart} deps={[chart]} className="bars" hidden={art} role="img" aria-label={pick.chart === 'stair' ? 'The staircase dimension' : 'The component exponent'} />517 <div className="stats" hidden={art}>{chart.error ? String(chart.error.message ?? chart.error) : chart.stats}</div>518 <p className="sub" hidden={art}>{chart.say ?? CHARTS[pick.chart]}</p>519 </Page>520 );521}522523mount(<App />);