A recording of the editor is as large as its canvas, and so far the canvas always followed the window. With ?w=1920&h=1080 the editor is laid out at exactly that size in CSS pixels, whatever the window. - src/ui/pageSize.ts reads w and h (both needed, 200 to 8192 each) and works out how the canvas fits the window. - A smaller window shows the canvas shrunk to fit, centred, with the same aspect. A larger one shows it 1:1, centred on a dark page. - Clicks still land right, since Pixi maps them over the canvas's on-screen box. Switching projects keeps w and h in the URL. - The pointer drawn into an editor recording now scales against the layout size, so a shrunk canvas still records a full-size pointer. Tests: - Unit page-size: 5 new, green. - e2e 62d: ?w=1600&h=900 in a 960x720 window gives a 1600x900 canvas shown at 960x540, y 90, and a click on the menu button opens it. Red without the shrinking. - Gate on melektaus-atx: 304 files, 4691 tests green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
6.0 KiB
JavaScript
144 lines
6.0 KiB
JavaScript
/**
|
||
* The interface on a screen scaled to 150 %: the one thing every other step is blind to, since the
|
||
* harness runs at 100 %, where a layout pixel is a device pixel. Text has to come out as hard as it
|
||
* does at 100 %, and a hairline has to be one device pixel of its own colour, not one and a half
|
||
* with the half mixed. Reloads the page twice, so the steps after it start from a fresh editor.
|
||
*/
|
||
export async function devicePixels(t) {
|
||
const { check, json, cdp, navigate, select, storedObjects, wheel, exceptions } = t;
|
||
const size = { width: await json('innerWidth'), height: await json('innerHeight') };
|
||
const scale = (deviceScaleFactor) =>
|
||
cdp.send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor, mobile: false });
|
||
// The toolbar: captions on a plain ground, read at 100 % and at 150 %.
|
||
const textEdges = () => json(`__gm.edgeContrast(0, 0, innerWidth, 30)`);
|
||
const pickFirst = async () => {
|
||
const holderId = await json('__gm.state.activeScene.id');
|
||
const first = (await storedObjects())[0];
|
||
await select({ kind: 'object', holderId, objectId: first.id, objectIds: [first.id] });
|
||
};
|
||
|
||
await pickFirst();
|
||
const plain = { grid: await json('__gm.pixelGrid()'), edges: await textEdges() };
|
||
|
||
await scale(1.5);
|
||
await navigate();
|
||
await pickFirst();
|
||
|
||
// 62a. The editor knows the grid it draws on.
|
||
const grid = await json('__gm.pixelGrid()');
|
||
check(
|
||
'at 150 % a hairline is one device pixel wide',
|
||
plain.grid.resolution === 1 &&
|
||
plain.grid.hairline === 1 &&
|
||
grid.resolution === 1.5 &&
|
||
Math.abs(grid.hairline * 1.5 - 1) < 1e-9,
|
||
JSON.stringify({ plain: plain.grid, grid }),
|
||
);
|
||
|
||
// 62b. Both lines of every band of the inspector, read down a column of device pixels: one row
|
||
// each, in the border's own colour, and only the band's colour between them. A band on an odd
|
||
// layout pixel starts half-way between two device pixels, which is the case that blurs.
|
||
const colours = {
|
||
border: await json(`__gm.themeColor('border')`),
|
||
header: await json(`__gm.themeColor('panelHeader')`),
|
||
};
|
||
// One pixel of scroll flips every band between even and odd, so both are read. The window is made
|
||
// too low for the inspector's rows first: one that shows them all has nothing to scroll.
|
||
await cdp.send('Emulation.setDeviceMetricsOverride', {
|
||
width: size.width,
|
||
height: 420,
|
||
deviceScaleFactor: 1.5,
|
||
mobile: false,
|
||
});
|
||
await t.settle();
|
||
const inspector = await json('__gm.inspectorRect()');
|
||
const bands = [];
|
||
for (const scrolled of [0, 1]) {
|
||
if (scrolled) await wheel(inspector.x + inspector.w / 2, inspector.y + inspector.h / 2, 1);
|
||
for (const title of ['SWITCHES', 'POSITION', 'TRANSFORM', 'PROPERTIES']) {
|
||
const band = await json(`__gm.section(${JSON.stringify(title)})`);
|
||
if (!band) continue;
|
||
const { x, y, w, h } = band.rect;
|
||
// A band cut off by the edge of the panel has no second line to read.
|
||
if (y < inspector.y || y + h > inspector.y + inspector.h) continue;
|
||
const column = await json(`__gm.deviceColumn(${x + w - 40}, ${y}, ${h})`);
|
||
const runs = [];
|
||
for (const colour of column) {
|
||
if (runs.at(-1)?.colour === colour) runs.at(-1).rows += 1;
|
||
else runs.push({ colour, rows: 1 });
|
||
}
|
||
bands.push({ title, y, runs: runs.map((run) => `${run.colour}*${run.rows}`) });
|
||
}
|
||
}
|
||
const crisp = (band) =>
|
||
band.runs.length === 3 &&
|
||
band.runs[0] === `${colours.border}*1` &&
|
||
band.runs[1].startsWith(`${colours.header}*`) &&
|
||
band.runs[2] === `${colours.border}*1`;
|
||
const odd = (band) => Math.round(band.y) % 2 === 1;
|
||
check(
|
||
'the lines of a band are one device pixel each, in the unmixed colour',
|
||
bands.every(crisp) && bands.some(odd) && bands.some((band) => !odd(band)),
|
||
JSON.stringify({ bands, colours }),
|
||
);
|
||
await scale(1.5);
|
||
await t.settle();
|
||
|
||
// 62c. Text keeps its edges: against the same captions at 100 %, where nothing can sit between pixels.
|
||
const edges = await textEdges();
|
||
const kept = edges / plain.edges;
|
||
check(
|
||
'text is as hard-edged at 150 % as at 100 %',
|
||
kept > 0.96,
|
||
JSON.stringify({ plain: plain.edges, edges, kept }),
|
||
);
|
||
await t.shot('62-device-pixels');
|
||
|
||
// 62d. `?w=&h=` give the canvas a size of its own: the editor is laid out at that size, a window
|
||
// too small for it shows it shrunk to fit and centred, and a click lands where the picture is.
|
||
await cdp.send('Emulation.setDeviceMetricsOverride', {
|
||
width: 960,
|
||
height: 720,
|
||
deviceScaleFactor: 1,
|
||
mobile: false,
|
||
});
|
||
await cdp.send('Page.navigate', { url: `${t.baseUrl}?project=${t.projectId}&w=1600&h=900` });
|
||
await t.sleep(4000);
|
||
await t.waitUntil(`window.__gm?.state?.project?.id === ${JSON.stringify(t.projectId)}`, {
|
||
timeout: 15000,
|
||
label: 'the editor at a fixed size is up',
|
||
});
|
||
const fixed = await json(`(() => {
|
||
const canvas = document.querySelector('canvas');
|
||
const r = canvas.getBoundingClientRect();
|
||
const round = (v) => Math.round(v * 100) / 100;
|
||
return { w: canvas.width, h: canvas.height, shown: { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) } };
|
||
})()`);
|
||
const menu = await json('__gm.toolbarAt("menu")');
|
||
const factor = fixed.shown.w / fixed.w;
|
||
await t.click(fixed.shown.x + menu.x * factor, fixed.shown.y + menu.y * factor);
|
||
const rows = await json('__gm.menuRows()');
|
||
await t.press('Escape');
|
||
check(
|
||
'?w=1600&h=900 lays the editor out at 1600×900, shrinks it into a 960×720 window and still takes clicks',
|
||
fixed.w === 1600 &&
|
||
fixed.h === 900 &&
|
||
fixed.shown.x === 0 &&
|
||
fixed.shown.y === 90 &&
|
||
fixed.shown.w === 960 &&
|
||
fixed.shown.h === 540 &&
|
||
Array.isArray(rows) &&
|
||
rows.includes('Start recording'),
|
||
JSON.stringify({ fixed, menu, rows }),
|
||
);
|
||
|
||
await scale(1);
|
||
await navigate();
|
||
const back = await json('__gm.pixelGrid()');
|
||
check(
|
||
'back at 100 % without exceptions',
|
||
back.resolution === 1 && exceptions().length === 0,
|
||
JSON.stringify({ back, exceptions: exceptions() }),
|
||
);
|
||
}
|