import { useMemo } from 'react'; import { ready, ink } from '../../lib/mrly.js'; import { board, line, axis, rules, tag } from '../../lib/chart.js'; import { mount, Page, Group, Pick, Slider, Btn, Stats, Stat, Note } from '../../lib/app.jsx'; import { Pixels, Sketch } from '../../lib/draw.jsx'; import { useQuery } from '../../lib/query.js'; import { useSeeds, seeded, Picker } from '../../lib/select.jsx'; const m = await ready(); const SIDES = [2, 3, 5]; const DEEPEST = 6; const CELLS = 729; const SLIDERS = 9; const CAP = 32; const SAMPLES = 241; const MOMENTS = 161; const REACH = 4; const PAD = 14; const cap = (side) => Math.min(DEEPEST, m.level_cap(side, 1, CELLS)); const sum = (list) => list.reduce((a, b) => a + b, 0); function App() { const s = useSeeds(); const [pick, set] = useQuery({ code: seeded(s, 2, 3, '69'), side: 3, base: 3, level: 4, w: '3,2,3', gamma: 35, curve: 'spectrum', }); const code = pick.code.trim(); const level = Math.max(1, Math.min(pick.level, cap(pick.side))); const span = pick.side ** level; const corners = useMemo(() => { try { return { list: Array.from(m.weights_corners(code, pick.side, pick.base)), error: null }; } catch (error) { return { list: [], error }; } }, [code, pick.side, pick.base]); const k = corners.list.length / 2; const shares = useMemo(() => { const read = pick.w.split(',').map((v) => Math.max(1, Math.min(CAP, Math.round(+v) || 1))); return read.length === k ? read : Array(k).fill(1); }, [pick.w, k]); const key = shares.join(','); const whole = sum(shares); const read = useMemo(() => { if (!k) return { error: corners.error }; try { const w = Float64Array.from(shares); return { dims: m.weights_dims(code, pick.side, pick.base, w), point: m.weights_point(code, pick.side, pick.base, w, 1), spectrum: m.weights_spectrum(code, pick.side, pick.base, w, SAMPLES), pressure: m.weights_pressure(code, pick.side, pick.base, w, -REACH, REACH, MOMENTS), name: m.name_of(code, 2, pick.base), error: null, }; } catch (error) { return { error }; } }, [code, pick.side, pick.base, key]); const drawn = useMemo(() => { if (!k || read.error) return null; const w = Float64Array.from(shares); const mass = m.weights_mass(code, pick.side, level, pick.base, w); let least = Infinity; for (const v of mass) if (v > 0 && v < least) least = v; return { sheet: m.weights_pixels(code, pick.side, level, pick.base, w, pick.gamma / 100), peak: Math.max(...mass), least, cells: mass.reduce((count, v) => count + (v > 0 ? 1 : 0), 0), }; }, [code, pick.side, pick.base, level, key, pick.gamma]); const curve = (canvas) => { if (read.error || !read.dims) return; const b = board(canvas, 340, { pad: PAD, top: 20, bottom: 22 }); const [low, high, one, roof] = Array.from(read.dims); if (pick.curve === 'pressure') { const xs = [], ys = []; for (let i = 0; i < read.pressure.length; i += 2) { xs.push(read.pressure[i]); ys.push(read.pressure[i + 1]); } const floor = Math.min(...ys), top = Math.max(...ys); const pad = (top - floor) * 0.08 || 0.05; const fx = (v) => (v + REACH) / (2 * REACH); const fy = (v) => (v - floor + pad) / (top - floor + 2 * pad); rules(b, [fx(0), fx(1)], { dash: [2, 4] }); line(b, [[0, fy(0)], [1, fy(0)]], ink.line, { width: 1 }); line(b, xs.map((v, i) => [fx(v), fy(ys[i])]), ink.blue, { width: 1.6 }); line(b, [[fx(0), fy(roof)]], ink.yellow, { dots: 3.6 }); line(b, [[fx(1), fy(0)]], ink.yellow, { dots: 3.6 }); axis(b, [[0, `s ${-REACH}`], [0.5, '0'], [1, `${REACH}`]], { wall: true }); const edge = tag(b, 'tau(s) = log_side sum w^s', ink.blue); tag(b, `tau(0) ${roof.toFixed(9)}, tau(1) 0`, ink.yellow, 'left', edge + 12); return; } const xs = [], ys = []; for (let i = 0; i < read.spectrum.length; i += 2) { xs.push(read.spectrum[i]); ys.push(read.spectrum[i + 1]); } const wide = high - low || 1; const fx = (v) => 0.04 + 0.92 * (v - low) / wide; const fy = (v) => 0.05 + 0.9 * v / (roof || 1); rules(b, [fx(one)], { dash: [2, 4] }); line(b, [[0, fy(0)], [1, fy(0)]], ink.line, { width: 1 }); line(b, xs.map((v, i) => [fx(v), fy(ys[i])]), ink.blue, { width: 1.6 }); line(b, [[fx(read.point[2]), fy(read.point[3])]], ink.yellow, { dots: 4 }); axis(b, [[0, `alpha ${low.toFixed(6)}`], [1, high.toFixed(6)]], { wall: true }); const edge = tag(b, 'f(alpha)', ink.blue); tag(b, `alpha(1) ${one.toFixed(9)}`, ink.yellow, 'left', edge + 12); }; const spread = (make) => set({ w: make().join(',') }); const flat = () => Array(k).fill(1); const rim = (v) => v === 0 || v === pick.side - 1; const heavy = () => Array.from({ length: k }, (_, i) => (rim(corners.list[2 * i]) && rim(corners.list[2 * i + 1]) ? 8 : 1)); const drawWeights = () => Array.from(m.random_between(s.next(), Array(k).fill(1), Array(k).fill(CAP))); const controls = ( <> [v, v])} onChange={(v) => set({ side: +v, level: Math.min(level, cap(+v)) })} /> set({ level: v })} /> {k <= SLIDERS && shares.map((share, i) => ( spread(() => shares.map((held, at) => (at === i ? v : held)))} /> ))} spread(flat)}>equal {k > SLIDERS && spread(heavy)}>corner-heavy} {k > SLIDERS && spread(drawWeights)}>seeded random} set({ gamma: v })} /> set({ curve: v })} /> ); return ( Give every filled corner of a design a weight and the cell at the level reached by the digit word f_1 ... f_level carries the mass w_(f_1) ... w_(f_level). The support never moves, the mass does: drag one weight and the picture darkens on one corner and lights on another while the design stays where it is. The curve beside it is the whole multifractal spectrum, and it is a closed form in the weights alone.} controls={controls} 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 corners the sliders weight, listed row by row, and the vector is normalised to sum to one inside the crate. The picture is the mass field at the level, one cell a pixel, read at (mass / peak)^gamma through the ground-blue-yellow ramp, so the gamma is display alone and never a printed number. The pressure is summed with the largest exponent factored out, which is why the curve stays exact out to s = 30 where the spectrum's two tails are sampled. Equal weights are the 0/1 design itself: the spectrum collapses to the single point (log_side fill, log_side fill), which is the box dimension the support has at every weighting. The geometry these weights leave alone is the same design fattened on the tube, cropped on the crop and laid over the torus on the modes. Every mass, every exponent and every point of the curve comes out of the crates through wasm; the page only draws.}>

Verified At contraction 1/side under the open set condition, which every design satisfies with the unit cell, the pressure equation closes in one line: tau(s) = log_side sum_f w_f^s, with f(alpha) = inf_s (alpha s + tau(s)) attained at alpha(s) = -tau'(s), so the spectrum is explicit in the weights and nothing is fitted. The proof it is read from and the exact tables are on the weights page.

The mass field {`level ${level}, ${span} by ${span}`}

{drawn && }

{pick.curve === 'pressure' ? 'The pressure' : 'The spectrum'} {pick.curve === 'pressure' ? `tau against s, ${MOMENTS} moments` : `f against alpha, ${SAMPLES} moments`}

{read.name ?? ''} {pick.base} {pick.side} {k} {`${shares.join(' ')} over ${whole}`} {drawn ? `${drawn.cells} of ${span * span}` : ''} {read.dims ? read.dims[0].toFixed(9) : ''} {read.dims ? read.dims[1].toFixed(9) : ''} {read.dims ? read.dims[2].toFixed(9) : ''} {read.dims ? read.dims[3].toFixed(9) : ''} {drawn ? drawn.peak.toExponential(6) : ''} {drawn ? drawn.least.toExponential(6) : ''}
); } mount();