Zoom the video tab's time smoothly like the other tabs (#377)
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:
1 parent
e191dc4b2e
commit
322fc39d16
4 files changed
+33
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user