feat(film): high-visibility cursor, lead-in trim, and rich storm-intel template

Replace the tiny SVG pointer with a solid brand-blue disc (white border + glow + bold click pulse) visible on any background. Orchestrator measures and trims the login/nav lead-in via ffmpeg -ss. Configurable FILM_BASE_URL. storm-intel becomes the rich hero template: waits for each load, drops a pin, waits for its storm history, clicks a storm event to visualise its polygon on the map, widens the range, and holds on the payoff.
This commit is contained in:
Satyam Rastogi
2026-05-30 21:57:00 +05:30
parent 8602c70eea
commit e58428126f
4 changed files with 91 additions and 46 deletions
@@ -1,30 +1,69 @@
// Storm Intel: drop a pin, wait for the REAL storm-history fetch, reveal the panel.
// Storm Intel — HERO scene (~75s). Full walkthrough that WAITS for each load to finish:
// load map+data → drop a pin → wait for the pin's storm history → click a storm EVENT
// so it's visualised (polygon) on the map → filter by Hail → widen the date range → hold.
// This is the rich template the other scenes follow.
export default async (page, cursor, ctx) => {
// Map tiles stream in — let them settle before interacting.
await page.waitForTimeout(2500);
const dwell = (ms) => page.waitForTimeout(ms);
// Enable pin-drop mode if there's a toggle (button text "Pin" / aria), else skip.
const pinToggle = page.getByRole("button", { name: /pin/i }).first();
// 1) Wait for the map AND the storm data to actually finish loading (not a fixed sleep).
await page.locator(".leaflet-container").first().waitFor({ state: "visible", timeout: 20000 });
// The page shows "Loading storm data..." until events resolve — wait for it to clear.
await page
.getByText(/loading storm data/i)
.first()
.waitFor({ state: "hidden", timeout: 20000 })
.catch(() => {});
await page.waitForLoadState("networkidle").catch(() => {});
await dwell(1800); // open on the fully-loaded map
// 2) Enter pin-drop mode (crosshair / "drop a pin" toggle).
const pinToggle = page
.getByRole("button", { name: /pin|crosshair|drop a pin/i })
.first();
if (await pinToggle.count()) {
await cursor.clickLocator(pinToggle);
await page.waitForTimeout(400);
await dwell(600);
}
// Click a point on the map (center-ish). The map container fills the main area.
// 3) Drop a pin over central Plano and WAIT for the per-pin storm history to load.
const map = page.locator(".leaflet-container").first();
const box = await map.boundingBox();
const target = { x: box.x + box.width * 0.5, y: box.y + box.height * 0.5 };
const target = { x: box.x + box.width * 0.52, y: box.y + box.height * 0.62 };
const waitHistory = page
.waitForResponse((r) => /\/api\/storm-history/.test(r.url()), { timeout: 20000 })
.catch(() => null);
await cursor.click(target.x, target.y, 900);
await waitHistory;
// Wait for the "Loading..." in the Storm History panel to resolve into real content.
await page
.getByText(/loading/i)
.first()
.waitFor({ state: "hidden", timeout: 12000 })
.catch(() => {});
await dwell(2500); // hold on the populated history panel
// Drive the real storm-history fetch and wait for it, so the panel fill is on camera.
const waitFetch = page
.waitForResponse((r) => /\/api\/storm-history/.test(r.url()), { timeout: 15000 })
.catch(() => null); // tolerate cached/no-fetch; the pin still renders
await cursor.click(target.x, target.y, 800);
await waitFetch;
await page.waitForTimeout(2500); // hold on the populated panel
// 4) Widen the range to 12 Mo FIRST (more history) — do filters before selecting an
// event so we never filter the selected event back off the map.
const range12 = page.getByRole("button", { name: /^\s*12\s*mo\s*$/i }).first();
if (await range12.count()) {
await cursor.clickLocator(range12, 700);
await dwell(2500);
}
// Pan the cursor to the populated history panel for emphasis.
const panel = page.getByText(/storm history/i).first();
if (await panel.count()) await cursor.clickLocator(panel, 700);
await page.waitForTimeout(1500);
// 5) Click the storm EVENT card (e.g. "S. Plano — Flash Flood") so it draws on the map,
// then HOLD on it — this visualised polygon is the payoff of the scene.
// Event titles use a "Place — Type" format (em-dash); filter chips are bare words —
// so match the em-dash title (fallback: the unique "homes affected" line) to avoid
// accidentally clicking a top-of-page filter chip.
let card = page
.getByText(/[—–-]\s*(flash\s*flood|flood|hail|tornado|wind|snow)/i)
.first();
if (!(await card.count())) card = page.getByText(/homes affected/i).first();
if (await card.count()) {
await cursor.clickLocator(card, 900);
await dwell(4000); // let the polygon render + the map fly/zoom to it, then hold
}
// 6) Final hold on the populated, visualised view (event drawn on the map).
await dwell(3000);
};