/* global React, Sprite, useSprite, useTime, Easing, interpolate, animate, clamp, ScrollStage, CursorTrail, CanvasChrome, SidePanel, useIsMobile */ // Bataj Products Scroll-Reel — the products section as one big scroll-driven // video. Replaces ProductsIntro + AllProducts + Stats + Process. // // Story arc (60 units of time mapped to ~750vh of scroll): // 00–03 Intro: "PRODUCTOS · Cuatro universos." // 03–12 Turnos: warm calendar booking, slot ignites, confirm pulses // 12–21 Sak: editorial luxury catalog, cards reveal, price glows // 21–30 Delivery: dark console, orders flow through pipeline // 30–39 Dunamis: dashboard, KPIs tick up, sparklines draw, alerts pop // 39–46 Stats: 4 monumental numbers count up // 46–53 Process: 01→04 BRIEF/SISTEMA/BUILD/LAUNCH march in // 53–60 Outro: "Mismo estudio. Cuatro lenguajes." + CTA wink const _cl = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); /* ───────────── Small reusable helpers ───────────── */ // progress 0 → 1 across [from, to] within sprite-local time function ramp(localTime, from, to, ease = Easing.easeOutCubic) { return ease(_cl((localTime - from) / Math.max(0.0001, to - from), 0, 1)); } // integer count tween function counted(localTime, from, to, target, fmt = (v) => v) { const t = ramp(localTime, from, to, Easing.easeOutCubic); return fmt(target * t); } function TrafficDots({ colors = ['#d0c8b8', '#d0c8b8', '#d0c8b8'] }) { return (
{colors.map((c, i) => ( ))}
); } /* ───────────── Window chrome shell ───────────── */ function WindowFrame({ x, y, w, h, bar, barBg, lineColor, bg, children, scaleAtEntry = 0 }) { // scaleAtEntry: 0 = no entry animation, otherwise it's an entrance progress 0->1 const scale = 0.94 + 0.06 * scaleAtEntry; const opacity = _cl(scaleAtEntry / 0.6, 0, 1); return (
{bar}
{children}
); } /* ═══════════════════════════════════════════════════════════════════════ SCENE 0 — INTRO (0 → 3) ═══════════════════════════════════════════════════════════════════════ */ function SceneIntro() { const isMobile = useIsMobile(); const { localTime } = useSprite(); const a1 = ramp(localTime, 0.1, 0.9, Easing.easeOutBack); const a2 = ramp(localTime, 0.6, 1.4); const a3 = ramp(localTime, 1.4, 2.2); const out = 1 - ramp(localTime, 2.4, 3.0); return (
▸ Sec.03 · Catálogo · 04 productos
PRO_DUCTOS
Cuatro universos.
01 · TURNOS
02 · SAK
03 · DELIVERY
04 · DUNAMIS
); } /* ═══════════════════════════════════════════════════════════════════════ SCENE 1 — APP TURNOS (3 → 12) ═══════════════════════════════════════════════════════════════════════ */ function SceneTurnosLive() { const isMobile = useIsMobile(); const { localTime, duration } = useSprite(); const entry = ramp(localTime, 0, 1.0); const exit = ramp(localTime, duration - 0.8, duration, Easing.easeInCubic); const opacity = entry * (1 - exit); // animation phases const headerIn = ramp(localTime, 0.3, 1.1); const weekIn = ramp(localTime, 0.9, 1.6); const slotsIn = ramp(localTime, 1.5, 2.4); const selectIn = ramp(localTime, 2.6, 3.4, Easing.easeOutBack); const ctaPulse = Math.sin(_cl((localTime - 3.7) * 4, 0, Math.PI * 2)) * 0.5 + 0.5; const toastIn = ramp(localTime, 4.4, 5.0, Easing.easeOutBack); const toastOut = ramp(localTime, 7.0, 7.6); // cursor keyframes (absolute canvas coordinates) const cursorKf = [ { t: 0.6, x: 1750, y: 850 }, // off-canvas right { t: 1.4, x: 760, y: 540 }, // hover Lun 27 { t: 2.2, x: 760, y: 540 }, // hold { t: 3.1, x: 660, y: 800, click: true }, // click 15:00 { t: 3.8, x: 660, y: 800 }, { t: 4.4, x: 1070, y: 920, click: true }, // click Confirmar { t: 5.5, x: 1070, y: 920 }, { t: 7.0, x: 1300, y: 980 }, ]; const slots = ['09:00','09:30','10:00','10:30','11:00','11:30','12:00','14:00','14:30','15:00','15:30','16:00']; const taken = new Set(['10:00','11:30','14:30']); return (
app.turnos.studio / reservar } > {/* Sidebar */}
A
Aurelia Reyes
Psicología clínica
{['Agenda','Pacientes','Notas','Cobros','Historial'].map((x,i) => (
{x}
))} {/* upcoming card */}
Próximo · Confirmado
Mariana López
15:00 · Sesión seguimiento
{/* Calendar area */}
Lunes, 27 de mayo
Reservá tu sesión
{/* Week strip */}
{['Lun 27','Mar 28','Mié 29','Jue 30','Vie 31','Sáb 1','Dom 2'].map((d, i) => { const a = ramp(localTime, 0.9 + i * 0.04, 1.4 + i * 0.04, Easing.easeOutBack); const isSelected = i === 0; return (
{d.split(' ')[0]}
{d.split(' ')[1]}
); })}
{/* Slots */}
Horarios disponibles
{slots.map((s, i) => { const a = ramp(localTime, 1.5 + (i * 0.05), 2.1 + (i * 0.05), Easing.easeOutCubic); const isTaken = taken.has(s); const isSel = s === '15:00'; const selRamp = isSel ? selectIn : 0; return (
{s}
); })}
{/* Footer row + CTA */}
Sesión 50 min · ARS 18.000
Confirmar reserva →
{/* Toast confirmation */} {toastIn > 0 && toastOut < 1 && (
Reserva confirmada
Lun 27 · 15:00 · Te llegará un email
)}
); } function SceneSakLive() { const isMobile = useIsMobile(); const { localTime, duration } = useSprite(); const entry = ramp(localTime, 0, 1.0); const exit = ramp(localTime, duration - 0.8, duration, Easing.easeInCubic); const opacity = entry * (1 - exit); const items = [ { n: 'Lino Crudo', p: 'AR$ 89.000', tag: 'COL/26', img: 'linear-gradient(160deg,#E4D8BD,#C8B58A)' }, { n: 'Vestido Aria', p: 'AR$ 142.000', tag: 'NEW', img: 'linear-gradient(160deg,#D7C2A1,#9A7A4B)' }, { n: 'Camisa Oporto', p: 'AR$ 96.500', tag: 'COL/26', img: 'linear-gradient(160deg,#EFE5D0,#B59C70)' }, { n: 'Pantalón Soho', p: 'AR$ 118.000', tag: 'STAFF', img: 'linear-gradient(160deg,#C9B797,#6F5D3B)' }, ]; const headerIn = ramp(localTime, 0.4, 1.2); const filterIn = ramp(localTime, 1.1, 1.8); // bag indicator ticks at click time const bagAdded = localTime > 5.4 ? 1 : 0; const cursorKf = [ { t: 0.8, x: 1700, y: 200 }, { t: 2.4, x: 820, y: 700 }, // hover product 1 { t: 3.6, x: 820, y: 700, click: true }, // click { t: 4.4, x: 980, y: 740 }, // move toward "add" pill { t: 5.4, x: 980, y: 740, click: true }, // click add { t: 6.6, x: 1240, y: 130 }, // up to bag { t: 8.6, x: 1240, y: 130 }, ]; return (
sak.studio / colección / otoño 26 } > {/* editorial header */}
Otoño · MMXXVI
Sak Atelier
{/* filter row */}
Filtrar: Todo LinoAlgodónSeda
Orden: EditorialVista: Grid
{/* products 2x2 */}
{items.map((it, i) => { const a = ramp(localTime, 1.6 + (i * 0.25), 2.4 + (i * 0.25), Easing.easeOutCubic); const hovered = i === 0 && localTime > 2.0 && localTime < 7.5; const liftT = hovered ? ramp(localTime, 2.0, 2.6) * (1 - ramp(localTime, 7.5, 8.0)) : 0; return (
0 ? `color-mix(in srgb, var(--bataj-bg) ${liftT * 35}%, transparent)` : 'transparent', transition: 'background .2s ease', }}>
{it.tag}
{it.n}
Lino 100% · 5 talles
{it.p}
{hovered && (
Añadir a bolsa ↗
)}
); })}
); } function SceneDeliveryLive() { const isMobile = useIsMobile(); const { localTime, duration } = useSprite(); const entry = ramp(localTime, 0, 1.0); const exit = ramp(localTime, duration - 0.8, duration, Easing.easeInCubic); const opacity = entry * (1 - exit); // Orders animate in over time; one transitions COCINA → LISTO → EN RUTA const allOrders = [ { id: '#3492', cli: 'Mesa 12', items: '2× Pizza · 1× Coca', base: '00:14', tIn: 1.0, transitions: [{ at: 3.2, status: 'COCINA', col: 'var(--bataj-accent)' }, { at: 5.5, status: 'LISTO', col: '#FFD93D' }, { at: 7.4, status: 'EN RUTA', col: 'var(--bataj-accent)' }] }, { id: '#3491', cli: 'Lourdes G.', items: '1× Bowl · Postre', base: '00:08', tIn: 1.3, transitions: [{ at: 0, status: 'EN RUTA', col: 'var(--bataj-accent)' }] }, { id: '#3490', cli: 'Diego F.', items: '3× Empanadas', base: '00:22', tIn: 1.6, transitions: [{ at: 0, status: 'COCINA', col: 'var(--bataj-accent)' }] }, { id: '#3489', cli: 'Mesa 04', items: '1× Pasta', base: '00:31', tIn: 1.9, transitions: [{ at: 0, status: 'LISTO', col: '#FFD93D' }] }, { id: '#3488', cli: 'Sofía R.', items: '2× Sushi · Postre', base: '00:42', tIn: 2.2, transitions: [{ at: 0, status: 'CERRADO', col: '#5A6071' }] }, { id: '#3487', cli: 'Mesa 09', items: '4× Pizza', base: '01:02', tIn: 2.5, transitions: [{ at: 0, status: 'CERRADO', col: '#5A6071' }] }, ]; function currentStatus(order) { let cur = order.transitions[0]; for (const tr of order.transitions) if (localTime >= tr.at + order.tIn) cur = tr; return cur; } // Timer ticks for first KDS card const ticker = (localTime > 0.5) ? Math.floor(localTime * 1.5) : 0; const cursorKf = [ { t: 0.6, x: 1700, y: 200 }, { t: 2.2, x: 700, y: 560 }, // hover #3492 { t: 3.4, x: 700, y: 560, click: true }, // click row { t: 5.4, x: 1280, y: 350, click: true }, // mark ready in KDS { t: 7.4, x: 1280, y: 460, click: true }, // confirm to ruta { t: 8.6, x: 1700, y: 800 }, ]; return (
deliv.app / console · tenant: cucina-roma ONLINE · LIVE } > {/* Nav */}
Tenant
Cucina Roma
Vistas
{[['Órdenes','78','active'],['Cocina','12',''],['Caja','5',''],['Delivery','9',''],['Reportes','—',''],['Settings','—','']].map(([n,c,act], i) => (
{n}{c}
))}
{/* Orders table */}
Órdenes activas
78 abiertas · 12 en cocina
{['TODO','COCINA','RUTA','LISTO'].map((t, i) => ( {t} ))}
{allOrders.map((o) => { const a = ramp(localTime, o.tIn, o.tIn + 0.4); const st = currentStatus(o); const highlighted = o.id === '#3492' && localTime > 3.2; return ( ); })}
ID Cliente Items Estado
{o.id} {o.cli} {o.items} {o.base} {st.status}
{/* KDS panel */}
KDS · Cocina
{[ { id:'#3492', items:['Pizza Margarita','Pizza 4 Quesos','Coca 500ml'], baseT: 14 }, { id:'#3490', items:['Empanada Carne ×2','Empanada Pollo'], baseT: 22, urgent: true }, { id:'#3493', items:['Ravioles caseros','Tiramisú'], baseT: 3 }, ].map((k, i) => { const a = ramp(localTime, 0.8 + i * 0.2, 1.4 + i * 0.2); const tt = k.baseT + (i === 0 ? Math.floor(localTime) : 0); const isMarkedReady = i === 0 && localTime > 5.5; return (
{k.id} ⏱ 00:{String(tt).padStart(2,'0')}
    {k.items.map((it, j) => (
  • {isMarkedReady && } {it}
  • ))}
); })}
); } function SceneDunamisLive() { const isMobile = useIsMobile(); const { localTime, duration } = useSprite(); const entry = ramp(localTime, 0, 1.0); const exit = ramp(localTime, duration - 0.8, duration, Easing.easeInCubic); const opacity = entry * (1 - exit); // Build chart path progressively const data = [50,58,55,62,70,68,74,80,77,84,82,90,95,92,100,98,105,110,108,115,122,118,128,134,130,138,142,148,144,152]; const maxV = Math.max(...data); const drawT = ramp(localTime, 1.6, 4.0, Easing.easeOutCubic); const drawCount = Math.max(2, Math.floor(drawT * data.length)); const drawn = data.slice(0, drawCount); const chartW = 720, chartH = 220; const pts = drawn.map((v, i) => `${(i / (data.length - 1)) * chartW},${chartH - (v / maxV) * (chartH - 20)}`).join(' '); const lastPt = drawn.length ? { x: ((drawn.length - 1) / (data.length - 1)) * chartW, y: chartH - (drawn[drawn.length - 1] / maxV) * (chartH - 20) } : null; // KPI tick-ups const mrr = (84.2 * ramp(localTime, 0.6, 2.0, Easing.easeOutCubic)).toFixed(1); const conv = (4.82 * ramp(localTime, 0.8, 2.2, Easing.easeOutCubic)).toFixed(2); const churn = (2.1 * ramp(localTime, 1.0, 2.4, Easing.easeOutCubic)).toFixed(1); const aov = Math.floor(142 * ramp(localTime, 1.2, 2.6, Easing.easeOutCubic)); const alert1 = ramp(localTime, 3.5, 4.2, Easing.easeOutBack); const alert2 = ramp(localTime, 4.5, 5.2, Easing.easeOutBack); const alert3 = ramp(localTime, 5.5, 6.2, Easing.easeOutBack); return (
dunamis.app / dashboard / Q2 — 26 Últ. 30 días LIVE } >
Bienvenida, María
Hoy el negocio está +{mrr === '0.0' ? '0.0' : (mrr * 0.147).toFixed(1)}%
Anomalías: 0 ? '#FFD93D' : 'var(--bataj-accent)' }}>{Math.round(alert1 + alert2)} Conectores: 6/6 OK
{/* KPI cards */}
{[ { l: 'MRR', v: `US$ ${mrr}k`, d: '+8.4%', col: 'var(--bataj-accent)' }, { l: 'Conversión', v: `${conv}%`, d: '+0.6pp', col: 'var(--bataj-accent)' }, { l: 'Churn', v: `${churn}%`, d: '+0.3pp', col: '#FF8C8C' }, { l: 'AOV', v: `US$ ${aov}`, d: '+11.0%', col: 'var(--bataj-accent)' }, ].map((k) => (
{k.l} {k.d}
{k.v}
))}
{/* Chart + alerts */}
Ingresos · 30 días
US$ {Math.floor(312480 * ramp(localTime, 1.0, 2.6)).toLocaleString('en-US')}{' '} +18.2%
{[0,1,2,3].map((i) => ( ))} {drawn.length > 1 && ( <> {lastPt && ( )} )}
Alertas {Math.round(alert1 + alert2)} nuevas
{[ { t:'Pico de tráfico orgánico', d:'+312% vs media · hace 14m', col:'var(--bataj-accent)', a: alert1 }, { t:'Churn detectado plan PRO', d:'2 cuentas · acción sugerida', col:'#FF8C8C', a: alert2 }, { t:'Stripe webhook delay', d:'>4s · revisar conector', col:'#FFD93D', a: alert3 }, ].map((al, i) => (
{al.t}
{al.d}
))}
); } /* ═══════════════════════════════════════════════════════════════════════ SCENE 5 — STATS (39 → 46) ═══════════════════════════════════════════════════════════════════════ */ function SceneStats() { const { localTime, duration } = useSprite(); const opacity = ramp(localTime, 0, 0.8) * (1 - ramp(localTime, duration - 0.6, duration)); const stats = [ { v: 4, suf:'', l:'productos en producción' }, { v: 100, suf:'%', l:'diseño + dev integrado' }, { v: 12, suf:'sem', l:'a primer launch', pre: '<' }, { v: 0, suf:'', l:'plantillas reutilizadas' }, ]; const headerIn = ramp(localTime, 0.2, 1.0); return (
▸ SEC.04 / PROOF · vital signs
El estudio en números.
{stats.map((s, i) => { const a = ramp(localTime, 0.8 + i * 0.18, 1.4 + i * 0.18, Easing.easeOutBack); const count = ramp(localTime, 1.0 + i * 0.15, 2.4 + i * 0.15, Easing.easeOutCubic); const display = s.v === 100 ? Math.floor(s.v * count) : Math.floor(s.v * count); return (
{s.pre || ''}{display}{s.suf}
{s.l}
); })}
); } /* ═══════════════════════════════════════════════════════════════════════ SCENE 6 — PROCESS (46 → 53) ═══════════════════════════════════════════════════════════════════════ */ function SceneProcess() { const { localTime, duration } = useSprite(); const opacity = ramp(localTime, 0, 0.8) * (1 - ramp(localTime, duration - 0.6, duration)); const steps = [ { n: '01', t: 'BRIEF', tag: '1 sem', d: 'Entendemos el negocio. No el ticket.' }, { n: '02', t: 'SISTEMA', tag: '2-3 sem', d: 'Identidad, tokens, voz. Antes de codear.' }, { n: '03', t: 'BUILD', tag: '6-12 sem', d: 'Sprints quincenales, deploy continuo.' }, { n: '04', t: 'LAUNCH', tag: 'día 1', d: 'Operativo. Cobranza, soporte, métricas.' }, ]; // current step pointer based on time const stepLen = (duration - 1.2) / steps.length; const idx = Math.min(steps.length - 1, Math.floor((localTime - 0.3) / stepLen)); return (
▸ SEC.05 / PROCESO · de 0 a producto
De cero a producto. En semanas.
{/* Progress rail */}
{steps.map((s, i) => { const a = ramp(localTime, 0.4 + i * 0.5, 1.0 + i * 0.5, Easing.easeOutBack); const active = i === idx; return (
{s.n} {s.tag}

{s.t}

{s.d}

); })}
); } /* ═══════════════════════════════════════════════════════════════════════ SCENE 7 — OUTRO (53 → 60) ═══════════════════════════════════════════════════════════════════════ */ function SceneOutro() { const { localTime, duration } = useSprite(); const a1 = ramp(localTime, 0.2, 1.2, Easing.easeOutBack); const a2 = ramp(localTime, 1.4, 2.4); const a3 = ramp(localTime, 2.8, 3.6); const out = 1 - ramp(localTime, duration - 0.6, duration); return (
▸ MISMO ESTUDIO · CUATRO LENGUAJES
Cuatro
UNIVERSOS.
↓ contemos el quinto.
▸ hola@bataj.systems
); } /* ═══════════════════════════════════════════════════════════════════════ COMPOSITION ═══════════════════════════════════════════════════════════════════════ */ function ProductsScrollReel() { return (
); } Object.assign(window, { ProductsScrollReel });