index.jsx

7.2 kB · jsx · 120 lines

1import { useMemo } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Group, Pick, Slider, Stats, Stat, Note } from '../../lib/app.jsx';4import { Grid, Pixels } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';6import { useSeeds, seeded, Picker, Ramp } from '../../lib/select.jsx';78const m = await ready();9const SIDES = [2, 3, 5];10const DEEPEST = 5;11const BUDGET = 65536;1213const cap = (side) => Math.min(DEEPEST, m.level_cap(side, 2, BUDGET));14const wrap = (value, span) => ((value % span) + span) % span;1516function App() {17  const s = useSeeds();18  const [pick, set] = useQuery({19    code: seeded(s, 2, 3, '495'), side: 3, base: 3, level: 4,20    t1: 0, t2: 0, bar: 10, ramp: 'heat', levels: 32, invert: false,21  });2223  const code = pick.code.trim();24  const level = Math.max(1, Math.min(pick.level, cap(pick.side)));25  const span = pick.side ** level;26  const t1 = wrap(pick.t1, span);27  const t2 = wrap(pick.t2, span);2829  const built = useMemo(() => {30    try {31      return {32        grid: m.two_grid(code, pick.side, level, 0, pick.base),33        field: m.modes_field(code, pick.side, level, pick.base),34        digits: m.modes_digits(code, pick.side, pick.base),35        name: m.name_of(code, 2, pick.base),36        error: null,37      };38    } catch (error) {39      return { grid: null, field: null, digits: 0, name: '', error };40    }41  }, [code, pick.side, level, pick.base]);4243  const read = useMemo(() => built.field && Float32Array.from(built.field, (v) => v ** (1 / level)), [built.field, level]);44  const heat = useMemo(() => read && m.paint_span(read, span, 0, 1, pick.ramp, pick.levels, pick.invert), [read, pick.ramp, pick.levels, pick.invert]);45  const wave = useMemo(() => {46    if (built.error) return null;47    return m.paint_span(m.modes_pattern(code, pick.side, level, pick.base, t1, t2), span, -1, 1, 'diverge', pick.levels, false);48  }, [built.field, t1, t2, pick.levels]);49  const value = useMemo(() => (built.error ? null : m.modes_value(code, pick.side, level, pick.base, t1, t2)), [built.field, t1, t2]);50  const large = useMemo(() => (built.error ? 0 : m.modes_large(code, pick.side, level, pick.base, pick.bar / 100)), [built.field, pick.bar]);5152  const mass = built.digits ** level;53  const seek = (event) => {54    const box = event.currentTarget.getBoundingClientRect();55    const col = Math.floor(((event.clientX - box.left) / box.width) * span);56    const row = Math.floor(((event.clientY - box.top) / box.height) * span);57    set({ t1: Math.max(0, Math.min(span - 1, row)), t2: Math.max(0, Math.min(span - 1, col)) });58  };5960  const controls = (61    <>62      <Group name="Design">63        <Picker dimension={2} bases={[3, 2]} code={pick.code} base={pick.base} seeds={s} onChange={set} />64        <Pick label="side" value={pick.side} options={SIDES.map((v) => [v, v])} onChange={(v) => set({ side: +v, level: Math.min(level, cap(+v)) })} />65        <Slider label="level" value={level} min={1} max={cap(pick.side)} show={`${level}, ${span} by ${span}`} onChange={(v) => set({ level: v, t1: wrap(t1, pick.side ** v), t2: wrap(t2, pick.side ** v) })} />66      </Group>67      <Group name="Frequency">68        <Slider label="t1" value={t1} min={0} max={span - 1} onChange={(v) => set({ t1: v })} />69        <Slider label="t2" value={t2} min={0} max={span - 1} onChange={(v) => set({ t2: v })} />70      </Group>71      <Group name="Large values">72        <Slider label="threshold" value={pick.bar} min={0} max={100} show={`${pick.bar}% of fill^level, ${large} frequencies`} onChange={(v) => set({ bar: v })} />73      </Group>74      <Group name="Colour">75        <Ramp value={pick} onChange={set} />76      </Group>77    </>78  );7980  return (81    <Page crumb="modes" title="The modes of a design mask"82      sub={<>Lay a design's stencil at the level over every point of a <code>side^level</code> by <code>side^level</code> torus and add up what it covers. That operator has one family of modes, the waves <code>e(&lt;t, x&gt; / side^level)</code>, and each one is stretched by a single number. Pick a frequency and watch its wave; the middle panel is the whole field of those numbers at once.</>}83      controls={controls}84      foot={<>The design is the picker's plane code at its side and its residue base; the filled cells of its level-one tile are the digit set <code>F</code>, and the stencil at the level is every sum of <code>level</code> of them scaled by the powers of the side. The eigenvalue is a product over the digits, so the field <code>|lambda|</code> is <code>level</code> rescaled copies of one small transform multiplied together, which is why it repeats at every scale like the design itself. The middle panel reads the field at its per-level root, <code>|lambda|^(1/level) / fill</code>, the average size of one factor; the printed numbers are the raw ones. The same design stacked over its own scales is <a href="../moire">moire</a>, turned on itself <a href="../radial">radial</a>, and joined into a network whose Laplacian has its own spectrum on <a href="../spectra">spectra</a>; the same stencil run as a neighbourhood is <a href="../mrlylife">mrlylife</a>. Every eigenvalue, every count and every wave comes out of the crates through wasm; the page only draws.</>}>85      <p><span className="chip proved">Proved</span> On the torus <code>(Z/side^level)^2</code> the mask operator <code>(A x)(u) = sum over s in S_level of x(u + s)</code> holds every character <code>e(&lt;t, x&gt; / side^level)</code> fixed in direction, with eigenvalue <code>lambda(t) = prod over j &lt; level of hat F(side^j t / side^level)</code> where <code>hat F(y) = sum over v in F of e(&lt;v, y&gt;)</code>, so <code>lambda(0) = fill^level</code>.</p>86      <div className="arena">87        <div className="panel">88          <h2>The mask <span>{`level ${level}, ${mass} cells of ${span * span}`}</span></h2>89          {built.grid && <Grid grid={built.grid} on={ink.yellow} role="img" aria-label="The design mask" />}90        </div>91        <div className="panel">92          <h2>The eigenvalue field <span>{`|lambda|^(1/${level}) / fill, click to pick t`}</span></h2>93          {heat && <Pixels data={heat} onPointerDown={seek} role="img" aria-label="The eigenvalue field on the frequency torus" />}94        </div>95        <div className="panel">96          <h2>The mode <span>{`t = (${t1}, ${t2})`}</span></h2>97          {wave && <Pixels data={wave} role="img" aria-label="The real mode of the chosen frequency" />}98        </div>99      </div>100      <Stats>101        <Stat label="design">{built.name}</Stat>102        <Stat label="side">{pick.side}</Stat>103        <Stat label="base">{pick.base}</Stat>104        <Stat label="digits">{built.digits}</Stat>105        <Stat label="mass fill^level">{mass}</Stat>106        <Stat label="torus">{`${span} by ${span}`}</Stat>107      </Stats>108      <Stats>109        <Stat label="lambda re">{value ? value[0].toFixed(6) : ''}</Stat>110        <Stat label="lambda im">{value ? value[1].toFixed(6) : ''}</Stat>111        <Stat label="|lambda| / fill^level">{value ? value[2].toFixed(6) : ''}</Stat>112        <Stat label="large values">{`${large} at ${pick.bar}% of fill^level`}</Stat>113        <Stat label="share">{`${((large / (span * span)) * 100).toFixed(3)}%`}</Stat>114      </Stats>115      <Note error={built.error} />116    </Page>117  );118}119120mount(<App />);