clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Fit long proof titles within mobile HUD and cap authored length

Dami committed 27d2bd7
commit
27d2bd72c10cb88d9f5d90d57ee973cd4344d945
parent
2c984a7b38b09c761d743aa4b7470ae36049681d
author
Dami <dami.etoile@gmail.com>
8 files changed, +104 -15
Msrc/mods.js4+++
Msrc/render/index.js4++-
Msrc/render/stage-hud.js5++-
Msrc/render/stage-proof.js42++++++++++++++++++++++--------
Msrc/render/stage-win.js4++-
Mtests/browser/long-title.mjs37++++++++++++++++++++++++++
Mtests/mods.test.js12+++++++++
Mtests/title-fitting.test.js11++++++++
M src/mods.js +4 -0
M src/render/index.js +3 -1
M src/render/stage-hud.js +3 -2
M src/render/stage-proof.js +31 -11
@@ -77,7 +77,16 @@ export function renderProofScene(ctx, state, view = {}) {
7777 );7878 const counterWidth = counterDigits * Math.max(20, ctx.measureText('0').width);7979 const titleWidth = Math.max(1, counterRight - counterWidth - titleToCounterGap - titleX);80- const titleFit = fitSceneTitle(ctx, fullProofTitle, { maxWidth: titleWidth, maxSize: 22, minSize: 16, maxLines: 2 });80+ let titleFit = fitSceneTitle(ctx, fullProofTitle, { maxWidth: titleWidth, maxSize: 22, minSize: 16, maxLines: 2 });81+ let inlineCounter = false;82+ // At 390px a 51–64 character title can require three 16px lines. Give it83+ // the full title band and move the compact odometer into the subline row.84+ if (width <= 760 && titleFit.overflow) {85+ titleFit = fitSceneTitle(ctx, fullProofTitle, {86+ maxWidth: Math.max(1, width - titleX - 16), maxSize: 16, minSize: 16, maxLines: 3,87+ });88+ inlineCounter = true;89+ }8190 const titleTop = 30;8291 const sublineTop = titleTop + titleFit.lines.length * titleFit.lineHeight + 3;8392 const hudBottom = sublineTop + 12;
@@ -93,15 +102,26 @@ export function renderProofScene(ctx, state, view = {}) {
93102 sublineTop,94103 );95104 ctx.fillStyle = "#F2EEE6";96- drawStepCounter(ctx, state.moves || 0, counterRight, 39, {97- now,98- reducedMotion: view.reducedMotion,99- roll: view.stepRoll,100- });101- ctx.textAlign = "right";102- ctx.font = "600 10px Recursive, monospace";103- ctx.fillStyle = "#8E877C";104- ctx.fillText("STEPS", counterRight, 70);105+ if (inlineCounter) {106+ ctx.textAlign = "right";107+ ctx.font = "800 22px Recursive, monospace";108+ const compactDigitWidth = Math.max(20, ctx.measureText("0").width);109+ const compactCounterWidth = counterDigits * compactDigitWidth;110+ ctx.font = "600 10px Recursive, monospace";111+ ctx.fillStyle = "#8E877C";112+ ctx.fillText("STEPS", counterRight - compactCounterWidth - 8, sublineTop + 8);113+ drawStepCounter(ctx, state.moves || 0, counterRight, sublineTop + 8, {114+ now, size: 22, reducedMotion: view.reducedMotion, roll: view.stepRoll,115+ });116+ } else {117+ drawStepCounter(ctx, state.moves || 0, counterRight, 39, {118+ now, reducedMotion: view.reducedMotion, roll: view.stepRoll,119+ });120+ ctx.textAlign = "right";121+ ctx.font = "600 10px Recursive, monospace";122+ ctx.fillStyle = "#8E877C";123+ ctx.fillText("STEPS", counterRight, 70);124+ }105125 ctx.textAlign = "left";106126 const geometry = drawBoard(ctx, state, {107127 x,
@@ -199,7 +219,7 @@ export function renderProofScene(ctx, state, view = {}) {
199219 );200220 return {201221 geometry,202- titleLayout: { ...titleFit, x: titleX, y: titleTop, width: titleWidth, sublineTop, hudBottom, boardTop: geometry.y },222+ titleLayout: { ...titleFit, x: titleX, y: titleTop, width: inlineCounter ? Math.max(1, width - titleX - 16) : titleWidth, sublineTop, hudBottom, boardTop: geometry.y, inlineCounter },203223 controlledCenter,204224 ariaLabel: `Miau Is True proof: ${fullProofTitle}`,205225 };
M src/render/stage-win.js +3 -1
M tests/browser/long-title.mjs +37 -0
@@ -90,6 +90,14 @@ try {
9090 ]) {9191 const context = await browser.newContext({ viewport, isMobile: name === 'mobile', hasTouch: name === 'mobile' });9292 const page = await context.newPage();93+ await page.addInitScript(() => {94+ window.__miauTextCalls = [];95+ const original = CanvasRenderingContext2D.prototype.fillText;96+ CanvasRenderingContext2D.prototype.fillText = function(value, x, y, ...rest) {97+ if (typeof value === 'string') window.__miauTextCalls.push({ value, x, y, font: this.font, align: this.textAlign });98+ return original.call(this, value, x, y, ...rest);99+ };100+ });93101 page.on('console', message => { if (message.type() === 'error') errors.push(`${name}: console: ${message.text()}`); });94102 page.on('pageerror', error => errors.push(`${name}: page: ${error.message}`));95103 await page.goto(shareUrl, { waitUntil: 'networkidle' });
@@ -97,6 +105,9 @@ try {
97105 await page.keyboard.press('Enter');98106 await waitScene(page, 'index');99107 await page.waitForTimeout(700);108+ assert.match(await page.locator('.stage-canvas').getAttribute('aria-label') || '', /Press Enter to enter/, `${name}: first entry shows a world card`);109+ await page.keyboard.press('Enter');110+ await page.waitForTimeout(100);100111 let metrics = await assertViewport(page, `${name} INDEX`);101112 assert.equal(metrics.state.level, authoredTitle, `${name}: INDEX must retain authored title`);102113 await page.screenshot({ path: `${screenshotDir}/long-title-${name}-index.png` });
@@ -104,9 +115,35 @@ try {
104115 await page.keyboard.press('Enter');105116 await waitScene(page, 'proof');106117 await page.waitForTimeout(700);118+ await page.evaluate(() => {119+ window.__miauTextCalls = [];120+ window.dispatchEvent(new Event('resize'));121+ });122+ await page.waitForTimeout(100);107123 metrics = await assertViewport(page, `${name} proof`);108124 assert.equal(metrics.state.level, authoredTitle, `${name}: proof must retain authored title`);109125 assert.equal(metrics.state.moves, 0, `${name}: proof starts at zero moves`);126+ const proofText = await page.evaluate(() => window.__miauTextCalls);127+ const titleWords = authoredTitle.split(/\s+/);128+ assert.ok(titleWords.every(word => proofText.some(call => call.value === word || call.value.includes(word))), `${name}: every title word remains visible`);129+ assert.equal(proofText.some(call => call.value.includes('…')), false, `${name}: authored title has no ellipsis`);130+ if (name === 'mobile') {131+ const steps = proofText.find(call => call.value === 'STEPS');132+ const proofLine = proofText.find(call => call.value.startsWith('PROOF '));133+ assert.ok(steps && proofLine, 'mobile: compact counter and proof subline are both drawn');134+ assert.equal(Math.round(steps.y), Math.round(proofLine.y + 8), 'mobile: STEPS shares the proof subline row');135+ const titleCalls = proofText.filter(call => titleWords.some(word => call.value.includes(word)) && call.font.includes('16px') && call.align === 'left');136+ assert.ok(titleCalls.length >= 2 && titleCalls.length <= 3, 'mobile: full-width fallback uses at most three 16px lines');137+ const measured = await page.evaluate(() => {138+ const canvas = document.querySelector('.stage-canvas');139+ const ctx = canvas.getContext('2d');140+ return window.__miauTextCalls.filter(call => call.font.includes('16px') && call.value.length > 0).map(call => {141+ ctx.font = call.font;142+ return { value: call.value, right: call.x + ctx.measureText(call.value).width };143+ });144+ });145+ assert.ok(measured.filter(call => titleWords.some(word => call.value.includes(word))).every(call => call.right <= 374), 'mobile: title lines clear the viewport inset and counter row');146+ }110147 await page.screenshot({ path: `${screenshotDir}/long-title-${name}-proof.png` });111148 112149 await page.keyboard.press('ArrowRight');
M tests/mods.test.js +12 -0
M tests/title-fitting.test.js +11 -0