website/src/components/home/ProjectThumb.astro (6796B)
--- // A small drawing of a project's demo for its card on the projects wheel, in // the theme's colours: the computer's LEDs, the scheduler's cores, an Othello // board mid-search, a compiled expression, and packets crossing over UDP. // // Shapes that look alike are joined into one path, so a card is a handful of // elements rather than a hundred: the page's first style and layout pass takes // longer the more elements there are, and Lighthouse counts it. interface Props { demo: string; } const { demo } = Astro.props; const range = (n: number) => Array.from({ length: n }, (_, i) => i); const bits = (byte: number) => range(8).map((k) => (byte >> (7 - k)) & 1); const num = (v: number) => +v.toFixed(3); // path data for circles [cx, cy, r], rectangles [x, y, w, h] and lines [x1, y1, x2, y2] const dots = (list: number[][]) => list.map(([cx, cy, r]) => `M${num(cx - r)} ${num(cy)}a${r} ${r} 0 1 0 ${num(2 * r)} 0a${r} ${r} 0 1 0 ${num(-2 * r)} 0`).join(""); const boxes = (list: number[][]) => list.map(([x, y, w, h]) => `M${num(x)} ${num(y)}h${num(w)}v${num(h)}h${num(-w)}z`).join(""); const lines = (list: number[][]) => list.map(([x1, y1, x2, y2]) => `M${num(x1)} ${num(y1)}L${num(x2)} ${num(y2)}`).join(""); // Sort shapes into a list per class (or colour), keeping the order they're added in. function sorter() { const by = new Map<string, number[][]>(); const add = (key: string, shape: number[]) => { if (!by.has(key)) by.set(key, []); by.get(key)!.push(shape); }; return { by, add }; } // the first few bytes of the Fibonacci program the computer starts with const RAM = [0x51, 0x4f, 0x50, 0xe0, 0x2f, 0x70, 0x4e]; // cores in the scheduler: [start, length, colour] blocks const CORES = [ [[0, 5, "yellow"], [5, 4, "red"], [9, 3, "yellow"]], [[0, 3, "orange"], [3, 6, "cyan"], [9, 3, "orange"]], [[0, 7, "magenta"], [7, 3, "green"], [10, 2, "magenta"]], [[0, 2, "green"], [2, 5, "red"], [7, 5, "blue"]], ]; const QUEUE = ["orange", "red", "cyan", "green", "yellow"]; // an Othello position: 1 is the player's disc, 2 the opponent's; shade is how // often the search tried each legal move const BOARD = [ "........", "...2....", "..1222..", "..2112..", "..1221..", "...112..", "....2...", "........", ]; const SHADE: Record<string, number> = { "1,2": 0.25, "2,6": 0.7, "6,3": 0.4, "5,6": 0.15 }; const CODE = ["iload_0", "iconst_2", "imul", "iconst_1", "iadd", "ireturn"]; // packets: 1 arrived, 0 lost and waiting to be resent const GOT = [1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1]; // the computer: its modules' outlines, the bus, and LEDs by colour const leds = sorter(); const led = (on: number, lit: string, cx: number, cy: number, r = 2.2) => leds.add(on ? lit : "th-off", [cx, cy, r]); range(4).forEach((k) => led(1, "th-on", 12 + k * 7, 11.5)); RAM.forEach((b, r) => bits(b).forEach((on, k) => led(on, "th-red", 12 + k * 7, 29.5 + r * 7))); [0, 1, 0, 1, 1, 1, 1, 0].forEach((on, k) => led(on, "th-on", 12 + k * 7, 86, 2)); [0, 1, 1, 0].forEach((on, k) => led(on, "th-on", 99 + k * 7, 11.5)); bits(0x2a).forEach((on, k) => led(on, "th-red", 99 + k * 7, 29.5)); bits(0x15).forEach((on, k) => led(on, "th-red", 99 + k * 7, 47.5)); const MODULES = [[4.5, 4.5, 64, 14], [4.5, 22.5, 64, 54], [4.5, 80.5, 64, 11], [74.5, 4.5, 11, 87], [91.5, 4.5, 64, 14], [91.5, 22.5, 64, 14], [91.5, 40.5, 64, 14], [91.5, 58.5, 64, 33]]; // the scheduler: each core's lane, then its blocks by colour const blocks = sorter(); CORES.forEach((row, r) => row.forEach(([s, len, c]) => blocks.add(c as string, [64 + (s as number) * 7.67, 6 + r * 18, (len as number) * 7.67 - 1, 13]))); // Othello: the grid, then the discs by side const discs = sorter(); BOARD.forEach((row, r) => [...row].forEach((ch, c) => { if (ch !== ".") discs.add(ch === "1" ? "th-disc" : "th-disc-o", [42.4 + c * 10.875, 10.4 + r * 10.875, 3.6]); })); const GRID = range(7).flatMap((k) => [[47.375 + k * 10.875, 5, 47.375 + k * 10.875, 91], [37, 15.375 + k * 10.875, 123, 15.375 + k * 10.875]]); // the packets: sent, then received or lost const cell = (x0: number, k: number) => [x0 + (k % 5) * 8, 14 + Math.floor(k / 5) * 8, 6, 6]; const got = sorter(); range(25).forEach((k) => got.add(GOT[k] ? "th-on" : "th-off", cell(114, k))); --- <svg class="th" viewBox="0 0 160 96" aria-hidden="true"> {demo === "computer" && ( <g> <path class="th-box" d={boxes(MODULES)} /> <path class="th-line" d={lines(range(4).map((k) => [77 + k * 2, 8, 77 + k * 2, 88]))} /> {[...leds.by].map(([cls, list]) => <path class={cls} d={dots(list)} />)} {/* the output, a seven-segment 42 */} <path class="th-seg-off" d="M112 64h8M112 75h8M112 86h8M111 65v9M121 65v9M111 76v9M121 76v9M126 64h8M126 75h8M126 86h8M125 65v9M135 65v9M125 76v9M135 76v9" /> <path class="th-seg" d="M111 65v9M121 65v9M112 75h8M121 76v9M126 64h8M135 65v9M126 75h8M125 76v9M126 86h8" /> </g> )} {demo === "scheduler" && ( <g> <path class="th-off" d={boxes(range(4).map((r) => [4, 6 + r * 18, 152, 13]))} /> {[...blocks.by].map(([c, list]) => <path d={boxes(list)} style={`fill: var(--${c})`} />)} {QUEUE.map((c, k) => <rect class="th-q" x={4.5 + k * 22} y="80.5" width="18" height="10" style={`stroke: var(--${c})`} />)} </g> )} {demo === "othello" && ( <g> <rect class="th-box" x="36.5" y="4.5" width="87" height="87" /> <path class="th-line" d={lines(GRID)} /> {Object.entries(SHADE).map(([at, a]) => { const [r, c] = at.split(",").map(Number); return <rect class="th-on" x={37 + c * 10.875} y={5 + r * 10.875} width="10.4" height="10.4" style={`opacity: ${a}`} />; })} {[...discs.by].map(([cls, list]) => <path class={cls} d={dots(list)} />)} </g> )} {demo === "bytecode" && ( <g> <text class="th-src" x="6" y="15">x * 2 + 1</text> {CODE.map((op, k) => <text class:list={["th-op", { "is-at": k === 2 }]} x="6" y={32 + k * 11}>{op}</text>)} {[["2", 70.5], ["x", 54.5]].map(([v, y]) => ( <g> <rect class="th-box" x="112.5" y={y} width="40" height="13" /> <text class="th-op" x="128" y={(y as number) + 9.5}>{v}</text> </g> ))} <line class="th-line" x1="108" y1="87.5" x2="156" y2="87.5" /> </g> )} {demo === "rbudp" && ( <g> <path class="th-dim" d={boxes(range(25).map((k) => cell(6, k)))} /> {[...got.by].map(([cls, list]) => <path class={cls} d={boxes(list)} />)} <path class="th-on" d={boxes([[58, 30], [72, 22], [86, 42], [97, 34]].map(([x, y]) => [x, y, 6, 6]))} style="opacity: 0.6" /> <line class="th-line th-dash" x1="6" y1="70.5" x2="154" y2="70.5" /> <path class="th-line" d="M100 77l-6 3.5 6 3.5" /> </g> )} </svg>