Improve videos

This commit is contained in:
Andras Schmelczer 2026-06-10 21:28:19 +01:00
parent 4012e4e047
commit d3418c67cc
11 changed files with 988 additions and 869 deletions

View file

@ -101,7 +101,10 @@ async function runCue(
videoTimeMs: cursor.ms + leadInMs,
durationMs: measuredAudioMs,
});
await showCaption(ctx.page, cue.text, cue.captionPlacement);
// The spoken line is never rendered; only an explicit short caption is.
if (cue.caption) {
await showCaption(ctx.page, cue.caption, cue.captionPlacement);
}
const during = cue.during ?? [];
const declaredSum = during.reduce((s, a) => s + a.durationMs, 0);
@ -126,7 +129,9 @@ async function runCue(
cursor.ms += duringElapsed;
}
await hideCaption(ctx.page);
if (cue.caption) {
await hideCaption(ctx.page);
}
for (const step of cue.tail ?? []) {
cursor.ms += await runStep(ctx, step);
@ -185,6 +190,12 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
}
case 'click': {
const selectionVersion = ctx.dashboard.getSelectionStatsVersion();
// Hexagon targets are projected from the latest map response; make
// sure that response corresponds to the settled viewport (a zoom in
// the previous cue may still have postcode fetches in flight).
if (step.target.kind === 'hexagon') {
await ctx.dashboard.waitForApiIdle(3000);
}
const candidates =
step.target.kind === 'hexagon' && step.waitForSelectionReady
? await ctx.dashboard.visibleHexagonTargets(4)
@ -228,7 +239,7 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
const mapVersion = ctx.dashboard.getMapDataVersion();
const delta = step.direction === 'out' ? -MAP_ZOOM_WHEEL_DELTA : MAP_ZOOM_WHEEL_DELTA;
const handled = await ctx.page.evaluate(
async ({ x, y, steps, durationMs, direction }) => {
async ({ x, y, steps, durationMs, direction, center }) => {
const root = document.querySelector('.maplibregl-map') as HTMLElement | null;
const fiberKey = root
? Object.getOwnPropertyNames(root).find((key) => key.startsWith('__reactFiber$'))
@ -264,6 +275,12 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
zoom: number,
options: { around?: unknown; duration?: number; essential?: boolean }
) => void;
easeTo?: (options: {
center?: unknown;
zoom?: number;
duration?: number;
essential?: boolean;
}) => void;
};
if (
typeof mapApi.getCanvas !== 'function' ||
@ -281,7 +298,16 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
const minZoom = mapApi.getMinZoom?.() ?? 0;
const maxZoom = mapApi.getMaxZoom?.() ?? 22;
const targetZoom = Math.max(minZoom, Math.min(maxZoom, mapApi.getZoom() + zoomDelta));
mapApi.zoomTo(targetZoom, { around, duration: durationMs, essential: true });
if (center && typeof mapApi.easeTo === 'function') {
mapApi.easeTo({
center: around,
zoom: targetZoom,
duration: durationMs,
essential: true,
});
} else {
mapApi.zoomTo(targetZoom, { around, duration: durationMs, essential: true });
}
await new Promise((resolve) => window.setTimeout(resolve, durationMs));
return true;
},
@ -291,6 +317,7 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
steps: step.steps,
durationMs: step.durationMs,
direction: step.direction,
center: step.center ?? false,
}
);
@ -363,6 +390,41 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
case 'scrollPane':
await scrollPaneTo(ctx.page, step.selector, step.top);
return;
case 'dragSheet': {
const sheet = ctx.page.locator('section[class*="rounded-t-2xl"]').first();
const sheetBox = await sheet.boundingBox().catch(() => null);
if (!sheetBox) return; // desktop layout — nothing to drag
const handle = ctx.page
.locator('section[class*="rounded-t-2xl"] [class*="touch-none"]')
.first();
const handleBox = (await handle.boundingBox().catch(() => null)) ?? {
x: sheetBox.x,
y: sheetBox.y + 4,
width: sheetBox.width,
height: 20,
};
const viewport = ctx.page.viewportSize() ?? { width: 540, height: 960 };
const startX = handleBox.x + handleBox.width / 2;
const startY = handleBox.y + handleBox.height / 2;
// The sheet resizes so its top tracks the pointer (the component
// clamps to its own min/max). Aim the pointer where the handle would
// sit when the sheet covers `toHeightFrac` of the viewport.
const handleOffset = startY - sheetBox.y;
const endY = viewport.height * (1 - step.toHeightFrac) + handleOffset;
const approachMs = Math.min(260, Math.max(140, Math.round(step.durationMs * 0.25)));
const dragMs = Math.max(180, step.durationMs - approachMs - 80);
await smoothMove(ctx.page, ctx.cursor, { x: startX, y: startY }, { durationMs: approachMs });
await ctx.page.mouse.down();
await smoothMove(
ctx.page,
{ x: startX, y: startY },
{ x: startX, y: endY },
{ durationMs: dragMs, realMouse: true }
);
await ctx.page.mouse.up();
ctx.cursor = { x: startX, y: endY };
return;
}
case 'openFilterGroup':
// Click is idempotent: if the group is already expanded, the click
// would collapse it — which we don't want. Detect via aria-expanded
@ -375,6 +437,9 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
trigger.click();
}, step.selector);
return;
case 'pressKey':
await ctx.page.keyboard.press(step.key);
return;
}
}
@ -395,7 +460,9 @@ async function resolveTarget(
y: Math.round(Math.min(1, Math.max(0, target.yFrac)) * viewport.height),
};
}
const box = await ctx.page.locator(target.selector).boundingBox();
// Bounded wait: a storyboard pointing at a missing element should fail in
// seconds with a clear error, not stall for Playwright's default 30s.
const box = await ctx.page.locator(target.selector).boundingBox({ timeout: 5000 });
if (!box) throw new Error(`No bounding box for selector: ${target.selector}`);
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
}