perfect-postcode/video/src/scenes.ts

125 lines
3.8 KiB
TypeScript

import type { Page } from 'playwright';
import {
PROMPT_TEXT,
DRAG_FILTER_NAME,
DRAG_TO_FRACTION,
} from './config.js';
import {
clearVignette,
hideCaption,
showCaption,
showOutro,
} from './dom.js';
import { fakeType, sleep, smoothMove, smoothDragSliderThumb } from './motion.js';
export interface SceneCtx {
page: Page;
cursor: { x: number; y: number };
}
/** Cold open. Vignette fades; cursor parks at a "natural" rest position. */
export async function sceneColdOpen(ctx: SceneCtx): Promise<void> {
await clearVignette(ctx.page);
await ctx.page.mouse.move(ctx.cursor.x, ctx.cursor.y);
await sleep(1100);
}
/**
* AI prompt scene: click the collapsed AI box, type the prompt, submit,
* watch the (stubbed) response apply.
*/
export async function sceneAiPrompt(ctx: SceneCtx): Promise<void> {
const { page } = ctx;
await showCaption(page, 'Describe the area you want.');
const aiButton = page.locator('[data-tutorial="ai-filters"] button').first();
const btnBox = await aiButton.boundingBox();
if (!btnBox) throw new Error('AI button not found');
const target = { x: btnBox.x + btnBox.width / 2, y: btnBox.y + btnBox.height / 2 };
await smoothMove(page, ctx.cursor, target, { durationMs: 400 });
ctx.cursor = target;
await page.mouse.click(target.x, target.y);
const textarea = page.locator('[data-tutorial="ai-filters"] textarea');
await textarea.waitFor({ state: 'visible', timeout: 3000 });
await sleep(120);
const taBox = await textarea.boundingBox();
if (taBox) {
const into = { x: taBox.x + 30, y: taBox.y + taBox.height / 2 };
await smoothMove(page, ctx.cursor, into, { durationMs: 220 });
ctx.cursor = into;
}
// fakeType runs the typing animation inside the browser to avoid CDP
// round-trip overhead per keystroke (which can quadruple total typing time).
await fakeType(page, '[data-tutorial="ai-filters"] textarea', PROMPT_TEXT, 35);
await sleep(180);
await page.keyboard.press('Enter');
await sleep(700);
await hideCaption(page);
await sleep(150);
}
/**
* Slider scene: pan to a numeric filter's right thumb and drag it inward.
* The whole point: the user sees the map react in real time to a human action,
* driving home that AI sets a starting point but you stay in control.
*/
export async function sceneSliderControl(ctx: SceneCtx): Promise<void> {
const { page } = ctx;
await showCaption(page, 'You stay in control.');
const card = page.locator(`[data-filter-name="${DRAG_FILTER_NAME}"]`);
await card.waitFor({ state: 'visible', timeout: 3000 });
await card.scrollIntoViewIfNeeded();
await sleep(120);
const thumbSelector = `[data-filter-name="${DRAG_FILTER_NAME}"] [role="slider"] >> nth=1`;
const trackSelector = `[data-filter-name="${DRAG_FILTER_NAME}"] [data-orientation="horizontal"] >> nth=0`;
ctx.cursor = await smoothDragSliderThumb(
page,
thumbSelector,
trackSelector,
ctx.cursor,
DRAG_TO_FRACTION,
1100
);
await sleep(550);
await hideCaption(page);
await sleep(150);
}
/** Property reveal: click a postcode on the map to open the side pane with charts. */
export async function scenePropertyReveal(ctx: SceneCtx): Promise<void> {
const { page } = ctx;
const viewport = page.viewportSize() ?? { width: 1920, height: 1080 };
const target = {
x: 360 + (viewport.width - 360) * 0.55,
y: viewport.height * 0.5,
};
await smoothMove(page, ctx.cursor, target, { durationMs: 500 });
ctx.cursor = target;
await page.mouse.click(target.x, target.y);
await sleep(1300);
}
/** Outro: full-screen logo card with brand + URL. */
export async function sceneOutro(ctx: SceneCtx): Promise<void> {
await showOutro(
ctx.page,
'Perfect Postcodes',
'Find where you actually want to live.',
'perfectpostcodes.com'
);
await sleep(1800);
}