// 9:16 portrait composition — 1080x1920, all 5 acts redesigned for vertical
// Reuses existing Sprite/CueLayer infra. Width 1080, height 1920.
// ── Act 1 — Problema ────────────────────────────────────────────────────────
function Act1ProblemV() {
return (
);
}
function Act1VInner() {
const { localTime: t } = useSprite();
const bars = Array.from({ length: 32 }, (_, i) => 8 + Math.abs(Math.sin(i * 0.4 + t * 4)) * 80 + Math.abs(Math.sin(i * 1.7 + t * 3)) * 50);
const pulse = 0.7 + Math.sin(t * 6) * 0.3;
const chatLines = [
{ u: 'gabriela_91', m: '🔥🔥🔥', c: '#F59E0B' },
{ u: 'el_madmax', m: 'NO J*DAS', c: '#EA580C' },
{ u: 'streamfan', m: 'guarda eso pa shorts', c: '#9061f9' },
{ u: 'kevin_g', m: 'clip clip clip', c: '#EA580C' },
{ u: 'lia_88', m: 'a tiktok ya', c: '#F59E0B' },
];
const visibleChat = Math.min(5, Math.floor(t * 2.4));
// Captions alineadas con Act1 (3 captions, 5s total)
const captions = [
{ t: 0.0, end: 1.8, text: '3 horas de stream.' },
{ t: 1.9, end: 3.4, text: '0 clips publicados.' },
{ t: 3.5, end: 5.0, text: 'La competencia publica a diario.' },
];
return (
{/* Stream panel — dentro de safe-zone TikTok/Reels (top 220 reservado) */}
{bars.map((h, i) => {
const fade = Math.max(0.2, 1 - Math.abs(i - 16) / 16);
return
;
})}
{t > 3 && t < 6 && (
● momento detectado
)}
Título del directo
Probando el nuevo update · gameplay caótico
{/* Chat panel — below stream, fuera de bottom safe-zone (400 reservados) */}
Chat en directo
{chatLines.slice(0, visibleChat).map((c, i) => (
{c.u}
{c.m}
))}
{/* Kinetic captions — bottom */}
{captions.map((c, i) => {
if (t < c.t || t > c.end) return null;
const local = t - c.t, dur = c.end - c.t;
let opacity = 1, ty = 0, scale = 1;
if (local < 0.3) { const k = local/0.3; opacity = k; ty = (1-k)*40; scale = 0.9 + k*0.1; }
else if (local > dur - 0.3) { const k = (dur-local)/0.3; opacity = k; }
return (
{c.text}
);
})}
);
}
// ── Act 2 — Solución ────────────────────────────────────────────────────────
function Act2SolutionV() {
return ;
}
function Act2VInner() {
const { localTime: t } = useSprite();
// 15s total, idéntico mapeo a Act2 desktop
return (
{t < 1.5 &&
}
{t >= 1.5 && t < 4.5 &&
}
{t >= 4.5 && t < 7.5 &&
}
{t >= 7.5 && t < 11.0 &&
}
{t >= 11.0 && t < 15.0 &&
}
);
}
function BoltSvgV({ size }) {
return ;
}
function LogoRevealV({ t }) {
// 1.5s logo reveal, slogan reservado para Act 5
if (t >= 1.5) return null;
const fadeIn = Easing.easeOutCubic(clamp(t / 0.4, 0, 1));
const fadeOut = t > 1.1 ? Easing.easeInCubic(clamp((1.5 - t) / 0.4, 0, 1)) : 1;
const opacity = Math.min(fadeIn, fadeOut);
const scaleK = Easing.easeOutBack(clamp(t / 0.55, 0, 1));
const scale = 0.6 + scaleK * 0.4;
return (
);
}
function PhaseURLV({ t }) {
// 3s phase, 1 URL typed + 1 instant (paridad con desktop)
const opacity = t < 0.3 ? Easing.easeOutCubic(t / 0.3) : t > 2.6 ? Easing.easeInCubic(clamp((3.0 - t) / 0.4, 0, 1)) : 1;
const url1 = 'twitch.tv/streamer/v/2847391';
const url2 = 'youtube.com/watch?v=Hk9oG7tT4nQ';
const typed1 = t < 1.4 ? url1.slice(0, Math.min(url1.length, Math.floor(t * 26))) : url1;
const url1Active = t < 1.4;
const url1Done = t >= 1.4;
const url2In = clamp((t - 1.4) / 0.35, 0, 1);
const url2Eased = Easing.easeOutBack(url2In);
const url2Done = t >= 1.6;
const showCursor = Math.floor(t * 4) % 2 === 0;
const buttonProgress = Easing.easeOutCubic(clamp((t - 1.8) * 4, 0, 1));
let pressed = t >= 2.35;
return (
paso 1 · directo o vídeo
{/* In-product CTA — neutral, no compite con Act 5 */}
);
}
function URLRowV({ label, labelColor, typed, active, done, cursor, kind }) {
const borderColor = active ? '#EA580C' : done ? 'rgba(34,197,94,0.5)' : 'rgba(255,255,255,0.08)';
const glow = active ? '0 0 40px rgba(234,88,12,0.3)' : 'none';
const icon = kind === 'twitch'
?
: ;
return (
{label}
{icon}
{typed}{active && cursor && ▋ }
{done && (
)}
);
}
function PhaseAnalysisV({ t }) {
// 3s phase
const opacity = t < 0.3 ? Easing.easeOutCubic(t / 0.3) : t > 2.5 ? Easing.easeInCubic(clamp((3.0 - t) / 0.5, 0, 1)) : 1;
const bars = Array.from({ length: 36 }, (_, i) => 10 + Math.abs(Math.sin(i*0.4 + t*3))*60 + Math.abs(Math.sin(i*1.3 + t*2))*40);
const markers = [
{ pos: 8, label: 'grito', appear: 0.6 },
{ pos: 16, label: 'reacción', appear: 1.2 },
{ pos: 24, label: 'giro', appear: 1.8 },
{ pos: 30, label: 'climax', appear: 2.4 },
];
return (
paso 2 · IA analiza
{bars.map((h, i) => {
const isNear = markers.some(m => Math.abs(m.pos - i) < 2 && t > m.appear);
return
;
})}
{markers.map((m, i) => {
if (t < m.appear) return null;
return (
);
})}
analizando · 03:24:18
● {markers.filter(m => t > m.appear).length} momentos
);
}
function PhaseClipV({ t }) {
// 3.5s phase
const opacity = t < 0.3 ? Easing.easeOutCubic(t / 0.3) : t > 3.0 ? Easing.easeInCubic(clamp((3.5 - t) / 0.5, 0, 1)) : 1;
const subProgress = Math.min(1, Math.max(0, (t - 0.5) / 0.5));
const cap = Math.min(2, Math.floor(Math.max(0, t - 0.6) / 0.55));
const captions = ['¡NO J*DAS!', 'NO ME LO CREO', 'ESTO ES BRUTAL'];
return (
paso 3 · clip vertical listo
9:16 · 28s
renderizado
{subProgress > 0 && (
{captions[cap]}
)}
{[0, 1, 2].map(i => (
))}
);
}
function ClipFeatureV({ delay, t, icon, label }) {
const local = t - delay;
const slide = Math.max(0, Math.min(1, local * 4));
const ease = 1 - Math.pow(1-slide, 3);
return (
);
}
function PhasePublishV({ t }) {
// 4s phase (Adapt+Publish fusionado para paridad con desktop)
const opacity = t < 0.3 ? Easing.easeOutCubic(t / 0.3) : t > 3.5 ? Easing.easeInCubic(clamp((4.0 - t) / 0.5, 0, 1)) : 1;
const headlineProgress = Math.max(0, Math.min(1, t / 0.5));
const logos = [
{ name: 'YouTube', color: '#FF0033', delay: 0.0 },
{ name: 'TikTok', color: '#fff', delay: 0.18 },
{ name: 'Instagram', color: '#E1306C', delay: 0.36 },
];
const showTs = t > 1.0;
return (
paso 4 · adapta + publica
publica mientrasduermes .
{showTs && (
· 03:47 a.m. ·
)}
{/* Subtítulo: cada red, su versión */}
Cada clip optimizado para su red.
{logos.map((l, i) => {
const local = t - l.delay;
const ease = Easing.easeOutBack(clamp(local * 3, 0, 1));
const done = t > l.delay + 0.6;
let icon;
if (l.name === 'YouTube') {
icon =
;
} else if (l.name === 'TikTok') {
icon =
;
} else {
icon = (
);
}
return (
);
})}
);
}
// ── Act 3 — IA aprende ──────────────────────────────────────────────────────
function Act3AIV() {
return ;
}
function Act3VInner() {
const { localTime: t } = useSprite();
return (
{/* Multi-idioma chip — 2s breve, no persistente */}
{t < 2.0 && (
1.7 ? Easing.easeInCubic(clamp((2.0 - t) / 0.3, 0, 1)) : 1 }}>
🌐 español · català · english
)}
{t < 1.5 &&
1.0 ? Easing.easeInCubic(clamp((1.4-t)/0.4, 0, 1)) : 1 }}>
diferenciador · IA personal
}
{t >= 1.0 && t < 4.5 &&
}
{t >= 4.5 && t < 7.0 &&
}
{t >= 7.0 && t < 12.0 &&
}
);
}
function LearningCurveV({ t }) {
const opacity = t < 0.3 ? t/0.3 : t > 4.0 ? Math.max(0, (4.5-t)/0.5) : 1;
const points = 20;
// Faster: complete by t=2.0
const drawProgress = Math.min(1, t / 2.0);
const xs = Array.from({ length: points }, (_, i) => 60 + (i/(points-1))*840);
const ys = Array.from({ length: points }, (_, i) => { const x = i/(points-1); return 320 - Math.pow(x,0.5)*240 + Math.sin(i*1.7)*10; });
// Smooth continuous draw with partial last segment
const totalSeg = points - 1;
const segF = drawProgress * totalSeg;
const fullSeg = Math.floor(segF);
const frac = segF - fullSeg;
let path = '';
let headX = xs[0], headY = ys[0];
if (fullSeg < 1 && frac > 0) {
headX = xs[0] + (xs[1]-xs[0])*frac;
headY = ys[0] + (ys[1]-ys[0])*frac;
path = `M${xs[0]} ${ys[0]} L${headX} ${headY}`;
} else {
for (let i = 0; i <= fullSeg; i++) { path += (i===0?'M':'L') + xs[i] + ' ' + ys[i] + ' '; headX = xs[i]; headY = ys[i]; }
if (fullSeg < totalSeg && frac > 0) {
const nx = xs[fullSeg] + (xs[fullSeg+1]-xs[fullSeg])*frac;
const ny = ys[fullSeg] + (ys[fullSeg+1]-ys[fullSeg])*frac;
path += `L${nx} ${ny}`; headX = nx; headY = ny;
}
}
// Sin claims numéricos: solo curva visual relativa
return (
Curva de aprendizaje
Cada clip que publicas,la IA mejora .
{[0,1,2,3,4].map(i => )}
{drawProgress > 0.02 && }
{drawProgress > 0.02 && }
engagement
primer clip
tras meses
);
}
function ABSubsV({ t }) {
const opacity = t < 0.3 ? t/0.3 : t > 5.0 ? Math.max(0, (5.5-t)/0.5) : 1;
// Barras relativas (sin cifras absolutas).
const b1 = Easing.easeOutCubic(clamp(t/1.5, 0, 1)) * 0.32;
const b2 = Easing.easeOutCubic(clamp(t/2.0, 0, 1)) * 0.92;
const winnerVisible = t > 2.5;
return (
🔥 Estrategia dual YouTube + TikTok
);
}
function ABCardV({ label, sub, barW, winner, bg, cleanClip }) {
return (
{Array.from({ length: 14 }).map((_, i) =>
)}
{!cleanClip ? (
¿QUÉ PASÓ?!
) : (
que pasó después...
)}
{label}
{winner &&
WINNER
}
{/* Barra relativa de engagement — sin claims numéricos */}
{sub}
);
}
function HookABV({ t }) {
const opacity = t < 0.3 ? t/0.3 : t > 4.0 ? Math.max(0, (4.5-t)/0.5) : 1;
const showV2 = t > 1.0;
const showResult = t > 2.5;
return (
hooks · qué titular gana
{showV2 &&
}
{showResult &&
💡
Y lo replica automáticamente.
}
);
}
function HookRowV({ version, text, barW, winner }) {
return (
{version}
"{text}"
{winner ? '✅' : '❌'}
);
}
// ── Act 4 — Resultados ──────────────────────────────────────────────────────
function Act4ResultsV() {
return ;
}
function Act4VInner() {
const { localTime: t } = useSprite();
return (
9 ? Easing.easeInCubic(clamp((9.5-t)/0.5, 0, 1)) : 1 }}>
tres formas de usar Chispa
Un mismo motor IA. Tres formatos .
{t >= 1.4 && t < 9.5 &&
}
{/* Act 4: no cue layer — visuals carry the message */}
);
}
function BigStatsV({ t }) {
const opacity = t < 0.3 ? t/0.3 : t > 4.5 ? Math.max(0, (5.0-t)/0.5) : 1;
const easeT = Math.min(1, t/2.5);
const easeO = 1 - Math.pow(1-easeT, 3);
const clips = (23.4*easeO).toFixed(1);
const views = (1.2*easeO).toFixed(1);
const tikTok = (4.7*easeO).toFixed(1);
return (
clips · 7 días
{clips}M
generados desde directos
);
}
function TickerCardV({ label, value, suffix, delta, highlight }) {
return (
{label}
{value}{suffix}
↑ {delta}
);
}
function UseCasesV({ t }) {
const opacity = t < 0.3 ? Easing.easeOutCubic(t / 0.3) : t > 8.5 ? Easing.easeInCubic(clamp((9.0 - t) / 0.5, 0, 1)) : 1;
const cases = [
{ tag: '01', label: 'Clips personalizados', meta: 'el día a día', desc: 'IA entrenada en TU canal. Gaming, deportes, podcast, comedy, lifestyle.', delay: 0.2 },
{ tag: '02', label: 'Recortar bruto', meta: 'montaje · sin silencios', desc: 'Elimina silencios, voz de producción y tomas falladas. Pieza lista para entregar.', delay: 0.7 },
{ tag: '03', label: 'Highlights del directo', meta: '3 h → 5 min', desc: 'Detecta los picos de un stream o podcast y los condensa sin perder el hilo.', delay: 1.2, highlight: true },
];
return (
{cases.map((c, i) => {
const local = t - c.delay;
const slide = Easing.easeOutCubic(clamp(local * 2.5, 0, 1));
return (
{c.tag}
{c.meta}
{c.label}
{c.desc}
);
})}
);
}
// ── Act 5 — CTA ─────────────────────────────────────────────────────────────
function Act5CTAV() {
return ;
}
function Act5VInner() {
const { localTime: t } = useSprite();
const pulseScale = 1 + Math.sin(t*2)*0.02;
return (
{/* Logo top=260 (fuera de top safe-zone 220px) */}
{t < 11 && (
)}
{/* Headline */}
{t > 0.4 && t < 11 && (
Genera tu primera chispa.
5 clips gratis. Sin tarjeta.
)}
{/* Pricing (PricingColV usa top=800) */}
{t >= 1.5 && t < 11 &&
}
{/* Closing tagline + compliance, fuera de bottom safe-zone 400px */}
{t >= 5.5 && t < 11 && (
Tu IA, entrenada con tu contenido .
RGPD · ENS-BÁSICA · OWASP ASVS L2
)}
{/* CTA button — bottom=460 (justo por encima del bottom safe-zone) */}
{t >= 3.0 && t < 11 &&
}
{/* Domain — bottom=240 (dentro de bottom safe-zone donde TikTok superpone description: lo subimos arriba) */}
{/* Eliminado: el wordmark Chispa de top ya transmite la marca. */}
);
}
function PricingColV({ t }) {
const tiers = [
{ name: 'Free', price: '€0', clips: '5 clips/mes', delay: 0.0, accent: 'rgba(255,255,255,0.5)' },
{ name: 'Creator', price: '€29', clips: '50 clips · YT + TT + IG', delay: 0.2, accent: '#F59E0B' },
{ name: 'Pro', price: '€79', clips: '200 clips · IA entrenada', delay: 0.4, accent: '#9061f9', highlight: true },
{ name: 'Studio', price: '€199', clips: 'ilimitado · editorial', delay: 0.6, accent: '#fff' },
];
const proPulse = 0.5 + Math.sin(t*3)*0.5;
return (
{tiers.map((tier, i) => {
const local = t - tier.delay;
if (local < 0) return null;
const slide = Math.min(1, local*3);
const ease = 1 - Math.pow(1-slide, 3);
return (
);
})}
);
}
function CTAButtonV({ t }) {
const opacity = Math.min(1, t/0.4);
const pulse = 1 + Math.sin(t*4)*0.015;
const glow = 0.5 + Math.sin(t*4)*0.5;
return (
);
}
window.Act1ProblemV = Act1ProblemV;
window.Act2SolutionV = Act2SolutionV;
window.Act3AIV = Act3AIV;
window.Act4ResultsV = Act4ResultsV;
// ── Act 3 PRO+ phase (vertical) ─────────────────────────────────────────────
function TrainYourAIV({ t }) {
// 5s phase (paridad con desktop)
const opacity = t < 0.4 ? Easing.easeOutCubic(t / 0.4) : t > 4.3 ? Easing.easeInCubic(clamp((4.8 - t) / 0.5, 0, 1)) : 1;
const proPulse = 0.85 + Easing.easeInOutSine((Math.sin(t * 3) + 1) / 2) * 0.15;
const traits = [
{ icon: '🎙️', label: 'Tu voz', desc: 'humor, ritmo, muletillas' },
{ icon: '👥', label: 'Tu audiencia', desc: 'qué responden tus fans' },
{ icon: '🚀', label: 'Lo que triunfa', desc: 'replica los hooks que ganan' },
];
return (
Una IA que conoce tu canal .
Entrenada con tus vídeos, tus clips ganadores y la reacción real de tu audiencia.
{traits.map((tr, i) => {
const delay = 0.3 + i*0.16;
const local = t - delay;
const slide = Math.max(0, Math.min(1, local*3));
const ease = 1 - Math.pow(1-slide, 3);
return (
);
})}
);
}
window.Act5CTAV = Act5CTAV;