- commit
- 72d29c603dcfa5f4fd452f8ba03e5269b1bb11d1
- parent
- 4636e19b640ac577f813f69a7ae959f978bbb50a
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 13 | ++++++++++++- |
| M | src/ | 13 | ++++++++++--- |
| M | tests/ | 18 | +++++++++++++++++- |
@@ -7,6 +7,8 @@ const EXIT_ITEM_DURATION = 170;
77 const EXIT_STAGGER = 25;88 const SLIDE_DURATION = 110;99 const PRESS_DURATION = 126;10+const HINT_DELAY = 260;11+const HINT_DURATION = 120;1012 const ITEM_PITCH = 68;1113 const ITEM_HEIGHT = 54;1214 @@ -60,6 +62,11 @@ export function pauseMotionFrame({
6062 const openProgress = reducedMotion ? 1 : progress(now, openedAt, OPEN_DURATION);6163 const exitProgress = reducedMotion ? 1 : progress(now, exitStartedAt, EXIT_DURATION);6264 const titleProgress = reducedMotion ? 1 : progress(now, openedAt + 60, 160);65+ const hintProgress = reducedMotion66+ ? 167+ : exiting68+ ? 1 - easeInCubic(exitProgress)69+ : easeOutCubic(progress(now, openedAt + HINT_DELAY, HINT_DURATION));6370 const itemProgress = Array.from({ length: itemCount }, (_, index) => {6471 if (reducedMotion) return 1;6572 if (exiting) {@@ -88,12 +95,16 @@ export function pauseMotionFrame({
8895 exitProgress,8996 shutterProgress: exiting ? 1 - easeInCubic(exitProgress) : easeOutBack(openProgress),9097 titleProgress,98+ hintProgress,9199 itemProgress,92100 selectedPosition,93101 pawHop,94102 pressProgress,95103 pressOffset,96- active: phase !== 'idle' || (now - selectedAt < SLIDE_DURATION) || (pressedIndex >= 0 && now - pressedAt < PRESS_DURATION),104+ active: phase !== 'idle' ||105+ (now - selectedAt < SLIDE_DURATION) ||106+ (pressedIndex >= 0 && now - pressedAt < PRESS_DURATION) ||107+ (!reducedMotion && now - openedAt - HINT_DELAY < HINT_DURATION),97108 };98109 }99110 @@ -1,6 +1,6 @@
11 import { drawBoard } from "./board.js";22 import { drawIndex } from "./index.js";3-import { drawKeycap, drawTouchControls } from "./stage-hud.js";3+import { drawKeycap } from "./stage-hud.js";44 import { pauseMotionFrame, pauseMotionLayout } from "./menu-motion.js";55 66 export function wrapCanvasText(ctx, value, maxWidth) {@@ -19,6 +19,10 @@ export function wrapCanvasText(ctx, value, maxWidth) {
1919 return lines;2020 }2121 22+export function pausePawX(width) {23+ return Math.max(24, width / 2 - 176);24+}25+2226 export function renderPauseScene(ctx, view = {}) {2327 const { width: w, height: h } = view;2428 const {@@ -83,6 +87,7 @@ export function renderPauseScene(ctx, view = {}) {
8387 ctx.fillStyle = "#FF4B3E";8488 ctx.fillRect(w / 2 - 160, selectedY - 27, 320, pauseMotionLayout.height);8589 }90+ const pawX = pausePawX(w);8691 pauseItems.forEach((item, i) => {8792 const itemMotion = motion.itemProgress[i] ?? 1;8893 const y = itemTop + i * pauseMotionLayout.pitch - (1 - itemMotion) * 22;@@ -92,13 +97,14 @@ export function renderPauseScene(ctx, view = {}) {
9297 ctx.fillStyle = "#0F0E0D";9398 ctx.font = "800 20px 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif";9499 ctx.textAlign = "right";95- ctx.fillText(i === Math.round(motion.selectedPosition) ? "🐾" : "", w / 2 - 176,100+ ctx.fillText(i === Math.round(motion.selectedPosition) ? "🐾" : "", pawX,96101 y + 1 - (i === Math.round(motion.selectedPosition) ? motion.pawHop : 0));97102 ctx.fillStyle = labelInHighlight ? "#0F0E0D" : "#F2EEE6";98103 ctx.font = "800 13px Recursive, monospace"; ctx.textAlign = "center";99104 ctx.fillText(item.label, w / 2, y + 1 + (i === selected ? motion.pressOffset : 0));100105 ctx.globalAlpha = 1;101106 });107+ ctx.globalAlpha = motion.hintProgress;102108 ctx.textAlign = "left"; ctx.fillStyle = "#8E877C"; ctx.font = "500 11px Recursive, monospace";103109 if (pausedFrom !== "index" && pausedFrom !== "title") {104110 const hintLines = wrapCanvasText(ctx, boardState?.hint || "Choose an action", Math.max(120, w - 64));@@ -111,5 +117,6 @@ export function renderPauseScene(ctx, view = {}) {
111117 drawKeycap(ctx, "↑↓", "choose", 32, h - 46);112118 drawKeycap(ctx, "ENTER", "select", w / 2 - 4, h - 46, "right");113119 drawKeycap(ctx, "ESC", "back", w - 32, h - 46, "right");114- } else drawTouchControls(ctx, w, h, coarsePointer);120+ }121+ ctx.globalAlpha = 1;115122 }@@ -1,7 +1,7 @@
11 import test from "node:test";22 import assert from "node:assert/strict";33 import { pauseMotionFrame, pauseMotionTimings } from "../src/render/menu-motion.js";4-import { wrapCanvasText } from "../src/render/stage-pause.js";4+import { pausePawX, wrapCanvasText } from "../src/render/stage-pause.js";55 66 test("pause menu drops items on a 35ms stagger and settles at the end", () => {77 const start = 1000;@@ -44,3 +44,19 @@ test("pause hints wrap to the available canvas width", () => {
4444 "through the rock.",4545 ]);4646 });47+48+test("pause paw stays inside narrow menu bounds", () => {49+ assert.equal(pausePawX(390), 24);50+ assert.equal(pausePawX(1280), 464);51+});52+53+test("pause hints wait until the menu lands and fade out on exit", () => {54+ const before = pauseMotionFrame({ now: 1200, openedAt: 1000, phase: "opening", itemCount: 2 });55+ const after = pauseMotionFrame({ now: 1400, openedAt: 1000, phase: "idle", itemCount: 2 });56+ assert.equal(before.hintProgress, 0);57+ assert.equal(after.hintProgress, 1);58+ const exiting = pauseMotionFrame({ now: 1100, openedAt: 0, phase: "exiting", exitStartedAt: 1000, itemCount: 2 });59+ assert.ok(exiting.hintProgress < 1);60+ assert.ok(exiting.hintProgress > 0);61+ assert.equal(pauseMotionFrame({ now: 1400, openedAt: 1000, phase: "idle", reducedMotion: true }).hintProgress, 1);62+});