Let a pixel perfect camera draw on the game view's pixel grid
A camera that follows smoothly stands between pixels most of the time, and pixel art then wanders between them. The new camera switch draws what the camera shows on the game view's own grid, while positions in the model keep their fractions. - `Pixel perfect` is a camera row, off by default. It works at every fixed game scale and at Auto, but not at Fit and not in the Scene tab, where one works with the fractions. - The grid has one step per device pixel of a world unit. At 100% that is whole units, at 500% fifths. The view carries it (`View.grid`), so no view draws with the grid of another. - The view looks from a point that puts the world's origin on a whole device pixel (`lookFrom`). This also covers odd camera sizes and views that cut the picture to an odd size. - Sprites, texts and tilemaps snap their top left corner when they are unturned, so an odd size lands on whole pixels too. A turned object snaps its pivot (`snapPlacement` in `pixelGrid.ts`). - Under a camera shader the picture is taken the same way, and the objects in it use the picture's grid. With Camera pixels that is one step per camera pixel. - An overlay camera has its own switch and snaps only while the main picture is at a fixed scale. - Particles are drawn where they are. - New test hook `__gm.gameDrawnRect(id)` reads where a sprite's picture is drawn, which `gameRect` (the model's) cannot tell. Found: the e2e fixture at 300% drew the crate at y 205.5 device pixels without moving the camera, because the view cut to an odd height puts its centre on half a pixel. With the switch on it is at 206. Tests: - Gate: 237 files, 3587 tests (+10 in pixel-grid.test.ts) - e2e 2,3: ALL PASSED, 75 checks (+1, 3j) - Counter-checks: without the origin snap 3 unit tests fail, with the centre snapped instead of the corner 2 fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
92395f6773
commit
1665576fb9
14 files changed
+353
-33
No files matched your search
@@ -418,6 +418,50 @@ export async function play(t) {
|
||||
await showTab('game');
|
||||
g = await pickScale('100%');
|
||||
check('and 100% can be picked back', g.mode === 1 && g.scale === 1, JSON.stringify(g));
|
||||
|
||||
// 3j. Pixel perfect, a switch on the camera: at a fixed scale the crate is drawn on whole device
|
||||
// pixels although the camera stands between them. Off, and at fit, it is drawn where the model
|
||||
// puts it. Read off the drawn picture, since the model keeps its fractions either way.
|
||||
const cameraX = (await object('o3')).position.x;
|
||||
const moveCamera = (x) =>
|
||||
mutate(`__gm.state.activeScene.objects.find(o => o.id === 'o3').position.x = ${x}`);
|
||||
const drawnCrate = async () => {
|
||||
const drawn = await json(`__gm.gameDrawnRect('o1')`);
|
||||
const model = await json(`__gm.gameRect('o1')`);
|
||||
const ratio = drawn.w / model.w;
|
||||
const left = (model.x - model.w / 2) * ratio;
|
||||
const top = (model.y - model.h / 2) * ratio;
|
||||
const whole = (v) => Math.abs(v - Math.round(v)) < 1e-6;
|
||||
return {
|
||||
onGrid: whole(drawn.x) && whole(drawn.y),
|
||||
asModel: Math.abs(drawn.x - left) < 1e-6 && Math.abs(drawn.y - top) < 1e-6,
|
||||
offModel: Math.max(Math.abs(drawn.x - left), Math.abs(drawn.y - top)),
|
||||
drawn,
|
||||
};
|
||||
};
|
||||
await pickScale('300%');
|
||||
await moveCamera(cameraX + 0.37);
|
||||
const off = await drawnCrate();
|
||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o3' });
|
||||
const pixelSwitch = await json("__gm.inspectorField('Pixel perfect')");
|
||||
await click(pixelSwitch.x, pixelSwitch.y);
|
||||
const on = await drawnCrate();
|
||||
await pickScale('Fit');
|
||||
const atFit = await drawnCrate();
|
||||
check(
|
||||
'a pixel perfect camera draws the crate on whole pixels at 300%, but not at fit or switched off',
|
||||
(await object('o3')).overrides.pixelPerfect === true &&
|
||||
!off.onGrid &&
|
||||
off.asModel &&
|
||||
on.onGrid &&
|
||||
// The origin and the corner move half a device pixel at most each.
|
||||
on.offModel <= 1 + 1e-6 &&
|
||||
atFit.asModel,
|
||||
JSON.stringify({ off, on, atFit }),
|
||||
);
|
||||
await undo();
|
||||
await moveCamera(cameraX);
|
||||
await pickScale('100%');
|
||||
await showTab('scene');
|
||||
check(
|
||||
'the fixture is left as it was found',
|
||||
|
||||
Reference in new issue
Block a user