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 { 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 { 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 { 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 { 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 { await showOutro( ctx.page, 'Perfect Postcodes', 'Find where you actually want to live.', 'perfectpostcodes.com' ); await sleep(1800); }