Zoom the video tab's time smoothly like the other tabs (#377)
game-maker / gate (push) Successful in 4m41s
game-maker / suite (push) Successful in 17m13s

The video tab jumped on every wheel step and on Fit. The Sound,
Pattern and Song tabs already ease the zoom since #377. The video
tab now takes the same transition, so all tabs feel alike.

- VideoCanvas runs wheel and Fit through `axisTween`. The duration
  comes from the General settings. Quick wheel steps add up, panning
  ends the transition, and the view that is stored is the one the
  transition lands on.
- Opening a video still fits at once without a transition. Only the
  Fit button eases, as in the Sound tab.
- `__gm.videoTab.zoom()` reports the zoom and whether a transition is
  on its way.

Tests:
- e2e 66 on melektaus-atx: ALL PASSED, 30 checks (1 new). The new
  check uses `sampleZoom` like the Sound tab. At 1000 ms the zoom is
  on its way two frames after the wheel (319 -> 321) and lands where
  the jump does (370.63). No counter-check against the old code.
- Gate on melektaus-atx: CHECK EXIT 0, 299 files, 4586 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-11 21:52:29 +02:00
1 parent e191dc4b2e
commit 322fc39d16
4 files changed
+33 -6

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, 2016 checks (GM_STEPS=20 for one step)
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2017 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
View File
@@ -28,6 +28,8 @@ 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),
/** The time's zoom, and whether a transition is on its way. */
zoom: () => videoEditor.canvas.zoomInfo(),
},
videoPicker: () => {
const picker = currentVideoPicker();
+26 -5
View File
@@ -42,6 +42,7 @@ import {
import { openVideo } from '../../viewport/VideoLibrary';
import type { EditorState } from '../state';
import { containRect } from '../texture/imageGrid';
import { axisTween } from '../viewTween';
const RULER_HEIGHT = 24;
/** The strip the clips stand in as bands, as in the Sound tab. */
@@ -110,6 +111,12 @@ export class VideoCanvas extends Widget {
private readonly stripTexts: Text[] = [];
private readonly hint = crispText({ text: '', style: textStyle('muted') });
private view: WaveView = { panX: FIT_MARGIN, pxPerSecond: 100 };
/** A zoom or a Fit of the time shown as a transition, as in the Sound tab (#377). */
private readonly tween = axisTween(
() => ({ scale: this.view.pxPerSecond, offset: this.view.panX }),
({ scale, offset }) => (this.view = { pxPerSecond: scale, panX: offset }),
() => this.redraw(),
);
private needsFit = true;
private assetId = '';
private gesture: Gesture | null = null;
@@ -157,6 +164,7 @@ export class VideoCanvas extends Widget {
}
saveView(): WaveView {
this.tween.land();
return { ...this.view };
}
@@ -168,7 +176,13 @@ export class VideoCanvas extends Widget {
this.redraw();
}
/** Fit by hand, shown as a transition. */
fit(): void {
this.tween.change(() => this.frame());
}
/** The whole video across. */
private frame(): void {
const asset = this.asset();
if (!asset || asset.duration <= 0 || this.w <= 0) {
this.needsFit = true;
@@ -242,6 +256,11 @@ export class VideoCanvas extends Widget {
return clip ? this.clipSpan(clip, asset) : { start: 0, end: asset.duration };
}
/** The zoom of the time as `scale`, and whether a transition is on its way (test hook). */
zoomInfo(): { scale: number; moving: boolean } {
return { scale: this.view.pxPerSecond, moving: this.tween.moving };
}
/** The middle of the strip at a second of the video, on screen (test hook). */
stripAt(seconds: number): Vec2 {
const origin = this.getGlobalPosition();
@@ -491,19 +510,21 @@ export class VideoCanvas extends Widget {
const asset = this.asset();
const p = this.toLocal(e.global);
if (!asset || asset.duration <= 0 || p.y < this.timelineTop) return;
this.view = clampWave(zoomWave(this.view, zoomFactor(e.deltaY), p.x), asset.duration, this.w);
this.needsFit = false;
this.redraw();
this.tween.change(() => {
this.view = clampWave(zoomWave(this.view, zoomFactor(e.deltaY), p.x), asset.duration, this.w);
this.needsFit = false;
this.redraw();
});
e.preventDefault();
}
override layout(): void {
if (this.needsFit) this.fit();
if (this.needsFit) this.frame();
}
override redraw(): void {
const asset = this.asset();
if (this.needsFit && asset && asset.duration > 0 && this.w > 0) return this.fit();
if (this.needsFit && asset && asset.duration > 0 && this.w > 0) return this.frame();
const c = Theme.colors;
const top = this.timelineTop;
this.bg.clear().rect(0, 0, this.w, this.h).fill(0x000000);
+4
View File
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs';
import { uploadVideo } from '../lib/projects.mjs';
import { LANES, MAKE_OBJECT, pickAssetType } from './lib/scaffold.mjs';
import { sampleZoom } from './lib/zoomTransition.mjs';
/**
* Videos (#402). A WebM goes up and the worker turns it into an MP4 (in CI beside the PHP server,
@@ -197,6 +198,9 @@ export async function video(t) {
JSON.stringify({ spanBefore, untouched, moved, undoneMove, pickedBeside }),
);
await shot('66-video-tab');
// At 1000 ms a frame after the wheel the time's zoom is on its way, as in the Sound tab (#377).
const timeZoom = await sampleZoom(t, await strip(1), '__gm.videoTab.zoom()');
check('a zoom of the time is shown on its way and lands where the jump does', timeZoom.ok, timeZoom.detail);
// 66f. A sprite wearing the clip shows only its stretch, over and over: green and blue, never red
// or white, without sound in the editor.