/* global React */ /* * PortariaScene — wide cinematic SVG scene driven by scroll progress (0..1). * * Phases: * P0 0.00–0.10 establish — calm portaria, gate down, doorman idle * P1 0.10–0.32 chaos — cars arrive and stack up * P2 0.32–0.42 peak — doorman overwhelmed * P3 0.42–0.50 reset — cars fade away * P4 0.50–0.60 Luna enters from above * P5 0.60–0.72 Luna installs camera on the pole * P6 0.72–0.80 system online — beam connects camera ↔ booth * P7 0.80–1.00 solved — cars return; smooth flow, scans, decisions (loops forever) */ const { useState, useEffect, useRef } = React; const clamp = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v)); const lerp = (a, b, t) => a + (b - a) * t; const smooth = (t) => t * t * (3 - 2 * t); const easeOut = (t) => 1 - Math.pow(1 - t, 3); const easeIn = (t) => t * t * t; const rng = (p, a, b) => clamp((p - a) / (b - a), 0, 1); /* ─────────────────── Car (sedan side-view, facing LEFT) ─────────────────── * Local coords: width 200 × height ~80 (body 10..60, wheels to 78) * Origin (0,0) = top-left bounding box. Front bumper is on the LEFT. */ function Car({ x, y = 600, color = "#cfd6e6", opacity = 1, scanned = false, plate = null, decision = null }) { return ( {/* shadow under car */} {/* body silhouette: front-left hood → windshield → roof → rear window → trunk → rear bumper */} {/* lower-body shading */} {/* windows (windshield + rear) */} {/* B-pillar separating windshield from rear window */} {/* roof highlight (subtle gloss) */} {/* door line */} {/* door handle */} {/* wheels (with arch shadows) */} {/* front headlight (LEFT side — car faces left) */} {/* front plate */} {/* tail light (right) */} {/* ───── Scan overlay (Luna's identifier) ───── */} {scanned && ( {/* dashed framing */} {/* corner brackets (legs 18px each) */} {[ { x: 0, y: 2, dx: 1, dy: 1 }, { x: 200, y: 2, dx: -1, dy: 1 }, { x: 0, y: 78, dx: 1, dy: -1 }, { x: 200, y: 78, dx: -1, dy: -1 }, ].map((c, i) => ( ))} {/* center crosshair on the windshield */} {/* connector dot above */} {/* PLATE readout — left */} {plate && ( PLACA {plate} )} {/* STATUS / decision — right */} {decision && (() => { const ok = decision === 'AUTORIZADO'; const visit = decision === 'VISITANTE'; const stroke = ok ? '#5fd29a' : visit ? '#f4c34d' : '#E74C3C'; const tint = ok ? 'rgba(95,210,154,0.12)' : visit ? 'rgba(244,195,77,0.12)' : 'rgba(231,76,60,0.14)'; return ( STATUS {decision} ); })()} )} ); } /* ─────────────────── Guard booth ─────────────────── */ function GuardBooth({ doormanState = 'calm', linked = false }) { return ( {/* booth shadow on ground */} {/* booth body */} {/* roof */} {/* window frame */} {/* horizontal window slats */} {/* inside glow */} {/* doorman head & shoulders */} {/* shoulders / uniform */} {/* tie / collar */} {/* head */} {/* hair */} {/* cap brim */} {/* eyes — vary with state */} {/* mouth */} {/* sweat for stressed */} {doormanState === 'stressed' && ( )} {/* link indicator above booth */} {linked && ( )} ); } function Eyes({ state }) { if (state === 'stressed') { return ( ); } if (state === 'focused') { return ( ); } if (state === 'happy') { return ( ); } return ( ); } function Mouth({ state }) { if (state === 'stressed') { return ; } if (state === 'focused') { return ; } if (state === 'happy') { return ; } return ; } /* ─────────────────── Gate (cancela) ─────────────────── */ function Cancela({ openness = 0 }) { const angle = -openness * 78; // 0 = horizontal, -78 = up return ( {/* base/post */} {/* pivot */} {/* arm */} {[20, 60, 100, 140, 180].map((px) => ( ))} {/* small light on post */} 0.1 ? '#5fd29a' : '#E74C3C'}> ); } /* ─────────────────── Camera on pole ─────────────────── */ function CameraPole({ progress = 0, beamOn = false }) { // progress 0..1 — 0 nothing, 0.4 pole appears, 0.7 camera appears, 1 fully connected const poleH = lerp(0, 360, smooth(clamp(progress / 0.5, 0, 1))); const camOpacity = clamp((progress - 0.45) / 0.25, 0, 1); const ringOpacity = clamp((progress - 0.7) / 0.3, 0, 1); return ( {/* base */} {progress > 0 && ( )} {/* pole */} {/* horizontal arm */} {camOpacity > 0 && ( )} {/* camera head */} {camOpacity > 0 && ( {beamOn && } {/* status LED */} )} {/* scan ring emanating */} {beamOn && ( )} ); } /* ─────────────────── Background skyline + grid ─────────────────── */ function SceneBackground() { return ( {/* sky */} {/* distant building silhouettes */} {/* tiny windows on buildings */} {Array.from({ length: 60 }).map((_, i) => { const x = 810 + (i * 13) % 780; const y = 440 + Math.floor(i / 20) * 30 + ((i * 7) % 3) * 10; return ; })} {/* horizon glow */} {/* ground */} {/* ground edge line */} {/* condo boundary wall on far left */} {/* wall texture - horizontal bricks */} {Array.from({ length: 8 }).map((_, i) => ( ))} {/* small windows */} {/* driveway markings */} {Array.from({ length: 12 }).map((_, i) => ( ))} ); } /* ─────────────────── Mini Luna for the scene ─────────────────── */ function SceneLuna({ x, y, size = 140, state = 'happy', floating = true }) { return ( {floating && ( )} {floating && ( )} {/* simplified Luna inline */} {/* ears */} {/* head */} {/* eyes */} {/* happy mouth */} {state === 'happy' && } {state === 'idle' && } ); } /* ─────────────────── Connection beam: camera ↔ booth ─────────────────── */ function ConnectionBeam({ strength = 0, poleTop = 340 }) { // beam from camera (~257, poleTop) curving to top of booth (~120, 488) if (strength <= 0) return null; const camX = 257, camY = poleTop; const bX = 120, bY = 490; const ctrlX = (camX + bX) / 2, ctrlY = Math.min(camY, bY) - 60; const d = `M${camX} ${camY} Q${ctrlX} ${ctrlY} ${bX} ${bY}`; return ( {/* small data packets */} ); } /* ─────────────────── Main scene ─────────────────── */ function PortariaScene({ progress = 0 }) { const p = progress; // ─── Infinite loop timer for solved-phase cars ─── // One full car cycle = 0.20 progress units × 26 000 ms = 5 200 ms. // We start the RAF timer the first time p crosses 0.78 and keep it running // forever, so the cars never stop even when the timeline pauses at 1.0. const CYCLE_MS = 5200; const [loopT, setLoopT] = useState(0); const timerRef = useRef({ started: false, startAt: 0 }); const inSolved = p >= 0.78; useEffect(() => { if (!inSolved) { timerRef.current.started = false; return; } if (!timerRef.current.started) { timerRef.current.started = true; const initialT = ((p - 0.78) * 5) % 1; timerRef.current.startAt = performance.now() - initialT * CYCLE_MS; } let active = true; let rafId; const tick = (now) => { if (!active) return; setLoopT(((now - timerRef.current.startAt) % CYCLE_MS) / CYCLE_MS); rafId = requestAnimationFrame(tick); }; rafId = requestAnimationFrame(tick); return () => { active = false; cancelAnimationFrame(rafId); }; }, [inSolved]); // eslint-disable-line react-hooks/exhaustive-deps // ─── Doorman state by phase ─── let doormanState = 'calm'; if (p > 0.18 && p < 0.45) doormanState = 'stressed'; else if (p > 0.50 && p < 0.72) doormanState = 'calm'; else if (p >= 0.72) doormanState = 'focused'; if (p > 0.95) doormanState = 'happy'; // ─── Gate openness ─── // closed by default; in solved phase, the gate opens for each passing car let gateOpen = 0; // In P7, oscillate based on the lead car's position // We'll compute this inline below based on solved cars // ─── Cars: define a set with spawn + queue position ─── // Chaos phase queue (cars come in but don't pass) const chaosTargets = [320, 540, 760, 980, 1200]; // x of car LEFT edge when stopped const chaosCars = chaosTargets.map((target, i) => { const spawn = 0.10 + i * 0.045; const stopBy = spawn + 0.07; const t = smooth(rng(p, spawn, stopBy)); let x = lerp(1700, target, t); // fade-out in P3 let opacity = 1; if (p > 0.42) { const t2 = rng(p, 0.42, 0.50); x = lerp(x, x + 60, smooth(t2)); opacity = 1 - smooth(t2); } if (p > 0.50) opacity = 0; return { x, y: 600, color: ['#cf3c3c', '#d9c265', '#8da7c4', '#5b6b85', '#a06f4a'][i], opacity }; }); // Solved phase — a slow loop of 3 cars passing through // Use the portion of progress in [0.80, 1.0] to drive a continuous loop const solvedActive = clamp((p - 0.78) / 0.22, 0, 1); // loopT now comes from the internal RAF timer (see top of component) so it // keeps cycling even after progress freezes at 1.0. // Three cars staggered by 1/3 of loop const solvedCarDefs = [ { color: '#2E75B6', plate: 'BRA·2A18', decision: 'AUTORIZADO', offset: 0.0 }, { color: '#d9c265', plate: 'JLU·5C92', decision: 'AUTORIZADO', offset: 0.33 }, { color: '#7c4dff', plate: 'XKR·9F31', decision: 'VISITANTE', offset: 0.66 }, ]; // Solved cars: each follows the same loop but offset. They appear from x=1700, slow at the gate (~x=320) for scan, then accelerate through to x=-300. const solvedCars = solvedCarDefs.map((def, i) => { let lt = (loopT + def.offset) % 1; // 0.00–0.55: drive in from 1700 to 320 (easeOut) // 0.55–0.70: scan dwell at 320 // 0.70–1.00: drive through to -300 (easeIn) let x; let scanning = false; let showDecision = null; if (lt < 0.55) { const t = lt / 0.55; x = lerp(1700, 320, easeOut(t)); } else if (lt < 0.70) { x = 320; scanning = true; showDecision = def.decision; } else { const t = (lt - 0.70) / 0.30; x = lerp(320, -300, easeIn(t)); scanning = lt < 0.78; // keep brackets briefly as it leaves showDecision = lt < 0.78 ? def.decision : null; } return { x, y: 600, color: def.color, opacity: solvedActive * (x > -250 && x < 1700 ? 1 : 0), scanned: scanning, plate: def.plate, decision: showDecision, }; }); // Compute gate openness based on whether any solved car is in the gate zone if (p > 0.78) { // open gate when a car is near 320 or passing through let maxOpen = 0; solvedCars.forEach((c) => { // open when car between x=400 and x=-100 if (c.x < 450 && c.x > -250 && c.opacity > 0) { // scale: smooth opening as car approaches scan, full open while passing let openness = 0; if (c.x > 320) openness = 1 - (c.x - 320) / 130; // approaching else if (c.x > -100) openness = 1; // passing else openness = 1 + (c.x + 100) / 150; // closing after openness = clamp(openness, 0, 1); if (openness > maxOpen) maxOpen = openness; } }); gateOpen = maxOpen; } // ─── Luna in scene ─── // P4 (0.50-0.60): Luna flies in from top to position above pole // P5 (0.60-0.72): Luna stays near pole, "installs" the camera // P6 (0.72-0.80): Luna drifts to upper-right (overseeing) let lunaX = null, lunaY = null, lunaSize = 0, lunaState = 'happy'; if (p > 0.48 && p < 0.85) { const enterT = smooth(rng(p, 0.48, 0.60)); // P4 enter: from above const startX = 800, startY = -200; const installX = 250, installY = 240; const overseeX = 1280, overseeY = 80; lunaSize = lerp(80, 160, enterT); if (p < 0.72) { lunaX = lerp(startX, installX, enterT); lunaY = lerp(startY, installY, enterT); lunaState = p < 0.62 ? 'idle' : 'happy'; } else { const moveT = smooth(rng(p, 0.72, 0.82)); lunaX = lerp(installX, overseeX, moveT); lunaY = lerp(installY, overseeY, moveT); lunaSize = lerp(160, 100, moveT); lunaState = 'happy'; } // exit fade (after 0.85) } if (p >= 0.85) { // Luna stays in upper right but smaller lunaX = 1280; lunaY = 80; lunaSize = 100; lunaState = 'happy'; } // ─── Camera install progress: P5 ─── // The pole and camera grow during P5 (0.55..0.72) — but actually we want them to appear AS Luna installs const cameraProgress = clamp((p - 0.58) / 0.18, 0, 1); const beamOn = p > 0.72; const beamStrength = clamp((p - 0.72) / 0.08, 0, 1); const poleTop = 720 - lerp(0, 360, smooth(cameraProgress)); // ─── Scene camera transform (subtle zoom/pan) ─── // Slight zoom-in during chaos (peak 0.42), pull back during install (0.65), zoom in for solved (0.9) let zoom = 1, panY = 0; if (p < 0.42) { zoom = lerp(1.0, 1.08, smooth(rng(p, 0.10, 0.42))); } else if (p < 0.60) { zoom = lerp(1.08, 1.0, smooth(rng(p, 0.42, 0.60))); } else if (p < 0.78) { zoom = lerp(1.0, 0.96, smooth(rng(p, 0.60, 0.78))); panY = lerp(0, -20, smooth(rng(p, 0.60, 0.78))); } else { zoom = lerp(0.96, 1.04, smooth(rng(p, 0.78, 1.0))); panY = lerp(-20, 0, smooth(rng(p, 0.78, 1.0))); } const sceneTransform = `translate(0, ${panY}) scale(${zoom})`; // ─── Chaos "alert" overlay (speed lines + red vignette) ─── const chaosIntensity = clamp((p - 0.20) / 0.18, 0, 1) * (1 - clamp((p - 0.42) / 0.08, 0, 1)); return ( {/* chaos cars (only opacity > 0) */} {chaosCars.map((c, i) => c.opacity > 0.01 && ( ))} {/* solved cars */} {solvedCars.map((c, i) => c.opacity > 0.01 && ( ))} {/* cancela (gate) */} {/* camera pole */} {/* booth */} {/* connection beam between camera and booth */} {/* Luna in scene */} {lunaX !== null && lunaSize > 0 && ( )} {/* Luna installing — small "tool" beam between Luna and the pole top during P5 */} {p > 0.60 && p < 0.72 && lunaX !== null && ( )} {/* chaos overlay: motion lines and red vignette */} {chaosIntensity > 0.05 && ( {/* motion lines on cars */} {chaosCars.map((c, i) => c.opacity > 0.5 && ( ))} {/* red vignette */} {/* honk badges */} {chaosIntensity > 0.6 && ( ! !? !! )} )} {/* solved overlay: success glow */} {solvedActive > 0.2 && ( )} {/* cinematic letterboxing (fades in during chaos peak + solved) */} ); } window.PortariaScene = PortariaScene;