/* AcreliaVideo.jsx — Video corporativo de producto (versión dinámica) La plataforma es la protagonista: marco de navegador grande y persistente, cámara que recorre la UI, etiquetas ancladas, cortes y montaje. Poco texto. Sincronizado al audio de la voz en off. 1920×1080, auto-escalado. */ const { useState, useRef, useEffect, useCallback } = React; /* ----------------------------------------------------------- paleta */ const C = { blue:'#1992C9', blueLt:'#46AEDC', blueDeep:'#0F6E97', blueDark:'#0B4A66', ink:'#16242E', ink2:'#24333D', navy:'#0B1B25', navy2:'#0E2533', mist:'#EAF4FA', mist2:'#F3F9FC', white:'#FFFFFF', soft:'#5C6E78', softLt:'rgba(255,255,255,0.66)', }; const FONT = "'Manrope', system-ui, sans-serif"; const MONO = "ui-monospace, 'SF Mono', Menlo, monospace"; /* --------------------------------------------------------- easing */ const E = { outCubic: x => 1 - Math.pow(1 - x, 3), inOutCubic: x => x < 0.5 ? 4*x*x*x : 1 - Math.pow(-2*x+2, 3)/2, outExpo: x => x >= 1 ? 1 : 1 - Math.pow(2, -10*x), outQuint: x => 1 - Math.pow(1 - x, 5), outBack: x => { const c1=1.70158, c3=c1+1; return 1 + c3*Math.pow(x-1,3) + c1*Math.pow(x-1,2); }, }; const clamp = (v,a,b) => Math.max(a, Math.min(b, v)); const lerp = (a,b,p) => a + (b-a)*p; function P(lt, delay, dur, ease){ return (ease||E.outCubic)(clamp((lt-(delay||0))/(dur||0.6), 0, 1)); } function rv(lt, delay, dur, ease, fromY){ const e = P(lt, delay, dur||0.7, ease||E.outCubic); return { opacity: e, transform: `translateY(${(1-e)*(fromY==null?44:fromY)}px)` }; } /* ------------------------------------------------------- timeline */ const TL = [ { key:'intro', s:0.0, e:5.62 }, { key:'plataforma', s:5.62, e:12.12 }, { key:'usable', s:12.12, e:17.52 }, { key:'crm', s:17.52, e:32.94 }, { key:'editor', s:32.94, e:48.10 }, { key:'ia', s:48.10, e:56.08 }, { key:'imagenes', s:56.08, e:67.0 }, { key:'home', s:67.0, e:83.82 }, { key:'topbar', s:83.82, e:92.34 }, { key:'reputacion', s:92.34, e:103.4 }, { key:'idiomas', s:103.4, e:110.7 }, { key:'cierre', s:110.7, e:125.00 }, { key:'tagline', s:125.00, e:129.18 }, ]; const DUR = 129.18; const AUDIO_SRC = 'uploads/audio_video_acrelia.mp3'; const MUSIC_SRC = 'uploads/music_acrelia.mp3'; /* capturas disponibles (añade aquí cada archivo cuando lo subas a assets/). Las que no estén en el set muestran placeholder automáticamente. Para el vídeo del CRM (assets/s4-crm.mp4), poner CLIP_READY = true. */ const READY_FILES = new Set([ 'assets/s2-overview.png', 'assets/s4-crm.mp4', 'assets/s2-overview-2.png', 'assets/s2-overview-3.png', 'assets/s5-editor.png', 'assets/s5-editor.mp4', 'assets/s6-ia.mp4', 'assets/s7-imagenes.png', 'assets/s7-imagenes-2.png', 'assets/s7-imagenes-3.png', 'assets/s9-topbar.png', 'assets/s9-topbar.mp4', 'assets/s10-reputacion.png', 'assets/s10-reputacion.mp4', 'assets/s8-home.png', 'assets/s8-home.mp4', 'assets/final-2.png', 'assets/final-1.png', 'assets/final-3.png', 'assets/final-4.png', 'assets/final-5.png', 'assets/final-6.png', 'assets/s2-usable.png', 'assets/s2-usable-dark.png', ]); const has = (f) => !!f && READY_FILES.has(f); const shot = (file) => has(file) ? file : null; const MEDIA = { playing: false }; // estado compartido para el vídeo embebido const BLOB_CACHE = {}; // path -> objectURL (vídeo descargado en memoria) const vsrc = (p) => (p && BLOB_CACHE[p]) || p; const ALL_VIDEOS = ['assets/s4-crm.mp4','assets/s5-editor.mp4','assets/s6-ia.mp4','assets/s8-home.mp4','assets/s9-topbar.mp4','assets/s10-reputacion.mp4']; /* marco de navegador grande y persistente */ const FR = { x:210, y:128, w:1500, h:892, chrome:60 }; const FRC = FR.h - FR.chrome; // altura del lienzo interior /* ===================================================== primitivos */ function Chrome() { return (
manager.acrelia.com
); } function Placeholder({ title, file, video }) { return (
{video ? : }
{title}
{file}
// arrastra aquí {video ? 'el vídeo' : 'la captura'}
); } /* etiquetas apiladas (opción G): bloques sólidos pegados a un lado, acumulativos. side: 'l' (izquierda) | 'r' (derecha). top: separación desde arriba. */ function TagStack({ lt, items, side, top, gap }) { const isR = side === 'r'; let active = -1; items.forEach((it,i)=>{ if (lt >= (it.at||0)) active = i; }); return (
{items.map((it,i) => { const e = clamp(P(lt, it.at||0, 0.5, E.outBack), 0, 1); const on = i === active; return (
{it.t}
); })}
); } /* caption inferior (lower-third) sobre scrim */ function Caption({ lt, kicker, title, sub }) { return (
{kicker ? (
{kicker}
) : null}
{title}
{sub ?
{sub}
: null}
); } /* MARCO PRODUCTO — el héroe de casi todas las escenas */ function VideoSlot({ src, lt, fit='contain', fitDur }) { const ref = useRef(null); // precarga en cuanto se monta para que no entre "en frío" al cortar la escena useEffect(() => { const v = ref.current; if (!v) return; try { v.load(); } catch(e){} if (MEDIA.playing) v.play().catch(()=>{}); }, []); useEffect(() => { const v = ref.current; if (!v) return; if (MEDIA.playing) { if (v.paused) v.play().catch(()=>{}); } else v.pause(); }, [lt]); // solo corrige la sincronía si la desviación es grande (no reescribir currentTime cada frame: congela el vídeo) useEffect(() => { const v = ref.current; if (!v || !v.duration) return; if (fitDur) { // encaja el vídeo ENTERO dentro de la duración de la escena (acelera/ralentiza un poco) v.playbackRate = Math.max(0.5, Math.min(2, v.duration / fitDur)); const want = Math.min((clamp(lt, 0, fitDur) / fitDur) * v.duration, v.duration - 0.05); if (Math.abs(v.currentTime - want) > 0.7) { try { v.currentTime = want; } catch(e){} } return; } const want = lt % v.duration; if (Math.abs(v.currentTime - want) > 0.7) { try { v.currentTime = want; } catch(e){} } }, [lt]); return