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 />);