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:
@@ -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)';
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user