index.jsx
4.4 kB · jsx · 108 lines
1import { useEffect, useRef, useState } from 'react';2import { ready, paint } from '../../lib/mrly.js';3import { mount, Page, Row, Slider, Check, Stats, Stat } from '../../lib/app.jsx';45const m = await ready();6const FPS = 25;7const SECTIONS = [['Uppers', 26], ['Lowers', 26], ['Digits', 10], ['Extras', 42], ['Specials', 4]];89function study(char) {10 const { rows, cols, frames } = JSON.parse(m.font_animate(char, 1));11 const cells = [];12 let prev = new Set();13 for (const frame of frames.slice(1)) {14 cells.push(frame.find((i) => !prev.has(i)));15 prev = new Set(frame);16 }17 const at = (i) => [Math.floor(i / cols), i % cols];18 let lifts = 0;19 for (let i = 1; i < cells.length; i++) {20 const [ar, ac] = at(cells[i - 1]);21 const [br, bc] = at(cells[i]);22 if (Math.abs(ar - br) + Math.abs(ac - bc) !== 1) lifts++;23 }24 const strokes = cells.length ? lifts + 1 : 0;25 const floor = m.font_floor(char);26 const chip = strokes > floor ? 'over floor' : lifts >= 5 ? `${lifts} lifts` : null;27 return { char, rows, cols, frames, strokes, floor, lifts, chip };28}2930const GLYPHS = [...m.font_chars()].map(study);31const GROUPS = SECTIONS.reduce((out, [name, count]) => {32 const from = out.reduce((n, g) => n + g.glyphs.length, 0);33 out.push({ name, glyphs: GLYPHS.slice(from, from + count) });34 return out;35}, []);36const CONTENTS = GROUPS.map((g) => ({ id: g.name.toLowerCase(), text: g.name, level: 2 }));3738function frameOf(glyph, tick, hold) {39 const n = glyph.frames.length;40 const i = tick % (n + hold);41 return glyph.frames[Math.min(i, n - 1)];42}4344function draw(canvas, glyph, frame) {45 const types = new Uint8Array(glyph.rows * glyph.cols);46 for (const i of frame) types[i] = 1;47 paint(canvas, { width: glyph.cols, height: glyph.rows, types });48}4950function Glyph({ glyph, canvases }) {51 const label = glyph.char === ' ' ? 'space' : glyph.char;52 return (53 <div className={glyph.chip ? 'card on' : 'card'}>54 <canvas ref={(node) => { canvases.current.set(glyph.char, node); }} role="img" aria-label={`${label} written stroke by stroke`} />55 <p><b>{label}</b> <span>{glyph.strokes} of {glyph.floor}</span>{glyph.chip && <> <span className="chip refuted">{glyph.chip}</span></>}</p>56 </div>57 );58}5960function App() {61 const [hold, setHold] = useState(FPS);62 const [slow, setSlow] = useState(false);63 const canvases = useRef(new Map());64 const tick = useRef(0);65 useEffect(() => {66 const step = () => {67 for (const glyph of GLYPHS) {68 const canvas = canvases.current.get(glyph.char);69 if (canvas) draw(canvas, glyph, frameOf(glyph, tick.current, hold));70 }71 tick.current++;72 };73 step();74 const timer = setInterval(step, slow ? 2000 / FPS : 1000 / FPS);75 return () => clearInterval(timer);76 }, [hold, slow]);77 const flagged = GLYPHS.filter((g) => g.chip);7879 const controls = (80 <Row>81 <Slider label="hold" value={hold} min={0} max={2 * FPS} onChange={setHold} />82 <Check label="half speed" checked={slow} onChange={setSlow} />83 </Row>84 );8586 return (87 <Page crumb="font" title="The 108 pens"88 sub="Every glyph of MrlyFont writes itself in the order its pen table gives, one cell a frame at 25 a second, then holds. Under each one: its stroke count against its floor, the least strokes that can write it. A chip marks a glyph penned over its floor or lifting the pen five times or more."89 controls={controls} contents={CONTENTS}90 foot={<>The frames come from the wasm bridge, so this page shows the crate's <code>pens.rs</code> as it is now. A stroke walks 4-adjacent cells; a lift is any step that is not. The floor is the crate's exact minimum cover of the glyph's cells by 4-adjacent paths, so a glyph over its floor is penned that way on purpose, like the wordmark letters, and a glyph on its floor with many lifts can only be helped by a different shape.</>}>91 <Stats>92 <Stat label="glyphs">{GLYPHS.length}</Stat>93 <Stat label="strokes">{GLYPHS.reduce((n, g) => n + g.strokes, 0)}</Stat>94 <Stat label="flagged">{flagged.map((g) => g.char).join(' ') || 'none'}</Stat>95 </Stats>96 {GROUPS.map((group) => (97 <section key={group.name} id={group.name.toLowerCase()} aria-label={group.name}>98 <h2 className="group">{group.name}</h2>99 <div className="cards pens">100 {group.glyphs.map((glyph) => <Glyph key={glyph.char} glyph={glyph} canvases={canvases} />)}101 </div>102 </section>103 ))}104 </Page>105 );106}107108mount(<App />);