Let the video tab's Play start at the playhead and reset its button
game-maker / gate (push) Successful in 4m18s
game-maker / suite (push) Successful in 17m57s

Two faults in the video tab. A click into the ruler while a clip
played stopped the playback, but the button kept showing "Stop". And
Play always started at the start of the clip, wherever the playhead
stood.

- The tab now reports when the ruler stops the playback, and the
  button switches back to "Play".
- Play starts at the playhead when it lies inside the clip. Without
  a selected clip the whole video counts. Outside or at the end,
  Play starts at the beginning.
- So Stop and Play continue where the playback stopped. A clip that
  ran to its end without loop starts again at the beginning.

Tests: gate on melektaus-atx green. e2e 66 ALL PASSED, 32 checks (2
new in 66e-i). With both fixes taken back, both checks are red with
exactly the reported faults: the button shows "Stop", and Play runs
from 0.71 s, i.e. from the clip start at 0.6 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-12 13:55:13 +02:00
1 parent f177ee90a4
commit 080df2d211
5 files changed
+60 -13

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, 2039 checks (GM_STEPS=20 for one step)
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2041 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
+1
View File
@@ -28,6 +28,7 @@ export function videoHooks({ state, editor }: HookContext) {
/** The middle of the clip strip, and of the track under it, at a second of the video. */
stripAt: (seconds: number) => videoEditor.canvas.stripAt(seconds),
trackAt: (seconds: number) => videoEditor.canvas.trackAt(seconds),
rulerAt: (seconds: number) => videoEditor.canvas.rulerAt(seconds),
/** The time's zoom, and whether a transition is on its way. */
zoom: () => videoEditor.canvas.zoomInfo(),
},
+32 -11
View File
@@ -129,7 +129,7 @@ export class VideoCanvas extends Widget {
/** The span the player is held to while it plays: the picked clip, else the whole video. */
private playing: CutSpan | null = null;
onTogglePlay: () => void = () => {};
/** The player stopped by itself (the end of a clip that does not repeat). */
/** The player stopped by itself (the end of a clip that does not repeat) or by a click in the ruler. */
onStopped: () => void = () => {};
constructor(private readonly state: EditorState) {
@@ -202,28 +202,43 @@ export class VideoCanvas extends Widget {
return this.playing !== null;
}
/** Plays the picked clip, else the whole video, from its start, with the video's volume and loop. */
/**
* Plays the picked clip, else the whole video, with the video's volume and loop: from the
* playhead when it stands inside, else from the start.
*/
play(): void {
const asset = this.asset();
const span = this.shownSpan();
const element = asset && this.player(asset);
if (!asset || !span || !element) return;
const preview = this.preview;
if (!asset || !span || !element || !preview) return;
this.playing = span;
element.muted = false;
element.volume = videoVolumeOf(asset.props);
// A moment still wanted from scrubbing would send it back after the start.
if (this.preview) this.preview.chase.wanted = null;
element.currentTime = span.start;
preview.chase.wanted = null;
const head = preview.asked;
element.currentTime = head >= span.start && head < span.end - LEAST_SPAN ? head : span.start;
void element.play().catch(() => {});
this.invalidate();
}
stop(): void {
const preview = this.preview;
// The playhead stays where it stopped; the next Play goes on from there.
if (this.playing && preview) preview.asked = preview.element.currentTime;
this.playing = null;
this.preview?.element.pause();
preview?.element.pause();
this.invalidate();
}
/** Stops from inside the canvas, and tells the header so its button follows. */
private halt(): void {
if (!this.isPlaying) return;
this.stop();
this.onStopped();
}
/** Where the tab's player stands, in seconds of the video; null while nothing is shown. */
get playhead(): number | null {
return this.preview && this.preview.file === this.asset()?.file ? this.preview.element.currentTime : null;
@@ -239,10 +254,7 @@ export class VideoCanvas extends Widget {
if (videoLoopsOf(asset.props)) {
element.currentTime = span.start;
if (element.paused) void element.play().catch(() => {});
} else {
this.stop();
this.onStopped();
}
} else this.halt();
}
this.redraw();
}
@@ -277,6 +289,15 @@ export class VideoCanvas extends Widget {
};
}
/** The middle of the ruler at a second of the video, on screen (test hook). */
rulerAt(seconds: number): Vec2 {
const origin = this.getGlobalPosition();
return {
x: origin.x + secondsToX(this.view, seconds),
y: origin.y + this.timelineTop + RULER_HEIGHT / 2,
};
}
/** The middle of the track at a second of the video, on screen (test hook). */
trackAt(seconds: number): Vec2 {
const origin = this.getGlobalPosition();
@@ -418,7 +439,7 @@ export class VideoCanvas extends Widget {
const from = Math.max(0, Math.min(asset.duration, xToSeconds(this.view, p.x)));
if (y < RULER_HEIGHT) {
this.gesture = { kind: 'seek' };
this.stop();
this.halt();
this.seekTo(from);
return;
}
+1 -1
View File
@@ -24,7 +24,7 @@ export class VideoEditor extends AssetTabEditor<VideoAsset, VideoCanvas> {
this.playButton = new Button('Play', {
icon: TOOLBAR_ICON_SVG.play,
minWidth: 72,
tooltip: 'Play the picked clip, else the whole video (Space)',
tooltip: 'Play the picked clip, else the whole video, from the playhead (Space)',
onClick: () => this.togglePlay(),
});
this.downloadButton = new Button('Download', {
+25
View File
@@ -159,6 +159,31 @@ export async function video(t) {
await click(playButton.x, playButton.y);
check('Stop holds it', (await json('__gm.videoTab.playing()')) === false);
// 66e-i. A click in the ruler while it plays stops it, and the header's button says Play again.
// Play then goes on from that moment, not from the clip's start.
const tabState =
'({ playing: __gm.videoTab.playing(), head: __gm.videoTab.playhead(), button: __gm.videoTab.playButton().text })';
await click(playButton.x, playButton.y);
await sleep(150);
const ruler = await json('__gm.videoTab.rulerAt(1)');
await click(ruler.x, ruler.y);
await sleep(150);
const halted = await json(tabState);
await click(playButton.x, playButton.y);
await sleep(150);
const resumed = await json(tabState);
await click(playButton.x, playButton.y);
check(
'a click in the ruler stops the play and its button',
halted.playing === false && halted.button === 'Play' && Math.abs(halted.head - 1) < 0.05,
JSON.stringify(halted),
);
check(
'Play goes on from the playhead',
resumed.playing && resumed.button === 'Stop' && resumed.head >= 1 && resumed.head < 1.4,
JSON.stringify(resumed),
);
// 66e-ii. With the clip picked (#403), a drag in the track off its handles or in the strip beside
// its band changes nothing; one on its band moves it whole, one undo step. A click in the
// track beside it picks the video, on it the clip again.