Hold a driven video at the end of its file instead of restarting (#404)

A driven video plays on for about 100 ms after the last key, until the
speed reading says it stands. With the key close to the end of the
file, that runs the video past it. The browser then marks it ended,
and follow() called play() on it in every frame. Per the HTML spec an
ended video starts over from the beginning of its file on play(). The
sprite flashed the first picture one to three times before it held.

- An ended video now counts as standing at its end. follow() holds it
  and seeks to the wanted picture instead of playing it again.
- The end of the file counts as an end too, not only the end of the
  clip.

Tests:
- e2e 66o plays an animation in once mode to 1.95 s of the 2 s test
  video and records the player time in every frame. It must never jump
  back, and the last picture must stand.
- Against the old follow() 66o fails, the player jumps back to 0 once.
  A first version with the key at 1.9 s stayed green, the video never
  reached the end of the file.
- e2e 66 on melektaus-atx: ALL PASSED, 26 checks.
- Unit video-library, video-follow, video-clock: 31 passed.
- tsc, eslint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-11 15:46:03 +02:00
1 parent 43fd22a763
commit 843b110bcb
3 files changed
+32 -2

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
The browser suite and the three example games need a machine with a GPU:
```bash
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2002 checks (GM_STEPS=20 for one step)
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2003 checks (GM_STEPS=20 for one step)
# every tool signs in first – see the Accounts page
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
+2 -1
View File
@@ -148,7 +148,8 @@ export class VideoLibrary {
target: drivenTimeOf(play, drive.time, element.duration),
speed: drive.reading?.speed ?? 0,
current: element.currentTime,
end: play.end,
// A video that ran to the end of its file starts over from its first picture on play().
end: element.ended ? element.currentTime : Math.min(play.end, element.duration),
});
if (step.seek !== null) this.seek(element, drive, step.seek);
if (step.kind === 'hold') {
+29
View File
@@ -381,6 +381,35 @@ export async function video(t) {
JSON.stringify(await rowKeys()) === keyed,
JSON.stringify({ ran, steps, duration: uploaded.duration, undone: await rowKeys() }),
);
// 66o. Played once to 1.95 s, the video runs on past the end of its file while the time still
// reads as moving. play() on an ended video starts it over from its first picture, and red
// flashed two or three times before the white of the last frame stood. Every frame is read in
// the page.
await mutate(`
const cut = __gm.state.project.assets.find((a) => a.id === '${CUT}');
cut.props.playback = 'once';
cut.tracks[0].keys = [{ frame: 0, value: 0.05 }, { frame: 19, value: 1.95 }];`);
await t.evaluate('__gm.animation.scrub(0)');
await waitFor(previewColor, (color) => color === COLORS.red, 10);
await t.evaluate(`window.__vd66 = [];
(function read() {
const player = __gm.videos().find((v) => v.key.includes('@') && !v.key.includes('${EARLY}') && !v.key.includes('${LATE}'));
if (player) window.__vd66.push(Math.round(player.time * 1000) / 1000);
if (window.__vd66.length < 400) requestAnimationFrame(read);
})();`);
await click(playCut.x, playCut.y);
await sleep(2800);
const times = await json('window.__vd66.splice(0)');
const endColor = await previewColor();
if ((await json('__gm.animation.playAt()')).text !== 'Play') await click(playCut.x, playCut.y);
const reached = times.findIndex((time) => time >= 1.8);
const fellBack = reached < 0 ? [] : times.slice(reached).filter((time) => time < 1.5);
check(
'a video played once to its last frame stays there and never shows its first picture again',
reached >= 0 && fellBack.length === 0 && endColor === COLORS.white,
JSON.stringify({ reached, fellBack, last: times.slice(-5), frames: times.length, endColor }),
);
await showTab('scene');
// 66m. Switched off, a driven sprite keeps its player ready at its Video time, longer than a player