- commit
- 97f08a513998d69b99f8125a5fdd27c7be8cd81d
- parent
- 206931d471553259baef4b4c6f619b1321fa09e1
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 78 | ++++++++++-------------------- |
| M | src/ | 29 | ++++++++++-- |
| M | tests/ | 23 | ++++++++- |
@@ -195,21 +195,14 @@ function automataGlyph(subject, palette = {}, glyphs = {}) {
195195 }196196 197197 function automataCue(action) {198- if (typeof action?.cue === "string" && action.cue.trim())199- return action.cue.trim().slice(0, 16);200- if (action?.action === "wait") return "WAIT";198+ // Beat cells stay a compact visual vocabulary. A blocked move keeps its199+ // intended direction; the board nudge carries the blocked feedback.200+ if (action?.action === "wait") return "‖";201201 if (action?.dx === 1) return "→";202202 if (action?.dx === -1) return "←";203203 if (action?.dy === 1) return "↓";204204 if (action?.dy === -1) return "↑";205- return "?";206-}207-208-function isEmojiCue(value) {209- return Array.from(String(value || "")).some((character) => {210- const code = character.codePointAt(0);211- return code > 0x1f000 || character === "\uFE0F";212- });205+ return "‖";213206 }214207 215208 export function drawAutomataStrip(@@ -230,9 +223,11 @@ export function drawAutomataStrip(
230223 if (!rows.length) return { bottom: y, height: 0 };231224 const visibleRows = rows.slice(0, viewportWidth <= 760 ? 2 : 3);232225 const rowHeight = viewportWidth <= 760 ? 24 : 30;233- const labelWidth = 76;226+ const actorWidth = 28;227+ const actorGap = 6;228+ const labelGap = 10;234229 const beatGap = 5;235- const maxBeats = Math.max(4, Math.floor((maxWidth - labelWidth) / 22));230+ const maxBeats = Math.max(4, Math.floor((maxWidth - actorWidth - actorGap - 76 - labelGap) / (22 + beatGap)));236231 ctx.save();237232 ctx.textBaseline = "middle";238233 visibleRows.forEach((system, rowIndex) => {@@ -259,67 +254,48 @@ export function drawAutomataStrip(
259254 ]),260255 ];261256 const omitted = system.pattern.length - beatIndexes.length;262- const beatWidth = Math.max(263- 14,264- Math.min(265- 34,266- (maxWidth - labelWidth - beatGap * (beatIndexes.length - 1)) /267- beatIndexes.length,268- ),269- );270257 const glyph = automataGlyph(derivedSubject, palette, glyphs);271258 ctx.fillStyle = "#F2EEE6";272- ctx.font = `${Math.min(22, beatWidth)}px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`;259+ ctx.font = `28px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`;273260 ctx.textAlign = "left";274261 ctx.fillText(glyph, x, rowY + rowHeight / 2);275- ctx.fillStyle = "#8E877C";276- ctx.font = "700 9px Recursive, monospace";277- ctx.fillText(278- String(system.property || "").toUpperCase() || "AUTOMATA",279- x + 28,280- rowY + 10,281- );282- ctx.font = "600 9px Recursive, monospace";283- ctx.fillText(284- String(system.active ? "ACTIVE" : "DORMANT"),285- x + 28,286- rowY + 21,287- );262+ const beatStart = x + actorWidth + actorGap;288263 beatIndexes.forEach((patternIndex, beatIndex) => {289264 const action = system.pattern[patternIndex];290- const beatX = x + labelWidth + beatIndex * (beatWidth + beatGap);265+ const beatX = beatStart + beatIndex * (22 + beatGap);291266 const current = patternIndex === phase;292267 const next = patternIndex === (phase + 1) % system.pattern.length;293268 ctx.globalAlpha = current || next ? 1 : 0.4;294269 ctx.fillStyle = current ? "#C6F432" : "#191816";295270 ctx.strokeStyle = current || next ? "#C6F432" : "#4A4640";296271 ctx.lineWidth = current ? 2 : 1;297- ctx.fillRect(beatX, rowY + 4, beatWidth, 22);298- if (!current) ctx.strokeRect(beatX + 0.5, rowY + 4.5, beatWidth - 1, 21);299- const cue =300- action.action === "wait" && beatWidth < 24 ? "○" : automataCue(action);301- const emojiCue = isEmojiCue(cue);302- const cueSize = emojiCue303- ? Math.min(16, beatWidth * 0.58)304- : Math.min(305- cue.length > 4 || action.action === "wait" ? 7 : 13,306- beatWidth * 0.8,307- );272+ ctx.fillRect(beatX, rowY + 1, 22, 22);273+ if (!current) ctx.strokeRect(beatX + 0.5, rowY + 1.5, 21, 21);274+ const cue = automataCue(action);308275 ctx.fillStyle = current ? "#0F0E0D" : next ? "#0F0E0D" : "#8E877C";309- ctx.font = `${current ? "800" : "600"} ${cueSize}px ${emojiCue ? '"Apple Color Emoji", "Segoe UI Emoji", sans-serif' : "Recursive, monospace"}`;276+ ctx.font = `${current ? "800" : "600"} 14px Recursive, monospace`;310277 ctx.textAlign = "center";311- ctx.fillText(cue, beatX + beatWidth / 2, rowY + 16);278+ ctx.fillText(cue, beatX + 11, rowY + 13);312279 ctx.globalAlpha = 1;313280 if (omitted && beatIndex === beatIndexes.length - 1) {314281 ctx.fillStyle = "#8E877C";315282 ctx.font = "600 8px Recursive, monospace";316283 ctx.fillText(317284 `+${omitted}`,318- beatX + beatWidth / 2,319- rowY + rowHeight - 4,285+ beatX + 11,286+ rowY + rowHeight - 2,320287 );321288 }322289 });290+ const labelX = beatStart + beatIndexes.length * (22 + beatGap) - beatGap + labelGap;291+ ctx.fillStyle = "#8E877C";292+ ctx.font = "600 9px Recursive, monospace";293+ ctx.textAlign = "left";294+ ctx.fillText(295+ `${String(derivedSubject || "AUTOMATA").toUpperCase()} IS ${String(system.property || "TICK").toUpperCase()}`,296+ labelX,297+ rowY + rowHeight / 2,298+ );323299 });324300 if (rows.length > visibleRows.length) {325301 ctx.fillStyle = "#8E877C";@@ -118,19 +118,40 @@ export function renderProofScene(ctx, state, view = {}) {
118118 { ledger: view.frame?.ledger },119119 );120120 const hint = String(state.hint || "").trim();121+ const mobileAutomata = width <= 760 && automata.length > 0;122+ const automataHeight = automataRows * automataRowHeight;121123 const automataY = automata.length122- ? height - (automataRows * automataRowHeight + 90)124+ ? mobileAutomata125+ ? Math.min(ledger.bottom + 14, height - automataHeight - 98)126+ : height - (automataHeight + 90)123127 : height - 80;128+ const hintY = mobileAutomata129+ ? Math.min(automataY + automataHeight + 12, height - 100)130+ : Math.min(ledger.bottom + 12, height - 78);131+ if (mobileAutomata && automata.length)132+ drawAutomataStrip(133+ ctx,134+ automata,135+ x,136+ automataY,137+ width - x - 32,138+ state.rules,139+ {140+ viewportWidth: width,141+ palette,142+ glyphs,143+ },144+ );124145 if (hint)125146 drawHint(126147 ctx,127148 hint,128149 x,129- Math.min(ledger.bottom + 12, height - 78),150+ hintY,130151 width - x - 32,131- automata.length ? automataY - 8 : height - 60,152+ mobileAutomata ? height - 100 : automata.length ? automataY - 8 : height - 60,132153 );133- if (automata.length)154+ if (!mobileAutomata && automata.length)134155 drawAutomataStrip(135156 ctx,136157 automata,@@ -1,6 +1,6 @@
11 import test from 'node:test';22 import assert from 'node:assert/strict';3-import { drawInlineActions, drawLedger, drawStepCounter } from '../src/render/stage-hud.js';3+import { drawAutomataStrip, drawInlineActions, drawLedger, drawStepCounter } from '../src/render/stage-hud.js';44 import { BOARD_COLORS } from '../src/render/board/model.js';55 66 function context() {@@ -19,6 +19,7 @@ function context() {
1919 rect(...args) { calls.push(['rect', ...args]); },2020 clip() { calls.push(['clip']); },2121 stroke() { calls.push(['stroke']); },22+ strokeRect(...args) { calls.push(['strokeRect', ...args]); },2223 fillRect(...args) { calls.push(['fillRect', ...args, this.fillStyle]); },2324 fillText(...args) { calls.push(['fillText', ...args]); },2425 measureText(text) { return { width: String(text).length * 6 }; },@@ -74,6 +75,26 @@ test('drawLedger falls back to settled rules when no frame ledger exists', () =>
7475 assert.equal(ctx.calls.filter(call => call[0] === 'fillText').length, 6);7576 });7677 78+test('drawAutomataStrip uses one actor tile, directional beat cells, and a sentence label', () => {79+ const ctx = context();80+ const result = drawAutomataStrip(ctx, [{81+ subject: 'BELL',82+ property: 'TICK',83+ phase: 0,84+ active: true,85+ pattern: [{ dx: -1, action: 'move' }, { action: 'wait' }, { dx: 1, action: 'move' }],86+ }], 10, 20, 300, [], { viewportWidth: 390, palette: { BELL: '🔔' } });87+ const text = ctx.calls.filter((call) => call[0] === 'fillText').map((call) => call[1]);88+ assert.ok(text.includes('🔔'));89+ assert.ok(text.includes('←'));90+ assert.ok(text.includes('‖'));91+ assert.ok(text.includes('BELL IS TICK'));92+ assert.equal(text.includes('ACTIVE'), false);93+ const beatRects = ctx.calls.filter((call) => call[0] === 'fillRect' && call[3] === 22);94+ assert.equal(beatRects.length, 3);95+ assert.equal(result.height, 24);96+});97+7798 test('drawInlineActions presses only the matching Win action keycap', () => {7899 const ctx = context();79100 drawInlineActions(ctx, [