Pixel art tools show only useful options and remove any colour (#201)
game-maker / gate (push) Successful in 1m42s
game-maker / suite (push) Successful in 8m15s

The pixel art side bar showed every tool option all the time, the
palette used only part of the column width and "Remove" could only
drop the last colour. Now each tool shows what it needs, the palette
fills the column and any colour can be removed.

- Options per tool live in toolOptions.ts. Brush shows for pencil,
  eraser, line and unfilled shapes. Filled shapes shows for rectangle
  and ellipse. Contiguous shows for fill. Picker has none.
- All options share one row with a fixed height, so nothing below
  jumps when the tool changes.
- The palette picks its column count from the width and stretches the
  swatches until the row ends flush. At 1280 px that is 11 per row
  instead of 8.
- The colour field takes the space the two buttons leave, so "Remove"
  is no longer cut off.
- "Remove" deletes the selected colour. Pixels of that colour turn
  transparent, later indices move down by one in every image that uses
  the palette, and palette plus images change in one undo step.
  Transparent itself cannot be removed.

Decisions:
- Pixels of a removed colour become transparent, like erasing
  (chosen by the user before the build).

Findings:
- The Filled checkbox slid away under the pointer. The brush button
  sat to its left and vanished when Filled turned on, so the second
  click missed. The brush button now sits to the right, and a check
  clicks the same spot twice.
- One start-up FAIL "debug hook present" came back never in three
  later runs. It is the known start flake.

Tests:
- ./check.sh: 141 files, 2080 tests, exit 0 (before 140 / 2070)
- 10 new unit tests in tests/editor/pixel-palette.test.ts
- GM_STEPS=48 (fresh seed): ALL PASSED, 40 checks, 4 of them new
- Counter-check: indices not shifting and brush always visible make
  4 unit tests and exactly the 2 matching e2e checks fail
- Check total 1282 (1278 + 4) in README and wiki, computed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-22 11:58:31 +02:00
1 parent 41772e53f7
commit fe894c630d
10 files changed
+424 -55

No files matched your search

+107 -1
View File
@@ -205,9 +205,11 @@ export async function pixelArt(t) {
JSON.stringify({ rectCount, lineCount }),
);
await undo();
// Filled shapes stands beside the shape tools only.
await press_('ellipse');
const filledBox = await control('filled');
await click(filledBox.x, filledBox.y);
await press_('ellipse');
await settle();
await paint([20, 20], [27, 27]);
data = await cel(id, 0, 1);
check(
@@ -526,6 +528,110 @@ export async function pixelArt(t) {
JSON.stringify({ recolouredColor, recoloured, restored }),
);
// 48k-ii. Remove takes the picked colour out of the middle (#201): its pixels turn transparent,
// the later colours move down one index, and every other pixel keeps the colour it had.
const paletteOf = () =>
json(
`(() => { const p = __gm.state.project; const a = p.assets.find((x) => x.id === ${JSON.stringify(id)});` +
` return p.assets.find((x) => x.id === a.props.palette).colors; })()`,
);
const colorsBefore = await paletteOf();
const allCels = async () => {
const a = await asset(id);
const out = [];
for (let f = 0; f < a.frames.length; f++)
for (let l = 0; l < a.layers.length; l++) out.push(await cel(id, f, l));
return out;
};
const celsBefore = await allCels();
await swatch(GREEN);
const removeButton = await control('removeColor');
await click(removeButton.x, removeButton.y);
await settle();
const colorsAfter = await paletteOf();
const celsAfter = await allCels();
const shifted = celsBefore.every((before, c) =>
before.every((i, p) => celsAfter[c][p] === (i === GREEN ? 0 : i > GREEN ? i - 1 : i)),
);
// The colour after it moves into its index, so an index that moved down still names its colour.
check(
'Remove takes the picked colour out: its pixels go transparent, every other pixel keeps its colour',
removeButton.text === 'Remove' &&
colorsAfter.length === colorsBefore.length - 1 &&
colorsAfter[GREEN - 1] === colorsBefore[GREEN] &&
celsBefore.some((before) => before.includes(GREEN)) &&
shifted,
JSON.stringify({
text: removeButton.text,
before: colorsBefore.length,
after: colorsAfter.length,
shifted,
}),
);
await undo();
const celsUndone = await allCels();
check(
'and one undo gives back the colour and every pixel',
JSON.stringify(await paletteOf()) === JSON.stringify(colorsBefore) &&
JSON.stringify(celsUndone) === JSON.stringify(celsBefore),
JSON.stringify({ colors: (await paletteOf()).length }),
);
// 48k-iii. The palette runs across the whole width of the panel (#201).
await control('palette');
const span = await json('__gm.pixelArt.sideSpan()');
const firstSwatch = await json(`__gm.pixelArt.swatch(0)`);
const cell = (await json(`__gm.pixelArt.swatch(1)`)).x - firstSwatch.x - 2;
let perRow = 1;
while (perRow < 32 && (await json(`__gm.pixelArt.swatch(${perRow})`)).y === firstSwatch.y) perRow++;
const lastInRow = await json(`__gm.pixelArt.swatch(${perRow - 1})`);
// Swatches are whole pixels wide, so the row may stop up to a pixel per swatch short.
const reach = { left: firstSwatch.x - cell / 2, right: lastInRow.x + cell / 2 };
const removeRight = removeButton.x + removeButton.w / 2;
check(
'the palette fills the width of the panel, more than eight swatches to a row, and Remove ends inside it',
perRow > 8 &&
Math.abs(reach.left - span.left) <= 1 &&
span.right - reach.right <= perRow &&
removeRight <= span.right + 0.5,
JSON.stringify({ span, reach, perRow, cell, removeRight }),
);
// 48k-iv. Each tool shows only the options that change what it paints (#201).
const options = async () => ({
brush: (await json(`__gm.pixelArt.control('brush')`)) !== null,
filled: (await json(`__gm.pixelArt.control('filled')`)) !== null,
contiguous: (await json(`__gm.pixelArt.control('contiguous')`)) !== null,
});
const shownFor = {};
for (const tool of ['pencil', 'fill', 'picker', 'rect']) {
await press_(tool);
shownFor[tool] = await options();
}
const filledToggle = await control('filled');
await click(filledToggle.x, filledToggle.y);
await settle();
shownFor.filledRect = await options();
// The box stays where it was while the brush beside it goes, so the same spot switches it off.
await click(filledToggle.x, filledToggle.y);
await settle();
shownFor.outlinedAgain = (await json(`__gm.pixelArt.control('brush')`)) !== null;
const want = {
pencil: { brush: true, filled: false, contiguous: false },
fill: { brush: false, filled: false, contiguous: true },
picker: { brush: false, filled: false, contiguous: false },
rect: { brush: true, filled: true, contiguous: false },
filledRect: { brush: false, filled: true, contiguous: false },
outlinedAgain: true,
};
check(
'brush, Filled shapes and Contiguous show only with the tools they change',
JSON.stringify(shownFor) === JSON.stringify(want),
JSON.stringify(shownFor),
);
await press_('pencil');
await shot('48-pixel-tools');
// 48l. A sprite wears the second frame; the picker offers every frame by name.
await mutate(`
const project = __gm.state.project;