index.jsx

6.1 kB · jsx · 118 lines

1import { useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Slider, Check, Stats, Stat, Note } from '../../lib/app.jsx';4import { Pixels, Sketch } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';6import { useSeeds, roll, Ramp, Sources, readSource, seedSource, SOURCE_FIRST } from '../../lib/select.jsx';7import { board, bars, axis, tag } from '../../lib/chart.js';89const m = await ready();10const OUT = 512;11const ORDERS = 48;12const RINGS = 160;13const BLENDS = ['mean', 'sum', 'union', 'meet', 'parity', 'difference'];14const FIRST = { ...SOURCE_FIRST, copies: 6, step: 60, blend: 'mean' };1516const drawn = (seed) => {17  const [copies, at] = roll(seed, [[1, 36], [0, BLENDS.length - 1]]);18  let k = at;19  while (BLENDS[k] === 'meet') k = (k + 1) % BLENDS.length;20  return { copies, blend: BLENDS[k] };21};2223function App() {24  const s = useSeeds();25  const [pick, set] = useQuery({ ...seedSource(s, FIRST), ...(s.get() ? drawn(s.get()) : null) });26  const [full, setFull] = useState(() => !new URLSearchParams(location.search).has('step'));27  const [samples, setSamples] = useState(2);28  const [look, setLook] = useState({ ramp: 'fire', levels: 64, invert: false });29  const shown = useRef(null);3031  const step = full ? +m.full_turn(pick.copies).toFixed(3) : pick.step;3233  let error = null;34  try {35    const src = readSource(pick);36    const stack = m.radial(src.field, src.size, OUT, pick.copies, step, pick.blend, samples);37    const pixels = m.sheet(stack, OUT, look.ramp, look.levels, look.invert);38    const power = m.harmonics(src.field, src.size, RINGS, ORDERS);39    const order = m.turns(power);40    shown.current = {41      pixels, name: src.name, side: src.size, order, copies: pick.copies, full, blend: pick.blend,42      step, power: Array.from(power), share: m.radial_share(power),43      petals: full ? (order ? m.petals(pick.copies, order) : 'none') : 'partial orbit',44    };45  } catch (fault) {46    error = fault;47  }4849  const chart = (canvas) => {50    const view = shown.current;51    if (!view) return;52    const b = board(canvas, 300);53    const lives = (k) => (view.full ? k % view.copies === 0 : k === 0);54    bars(b, view.power, { color: (k) => (k === 0 ? ink.yellow : lives(k) ? ink.blue : ink.line) });55    axis(b, view.power.map((_, k) => [(k + 0.5) / view.power.length, k]).filter(([, k]) => k % 4 === 0));56    tag(b, `order 0 carries ${view.share.toFixed(1)}% of the power`, ink.yellow);57    tag(b, view.order ? `live orders are multiples of ${view.order}` : 'no live order', ink.pink, 'right');58  };5960  const view = shown.current;6162  const controls = (63    <>64      <section>65        <h3>Source</h3>66        <Row>67          <Sources value={pick} onChange={set} seeds={s} onSeed={(seed) => { set(drawn(seed)); setFull(true); }} />68        </Row>69      </section>70      <section>71        <h3>Stack</h3>72        <Row>73          <Slider label="copies" value={pick.copies} min={1} max={36} onChange={(v) => set({ copies: v })} />74          <Check label="full turn" checked={full} onChange={(v) => { setFull(v); if (!v) set({ step }); }} />75          <label>step <input type="number" min={0} max={360} step={0.5} value={step} disabled={full} onChange={(e) => set({ step: +e.target.value })} /> degrees</label>76          <Pick label="blend" value={pick.blend} options={BLENDS} onChange={(v) => set({ blend: v })} />77          <Pick label="samples" value={samples} options={[[1, 1], [2, 2], [3, 3]]} onChange={(v) => setSamples(+v)} />78        </Row>79      </section>80      <section>81        <h3>Colour</h3>82        <Row>83          <Ramp value={look} onChange={(patch) => setLook({ ...look, ...patch })} />84        </Row>85      </section>86    </>87  );8889  return (90    <Page crumb="radial" title="Stack a design around its centre"91      sub={<>Copy a design, turn it, lay it on the first, and repeat. A full turn shared out over <code>q</code> copies is what a screen shows when a spinning design advances <code>p/q</code> of a turn per frame: it keeps exactly the circular harmonics whose order is a multiple of <code>q</code>, and a design of rotation order <code>g</code> shows <code>lcm(q, g)</code> petals. The spectrum on the right says which harmonics the design has to give.</>}92      controls={controls}93      foot={<>Each copy is the source turned about its centre; a pixel of the stack averages a few points, each point reading every copy at once and merging them by the blend: their mean, their sum, their union, their meet, their parity, or what the first copy keeps that no other has. The harmonics are exact: every circle about the centre is cut at the grid lines it crosses and the arc integrals of <code>e^(-i m theta)</code> are taken in closed form. Two copies at 45 degrees is the same picture as an afterglow at 900 rpm on a 60 Hz screen with the phase slipping by an eighth of a turn. Which harmonics a turned stack keeps, and the exact circle mean behind them, are in <a href="/research/spin/">the spin note</a>.</>}>94      <div className="arena">95        <div className="panel">96          <h2>The stack <span>{view && `${view.copies} ${view.copies === 1 ? 'copy' : 'copies'}, ${view.blend}`}</span></h2>97          {view && <Pixels data={view.pixels} role="img" aria-label="The stack" />}98        </div>99        <div className="panel">100          <h2>Spin spectrum <span>circular-harmonic power by order</span></h2>101          <Sketch draw={chart} deps={[view]} className="bars" style={{ height: 300 }} role="img" aria-label="Spin spectrum" />102          <p className="foot" style={{ marginTop: 12, paddingTop: 10 }}>Gold is order zero, the infinite spin. Blue orders survive this stack, dim ones are cancelled by it. The rotation order of the source is the gcd of its live orders.</p>103        </div>104      </div>105      <Stats>106        <Stat label="name">{view?.name}</Stat>107        <Stat label="side">{view?.side}</Stat>108        <Stat label="copies">{view?.copies}</Stat>109        <Stat label="step">{view && `${view.step}°`}</Stat>110        <Stat label="rotation order">{view && (view.order || 'round')}</Stat>111        <Stat label="petals">{view?.petals}</Stat>112      </Stats>113      <Note error={error} />114    </Page>115  );116}117118mount(<App />);