select.jsx
7.2 kB · jsx · 156 lines
1import { useMemo, useRef } from 'react';2import { mrly } from './mrly.js';3import { stamp } from './query.js';4import { Pick, Text, Btn, Slider, Check } from './app.jsx';56const FLAT3 = [['carpet', '495'], ['runner', '127']];78function designs(dimension, bases) {9 const list = [];10 if (bases.includes(2)) {11 for (const design of JSON.parse(mrly.universe(dimension)).designs) list.push([`${design.code} · ${design.anf}`, design.code, 2]);12 }13 if (dimension === 2 && bases.includes(3)) {14 for (const [word, code] of FLAT3) list.push([`${word} · ${code} · base 3`, code, 3]);15 }16 return list;17}1819export function useSeeds() {20 const seed = useRef(+(new URLSearchParams(location.search).get('seed') ?? 0));21 return useMemo(() => ({22 get: () => seed.current,23 next: () => {24 seed.current += 1;25 stamp({ seed: seed.current });26 return seed.current;27 },28 drop: () => {29 seed.current = 0;30 stamp({ seed: null });31 },32 }), []);33}3435export function roll(seed, spans) {36 return mrly.random_between(seed, spans.map(([low]) => low), spans.map(([, high]) => high));37}3839export function seeded(seeds, dimension, base, code) {40 return seeds.get() ? mrly.random_code(dimension, base, seeds.get()) : code;41}4243export function Picker({ dimension, bases = [2], code, base = bases[0], onChange, seeds, button = true }) {44 const list = useMemo(() => designs(dimension, bases), [dimension, String(bases)]);45 const picked = list.some(([, value, b]) => value === code.trim() && b === base) ? `${code.trim()}:${base}` : '';46 return (47 <>48 <Pick label="design" value={picked} options={[['', 'type a code'], ...list.map(([text, value, b]) => [`${value}:${b}`, text])]}49 onChange={(v) => {50 if (!v) return;51 const [value, b] = v.split(':');52 seeds.drop();53 onChange(bases.length > 1 ? { code: value, base: +b } : { code: value });54 }} />55 <Text label="code" value={code} onChange={(v) => { seeds.drop(); onChange({ code: v }); }} />56 {bases.length > 1 && <Pick label="base" value={base} options={bases.map((b) => [b, b])} onChange={(v) => onChange({ base: +v })} />}57 {button && <Btn onClick={() => onChange({ code: mrly.random_code(dimension, base, seeds.next()) })}>Randomize</Btn>}58 </>59 );60}6162export function Ramp({ value, onChange }) {63 return (64 <>65 <Pick label="ramp" value={value.ramp} options={['fire', 'heat', 'diverge']} onChange={(v) => onChange({ ramp: v })} />66 <Slider label="levels" value={value.levels} min={2} max={64} onChange={(v) => onChange({ levels: v })} />67 <Check label="invert" checked={value.invert} onChange={(v) => onChange({ invert: v })} />68 </>69 );70}7172export function Cropper({ dimension = 2, value, onChange }) {73 const shapes = useMemo(() => JSON.parse(mrly.crop_shapes(dimension)), [dimension]);74 return (75 <>76 <Pick label="crop" value={value.crop} options={[['', 'off'], ...shapes]} onChange={(v) => onChange({ crop: v })} />77 <Slider label="radius" value={value['crop-r']} min={1} max={32} show={`${value['crop-r']}/32`} onChange={(v) => onChange({ 'crop-r': v })} />78 <Check label="anti" checked={value['crop-anti']} onChange={(v) => onChange({ 'crop-anti': v })} />79 </>80 );81}8283export function cropOf(value) {84 return { shape: value.crop, rnum: value['crop-r'], rden: 32, anti: value['crop-anti'], active: value.crop !== '' };85}8687export const SOURCE_FIRST = { source: 'flat', code: '495', base: 3, number: 3, level: 4, preset: 'weave', limit: 9, scode: '23', tile: 3, slevel: 2 };8889function drawMoire(seed) {90 const names = mrly.moire_names();91 const [p, l] = roll(seed, [[0, names.length - 1], [1, 41]]);92 return { preset: names[p], limit: l | 1 };93}9495export function seedSource(seeds, first) {96 const s = seeds.get();97 if (!s) return first;98 const params = new URLSearchParams(location.search);99 const source = params.get('source') ?? first.source;100 if (source === 'flat') return { ...first, code: mrly.random_code(2, +params.get('base') || first.base, s) };101 if (source === 'slice') return { ...first, scode: mrly.random_code(3, 2, s) };102 return { ...first, ...drawMoire(s) };103}104105export function Sources({ value, onChange, seeds, onSeed }) {106 const presets = useMemo(() => [...mrly.moire_names()], []);107 const randomize = () => {108 const s = seeds.next();109 if (value.source === 'flat') onChange({ code: mrly.random_code(2, value.base, s) });110 else if (value.source === 'slice') onChange({ scode: mrly.random_code(3, 2, s) });111 else onChange(drawMoire(s));112 if (onSeed) onSeed(s);113 };114 return (115 <>116 <Pick label="source" value={value.source} options={[['flat', 'flat design'], ['moire', 'moire stack'], ['slice', 'hex slice']]} onChange={(v) => onChange({ source: v })} />117 {value.source === 'flat' && (118 <span className="set">119 <Picker dimension={2} bases={[3, 2]} code={value.code} base={value.base} seeds={seeds} button={false} onChange={onChange} />120 <Pick label="side" value={value.number} options={[[3, 3], [5, 5], [7, 7]]} onChange={(v) => onChange({ number: +v })} />121 <Slider label="level" value={Math.min(value.level, mrly.level_cap(value.number, 1, 243))} min={1} max={mrly.level_cap(value.number, 1, 243)} onChange={(v) => onChange({ level: v })} />122 </span>123 )}124 {value.source === 'moire' && (125 <span className="set">126 <Pick label="preset" value={value.preset} options={presets} onChange={(v) => onChange({ preset: v })} />127 <Slider label="scales up to" value={value.limit} min={1} max={41} step={2} onChange={(v) => onChange({ limit: v })} />128 </span>129 )}130 {value.source === 'slice' && (131 <span className="set">132 <Picker dimension={3} code={value.scode} seeds={seeds} button={false} onChange={(patch) => onChange(patch.code === undefined ? patch : { scode: patch.code })} />133 <Pick label="tile" value={value.tile} options={[[3, 3], [5, 5]]} onChange={(v) => onChange({ tile: +v })} />134 <Slider label="level" value={Math.min(value.slevel, mrly.level_cap(value.tile, 1, 27))} min={1} max={mrly.level_cap(value.tile, 1, 27)} onChange={(v) => onChange({ slevel: v })} />135 </span>136 )}137 <Btn onClick={randomize}>Randomize</Btn>138 </>139 );140}141142export function readSource(value) {143 if (value.source === 'flat') {144 const level = Math.min(value.level, mrly.level_cap(value.number, 1, 243));145 const grid = mrly.two_grid(value.code, value.number, level, 0, value.base);146 const fills = JSON.parse(mrly.two_census(value.code, value.number, level, 0, value.base)).fills;147 return { kind: 'flat', grid, field: Float32Array.from(grid.types), size: grid.width, name: mrly.name_of(value.code, 2, value.base), fills };148 }149 if (value.source === 'moire') {150 return { kind: 'moire', grid: null, field: mrly.moire_field(value.preset, value.limit, 256), size: 256, name: value.preset, fills: '' };151 }152 const level = Math.min(value.slevel, mrly.level_cap(value.tile, 1, 27));153 const grid = mrly.slice_grid(value.scode, value.tile, level, 2, 384);154 const fills = JSON.parse(mrly.slice_census(value.scode, value.tile, level, 2)).fills;155 return { kind: 'slice', grid, field: Float32Array.from(grid.types), size: 384, name: mrly.name_of(value.scode, 3, 2), fills };156}