smaller stage, half steps above 2x — bigger, still even

Trimming the stage alone does nothing, and that is worth writing down: the
scale is snapped, so at 3x the game is 1512 device pixels wide whether the
stage is 560 or 512. Shrinking the surround only pays when it crosses a step.
So both halves land together.

── the stage ──

560x400 down to 512x352, which is expert (504x342, measured) plus four pixels
a side. There is nothing left to give — intermediate is 342 tall too, so
anything shorter clips a board people actually play.

── half steps from 2x ──

Not a retreat from whole-number scaling. The unit that has to stay regular is
the CELL, not the pixel: this is a 16px grid, so at 3.5x a cell is exactly 56
screen pixels — as even as 3x or 4x — and only sub-cell pixels alternate
between three and four. Below 2x that alternation is a third of a pixel rather
than a seventh, so whole numbers still apply there. The case that started all
this, a fractional devicePixelRatio at nominal 1:1, put the CELL boundaries at
26.4px, which is a different thing entirely.

Together, on a 1800x1400 device-pixel panel: 3.0x becomes 3.5x, and the game
goes from 1512 to 1764 device pixels wide.

Known cost, and it is in the README: the Extras > Auto Options dialog is 328
wide and opens 25px right of the game, so it needs a 554-wide stage to fit
whole from beginner. It cannot be closed from the panel anyway — no window
manager, no title bar, no OK button — so that is the thing to fix before this
number is revisited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
This commit is contained in:
2026-08-23 22:24:57 +00:00
co-authored by Claude Opus 5
parent 105068170c
commit e4247cd760
3 changed files with 42 additions and 24 deletions
+20 -12
View File
@@ -20,7 +20,7 @@ type SessionStatus = {
//
// Only a placeholder until `onReady`, which sizes the canvas from the real framebuffer. Drift with the
// sidecar's stage costs one frame of the wrong aspect ratio and nothing else.
const PLACEHOLDER = { width: 560, height: 400 };
const PLACEHOLDER = { width: 512, height: 352 };
const BUTTON_MASK: Record<number, number> = { 0: 1, 1: 2, 2: 4 };
@@ -82,25 +82,33 @@ export const MinesweeperPanel = () => {
}, [status?.running]);
/**
* How big to draw the stage — and the answer is always a WHOLE number of screen pixels per source
* pixel, or a whole number of source pixels per screen pixel.
* How big to draw the stage. Computed in DEVICE pixels, so a 2x display gets twice the detail rather
* than the same picture twice the size — and allowed to scale UP past the framebuffer's own size,
* without which a wide panel leaves the canvas sitting at a fraction of the pixels available to it.
*
* This is a 16px-per-cell bitmap from 1995. At a fractional scale some cells get three screen pixels
* and their neighbours get two, so the grid goes uneven and the digits wobble: sharp everywhere and
* wrong overall, which is what "not quite right" looks like on pixel art. `image-rendering: pixelated`
* does not save you — it removes the blur, not the unevenness.
* ── Why the scale is snapped, and to what ──
*
* Computed in DEVICE pixels, so a 2x display gets 2x the detail rather than the same picture twice as
* large. And it is allowed to scale UP past the framebuffer's own size, which is the part that was
* missing: on a 1x display the canvas sat at 800 CSS px in a panel twice that wide, using a quarter of
* the pixels available to it.
* This is a 16px-per-cell bitmap from 1995. At an arbitrary scale a cell is 26.4 screen pixels, so the
* grid lines themselves land unevenly and the digits wobble: sharp everywhere and wrong overall, which
* is what "not quite right" looks like on pixel art. `image-rendering: pixelated` does not save you —
* it removes the blur, not the unevenness. A fractional devicePixelRatio produces this at what looks
* like 1:1, which is the case that is easiest to miss.
*
* HALF steps are allowed from 2x up, and that is not a compromise on the above. The unit that has to
* stay regular is the CELL, not the pixel: at 3.5x a cell is exactly 56 screen pixels, so the grid is
* as even as it is at 3x or 4x, and only sub-cell pixels alternate between three and four. Below 2x
* that alternation is a third of the pixel rather than a seventh, so it stays whole-number there.
*
* The steps are what make the size quantised — at 3x the game is the same width whether the stage is
* 512 or 560 wide. Shrinking the stage is only worth anything when it crosses a step, which is why
* these two changes landed together.
*/
const stage = useMemo(() => {
if (!frame || !box || box.width <= 0 || box.height <= 0) return null;
const fit = Math.min((box.width * box.dpr) / frame.width, (box.height * box.dpr) / frame.height);
// Below 1 there is nothing to floor to, so take a unit fraction: the rows that survive are then
// evenly spaced instead of dropped in clumps.
const scale = fit >= 1 ? Math.floor(fit) : 1 / Math.ceil(1 / fit);
const scale = fit < 1 ? 1 / Math.ceil(1 / fit) : fit < 2 ? 1 : Math.floor(fit * 2) / 2;
return { width: (frame.width * scale) / box.dpr, height: (frame.height * scale) / box.dpr };
}, [frame, box]);