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
+24 -22
View File
@@ -16,42 +16,44 @@ export function easeSteps(from, to, steps) {
// Page-side script: appends a pointer element and exposes window.__cursor*.
// pointer-events:none so it never blocks real clicks. Injected via addInitScript.
// High-visibility CSS cursor (no SVG/innerHTML): a brand-blue disc with a white border,
// a dark contrast ring, and a glow — visible on dark, light, and busy map backgrounds.
// A bold yellow ring expands on every click. Starts centered so it's never stuck at 0,0.
export const CURSOR_INIT_SCRIPT = `
(() => {
if (window.__cursorEl) return;
const el = document.createElement('div');
el.id = '__film_cursor';
Object.assign(el.style, {
position: 'fixed', left: '0', top: '0', width: '22px', height: '22px',
marginLeft: '-4px', marginTop: '-4px', zIndex: '2147483647',
pointerEvents: 'none', transition: 'transform 0.04s linear',
background: 'transparent',
const dot = document.createElement('div');
dot.id = '__film_cursor';
Object.assign(dot.style, {
position: 'fixed', left: '0', top: '0', width: '30px', height: '30px',
marginLeft: '-15px', marginTop: '-15px', borderRadius: '50%',
background: 'rgba(11,95,255,0.92)', border: '3px solid #ffffff',
boxShadow: '0 0 0 2px rgba(0,0,0,0.6), 0 0 18px 5px rgba(11,95,255,0.9)',
zIndex: '2147483647', pointerEvents: 'none', willChange: 'transform',
transition: 'transform 0.03s linear', transform: 'translate(960px,540px)',
});
el.innerHTML =
"<svg width='22' height='22' viewBox='0 0 22 22'><path d='M2 2 L2 17 L6 13 L9 20 L12 19 L9 12 L15 12 Z' fill='white' stroke='#0B5FFF' stroke-width='1.5'/></svg>";
document.documentElement.appendChild(el);
document.documentElement.appendChild(dot);
const ring = document.createElement('div');
Object.assign(ring.style, {
position: 'fixed', left: '0', top: '0', width: '34px', height: '34px',
marginLeft: '-17px', marginTop: '-17px', borderRadius: '50%',
border: '2px solid rgba(11,95,255,0.9)', zIndex: '2147483646',
pointerEvents: 'none', opacity: '0', transform: 'scale(0.4)',
transition: 'opacity 0.3s, transform 0.3s',
position: 'fixed', left: '0', top: '0', width: '30px', height: '30px',
marginLeft: '-15px', marginTop: '-15px', borderRadius: '50%',
border: '4px solid rgba(255,209,0,0.95)', boxShadow: '0 0 10px 2px rgba(255,209,0,0.6)',
zIndex: '2147483646', pointerEvents: 'none', opacity: '0',
transform: 'translate(960px,540px) scale(0.5)',
});
document.documentElement.appendChild(ring);
window.__cursorEl = el; window.__cursorRing = ring;
window.__cursorEl = dot; window.__cursorRing = ring;
window.__cursorMove = (x, y) => {
el.style.transform = 'translate(' + x + 'px,' + y + 'px)';
ring.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(0.4)';
dot.style.transform = 'translate(' + x + 'px,' + y + 'px)';
};
window.__cursorPulse = (x, y) => {
ring.style.transition = 'none';
ring.style.opacity = '0.9';
ring.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(0.4)';
ring.style.opacity = '0.95';
ring.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(0.5)';
requestAnimationFrame(() => {
ring.style.transition = 'opacity 0.4s, transform 0.4s';
ring.style.transition = 'opacity 0.55s ease-out, transform 0.55s ease-out';
ring.style.opacity = '0';
ring.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(1.4)';
ring.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(2.6)';
});
};
})();
+4 -2
View File
@@ -36,10 +36,12 @@ export async function probeSeconds(file) {
}
// Convert a recorded .webm to a constant-30fps .mp4 (h.264, yuv420p for broad support).
export async function webmToMp4(webm, mp4, fps = 30) {
// startSec trims the lead-in (login + navigation) recorded before the scene began.
export async function webmToMp4(webm, mp4, fps = 30, startSec = 0) {
await mkdir(dirname(mp4), { recursive: true });
const seek = startSec > 0 ? ["-ss", startSec.toFixed(3)] : [];
await run(ffmpegPath, [
"-y", "-i", webm,
"-y", ...seek, "-i", webm,
"-r", String(fps), "-vsync", "cfr",
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", "-preset", "medium",
"-an", mp4,