Timeline keys can be copied and pasted at the playhead, duplicated with Ctrl+drag, and frames inserted or removed from a right-click menu (#102)
game-maker / gate (push) Successful in 1m34s
game-maker / suite (push) Successful in 5m38s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 12:53:56 +02:00
1 parent 195fc94e08
commit bf02c9e8b3
11 files changed
+666 -23

No files matched your search

+75 -5
View File
@@ -5,9 +5,9 @@ const RED = '#ff0000';
const SIZE = 40;
/**
* Animations (#97, #98, #99, #110): a row is added through the dialog, keys are set, moved, banded
* and deleted on the timeline with real gestures, and the preview above it is read at the playhead
* – as a model value, as a screen rectangle and as pixels. The subject is a container whose child
* Animations (#97, #98, #99, #110, #102): a row is added through the dialog, keys are set, moved,
* banded, deleted, copied, pasted and duplicated on the timeline with real gestures, frames are
* inserted, and the preview above it is read at the playhead – as a model value, as a screen rectangle and as pixels. The subject is a container whose child
* is animated, since a row aiming inside the root is the case a single sprite cannot show. It
* removes everything it made.
*/
@@ -231,10 +231,80 @@ export async function animation(t) {
JSON.stringify({ held, reset }),
);
// 25g. Undo all the way: the key at 5, the value edit, the keys at 10 and 0, then the row. Exactly
// 25g. Copy and paste (#102): two keys banded, Ctrl+C, the playhead to 12, Ctrl+V – they land
// relative to the playhead, keeping their distance, and come back picked. Each step undone.
await mutate(`__gm.state.project.assets.find((a) => a.id === 'an-walk').props.playback = 'loop'`);
const three = JSON.stringify(withMiddle);
const pickedFrames = async () =>
(await json('__gm.animation.keys()')).filter((k) => k.selected).map((k) => k.frame);
const c3 = await cell(3);
await drag((await cell(14)).x, c0.y + 40, c3.x, c0.y - 11);
const copied = await pickedFrames();
await press('c', { ctrl: true });
const clip = await json('__gm.animation.clipboard()');
await scrubTo(12);
await press('v', { ctrl: true });
const pasted = await keysOf();
const pastedPick = await pickedFrames();
check(
'Ctrl+C and Ctrl+V paste the banded keys relative to the playhead and pick them',
copied.join() === '5,10' &&
JSON.stringify(clip?.rows?.[0]?.keys?.map((k) => k.offset)) === '[0,5]' &&
JSON.stringify(pasted) ===
JSON.stringify([...withMiddle, { frame: 12, value: 50 }, { frame: 17, value: 100 }]) &&
pastedPick.join() === '12,17',
JSON.stringify({ copied, clip, pasted, pastedPick }),
);
await undo();
check(
'one undo takes the paste back',
JSON.stringify(await keysOf()) === three,
JSON.stringify(await keysOf()),
);
// Ctrl+drag duplicates: the key at 5 stays, a copy lands on 7.
const k5 = await cell(5);
const k7 = await cell(7);
await click(k5.x, k5.y);
await drag(k5.x, k5.y, k7.x, k7.y, { modifiers: { ctrl: true } });
const duplicated = await keysOf();
check(
'Ctrl+drag leaves the key where it was and drops a copy where the pointer lets go',
JSON.stringify(duplicated) ===
JSON.stringify([withMiddle[0], withMiddle[1], { frame: 7, value: 50 }, withMiddle[2]]),
JSON.stringify(duplicated),
);
await undo();
// Right-click → Insert frames… at the playhead: every key from there on moves by the count.
await scrubTo(5);
const inCells = await cell(2);
await click(inCells.x, inCells.y + 40, 'right');
const insertItem = await find('Insert frames at 5…', true);
await click(insertItem.x, insertItem.y);
await evaluate('__gm.animation.framesDialogCount(3)');
await settle();
const dialog = await json('__gm.animation.framesDialog()');
const confirm = await json('__gm.animation.framesDialogConfirm()');
await click(confirm.x, confirm.y);
const inserted = await keysOf();
check(
'the menu inserts 3 frames at the playhead: the keys on 5 and 10 move to 8 and 13, the one on 0 stays',
dialog?.summary === 'Moves 2 keys 3 frames later.' &&
JSON.stringify(inserted.map((k) => k.frame)) === '[0,8,13]' &&
(await json('__gm.animation.framesDialog()')) === null,
JSON.stringify({ dialog, inserted }),
);
await undo();
check(
'undo takes the inserted frames back',
JSON.stringify(await keysOf()) === three,
JSON.stringify(await keysOf()),
);
// 25h. Undo all the way: the key at 5, the value edit, the keys at 10 and 0, then the row. Exactly
// that many – one more would take back a command of an earlier step, so the count is checked
// on the way: after four the row is still there, empty.
await mutate(`__gm.state.project.assets.find((a) => a.id === 'an-walk').props.playback = 'loop'`);
await focusTimeline();
for (let i = 0; i < 4; i++) await undo();
const emptied = await walk();
+5 -2
View File
@@ -44,15 +44,18 @@ const KEYS = {
ArrowDown: { key: 'ArrowDown', code: 'ArrowDown', windowsVirtualKeyCode: 40 },
Delete: { key: 'Delete', code: 'Delete', windowsVirtualKeyCode: 46 },
a: { key: 'a', code: 'KeyA', windowsVirtualKeyCode: 65 },
c: { key: 'c', code: 'KeyC', windowsVirtualKeyCode: 67 },
d: { key: 'd', code: 'KeyD', windowsVirtualKeyCode: 68 },
g: { key: 'g', code: 'KeyG', windowsVirtualKeyCode: 71 },
s: { key: 's', code: 'KeyS', windowsVirtualKeyCode: 83 },
v: { key: 'v', code: 'KeyV', windowsVirtualKeyCode: 86 },
w: { key: 'w', code: 'KeyW', windowsVirtualKeyCode: 87 },
z: { key: 'z', code: 'KeyZ', windowsVirtualKeyCode: 90 },
};
/** CDP modifier bitmask: 2 = Ctrl, 8 = Shift. */
const modifierBits = ({ ctrl = false, shift = false } = {}) => (ctrl ? 2 : 0) | (shift ? 8 : 0);
/** CDP modifier bitmask: 1 = Alt, 2 = Ctrl, 8 = Shift. */
const modifierBits = ({ alt = false, ctrl = false, shift = false } = {}) =>
(alt ? 1 : 0) | (ctrl ? 2 : 0) | (shift ? 8 : 0);
class CDP {
constructor(ws) {