// The footage's box at a moment of the cut, as umtool's preview draws it. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import test from "node:test"; import { estimateSchedule } from "umtool-report-to-video/deck"; import { backdropTransform, ease, footageAt, segmentIdAt } from "./footage-move.mjs"; const FROM = { x: 173, y: 2, width: 1574, height: 886 }; const TO = { x: 24, y: 64, width: 1354, height: 762 }; const schedule = { segments: [ { id: "a", start: 0 }, { id: "b", start: 9.5 }, { id: "c", start: 19 }, ], moves: [{ segment: "a", at: 4, segmentAt: 4, seconds: 0.5, from: FROM, to: TO }], }; test("ease: smoothstep, clamped", () => { assert.equal(ease(-1), 0); assert.equal(ease(0), 0); assert.equal(ease(0.5), 0.5); assert.equal(ease(0.25), 0.15625); assert.equal(ease(1), 1); assert.equal(ease(2), 1); assert.equal(ease(Number.NaN), 0); }); test("segmentIdAt: the last segment that has started", () => { assert.equal(segmentIdAt(schedule, 0), "a"); assert.equal(segmentIdAt(schedule, 9.49), "a"); assert.equal(segmentIdAt(schedule, 9.5), "b"); assert.equal(segmentIdAt(schedule, 100), "c"); assert.equal(segmentIdAt(schedule, -1), "a"); assert.equal(segmentIdAt({ segments: [] }, 1), null); }); test("footageAt: the box before the move, eased through it, held at `to` to the end of the segment", () => { assert.deepEqual(footageAt(schedule, 3.9).rect, FROM); assert.equal(footageAt(schedule, 3.9).progress, 0); const mid = footageAt(schedule, 4.25); assert.equal(mid.progress, 0.5); assert.deepEqual(mid.rect, { x: 98.5, y: 33, width: 1464, height: 824 }); assert.equal(footageAt(schedule, 4.125).progress, 0.15625); assert.deepEqual(footageAt(schedule, 4.5).rect, TO); assert.deepEqual(footageAt(schedule, 9.4).rect, TO); // The next segment comes in at the normal box: no move once it is on screen. assert.equal(footageAt(schedule, 9.5), null); assert.equal(footageAt(schedule, 20), null); assert.equal(footageAt({ segments: schedule.segments }, 5), null); // A zero-second move is a cut. const snap = { ...schedule, moves: [{ ...schedule.moves[0], seconds: 0 }] }; assert.equal(footageAt(snap, 3.99).progress, 0); assert.equal(footageAt(snap, 4).progress, 1); }); test("footageAt over deck.mjs's own schedule: the moves estimateSchedule emits", () => { const m = { render: { width: 1920, height: 1080, fps: 30, transition: 0.5, chrome: { engine: "hyperframes", layout: "deck", deck: {} } }, timeline: [ { type: "clip", id: "c01", video: "v", start: 0, end: 12, date: "2024-09-03" }, { type: "clip", id: "c02", video: "v", start: 20, end: 30, date: "2024-09-10" }, ], posts: [{ id: "p", platform: "x", date: "2024-09-05", text: "t", url: "https://x.com/a/status/1" }], }; const s = estimateSchedule(m); const [move] = s.moves; assert.equal(move.segment, "c01"); assert.deepEqual(footageAt(s, move.at - 0.01).rect, move.from); assert.deepEqual(footageAt(s, move.at + move.seconds).rect, move.to); // Through c01's hold, still aside; c02 is at its box. const c02 = s.segments.find((x) => x.id === "c02"); assert.deepEqual(footageAt(s, c02.start - 0.01).rect, move.to); assert.equal(footageAt(s, c02.start), null); }); test("backdropTransform: the frame scaled and moved so `from` lands on the rect", () => { const frame = { W: 1920, H: 1080 }; assert.equal(backdropTransform(FROM, FROM, frame), "none"); const tr = backdropTransform(FROM, TO, frame); // sx 1354/1574, tx 24 − sx·173 = −124.82 px = −6.501 % of 1920; ty 64 − sy·2 = 62.28 px. assert.equal(tr, "translate(-6.501%, 5.7667%) scale(0.860229, 0.860045)"); // Check the mapping itself: the box's corners land on TO's. const sx = TO.width / FROM.width; const sy = TO.height / FROM.height; const tx = (-6.501 / 100) * 1920; const ty = (5.7667 / 100) * 1080; assert.ok(Math.abs(tx + sx * FROM.x - TO.x) < 0.01); assert.ok(Math.abs(ty + sy * FROM.y - TO.y) < 0.01); assert.ok(Math.abs(tx + sx * (FROM.x + FROM.width) - (TO.x + TO.width)) < 0.01); });