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)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
195fc94e08
commit
bf02c9e8b3
11 files changed
+666
-23
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user