/* * Animação de intro pós-login — porta fiel da composição de * pid-intro-escuro.html (motor "OM" de animação por cena/tempo autorado, * ver CLAUDE.md "Animação de intro do login" para a extração original). * Durações das cenas ACELERADAS em relação ao original (pedido explícito do * usuário): Build e Face de 2,6s/2s pra 1s cada; Wordmark mantida em 2,8s * (é a parte "onde aparece o nome do portal", pedida pra não mudar); Portal * (a parte em que ele some) de ~1s. As fórmulas de easing/animate/blink * abaixo são a tradução 1:1 das do original (animations-v3.jsx), só os * deslocamentos de tempo dentro de cada cena foram reproporcionados pra * caber na duração nova; o "fly" final foi recalibrado pro canto superior * esquerdo REAL da janela (onde a logo colapsada da sidebar mora, * `.sidebar__logo--icon`, 40px) em vez das coordenadas de um frame de vídeo * 1920x1080 do arquivo original, que não correspondem a nada nesta página. */ const PID_INTRO_CUES = { Build: 0, Face: 1, Wordmark: 2, Portal: 4.8 }; const PID_INTRO_TOTAL = 5.7; const PID_INTRO_FADE_MS = 500; const PID_INTRO_EASING = { easeOutCubic: (t) => (--t) * t * t + 1, easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), easeOutBack: (t) => { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); }, }; function pidIntroClamp(v, min, max) { return Math.max(min, Math.min(max, v)); } function pidIntroAnimate({ from = 0, to = 1, start = 0, end = 1, ease = PID_INTRO_EASING.easeInOutQuad }) { return (t) => { if (t <= start) return from; if (t >= end) return to; const local = (t - start) / (end - start); return from + (to - from) * ease(local); }; } function pidIntroBlinkAt(t0, T) { const d = T - t0; if (d < 0 || d > 0.22) return 1; return Math.abs(Math.cos((d / 0.22) * Math.PI)); } function pidIntroCornerTarget(iconSize, finalSize, margin) { const cx = window.innerWidth / 2; const cy = window.innerHeight / 2; return { x: margin + finalSize / 2 - cx, y: margin + finalSize / 2 - cy, }; } function pidPlayLoginIntro(onDone) { const overlay = document.getElementById("login-intro"); const icon = document.getElementById("login-intro-icon"); const stage = document.getElementById("login-intro-stage"); const wordmark = document.getElementById("login-intro-wordmark"); if (!overlay || !icon || !stage || !wordmark) { onDone(); return; } const body = icon.querySelector(".login-intro__body"); const tab = icon.querySelector(".login-intro__tab"); const screen = icon.querySelector(".login-intro__screen"); const eyeL = icon.querySelector(".login-intro__eye--l"); const eyeR = icon.querySelector(".login-intro__eye--r"); const smile = icon.querySelector(".login-intro__smile"); const letters = Array.from(wordmark.querySelectorAll(".login-intro__letter")); const rule = wordmark.querySelector(".login-intro__rule"); const tagline = wordmark.querySelector(".login-intro__tagline"); const { easeOutCubic, easeInOutQuad, easeOutBack } = PID_INTRO_EASING; const CUES = PID_INTRO_CUES; const bodyDrawFn = pidIntroAnimate({ start: CUES.Build + 0.05, end: CUES.Build + 0.45, ease: easeInOutQuad }); const bodyFillFn = pidIntroAnimate({ start: CUES.Build + 0.3, end: CUES.Build + 0.55, ease: easeOutCubic }); const tabDropFn = pidIntroAnimate({ from: -18, to: 0, start: CUES.Build + 0.4, end: CUES.Build + 0.65, ease: easeOutBack }); const tabFadeFn = pidIntroAnimate({ start: CUES.Build + 0.4, end: CUES.Build + 0.55, ease: easeOutCubic }); const screenFn = pidIntroAnimate({ start: CUES.Build + 0.6, end: CUES.Build + 0.95, ease: easeOutCubic }); const eyeLFn = pidIntroAnimate({ start: CUES.Face + 0.05, end: CUES.Face + 0.35, ease: easeOutBack }); const eyeRFn = pidIntroAnimate({ start: CUES.Face + 0.15, end: CUES.Face + 0.45, ease: easeOutBack }); const smileFn = pidIntroAnimate({ start: CUES.Face + 0.35, end: CUES.Face + 0.65, ease: easeInOutQuad }); const shiftFn = pidIntroAnimate({ start: CUES.Wordmark - 0.35, end: CUES.Wordmark + 0.7, ease: easeOutCubic }); // Cena Portal comprimida (ver comentário de PID_INTRO_TOTAL acima): a // wordmark some quase na hora, o ícone voa rápido e o stage inteiro // desaparece logo depois, sem pausa parada no meio. const flyFn = pidIntroAnimate({ start: CUES.Portal + 0.05, end: CUES.Portal + 0.55, ease: easeOutCubic }); const wordmarkOutFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Portal, end: CUES.Portal + 0.3, ease: easeOutCubic }); const stageFadeFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Portal + 0.5, end: CUES.Portal + 0.9, ease: easeOutCubic }); const ruleFn = pidIntroAnimate({ start: CUES.Wordmark + 0.85, end: CUES.Wordmark + 1.6, ease: easeInOutQuad }); const tagFn = pidIntroAnimate({ start: CUES.Wordmark + 1.15, end: CUES.Wordmark + 1.9, ease: easeOutCubic }); const letterFns = letters.map((_, i) => { const s = CUES.Wordmark + 0.25 + i * 0.13; return { o: pidIntroAnimate({ start: s, end: s + 0.5, ease: easeOutCubic }), y: pidIntroAnimate({ from: 26, to: 0, start: s, end: s + 0.6, ease: easeOutCubic }), }; }); const SHIFT_X = -100; const ICON_SIZE = 160; const FINAL_ICON_SIZE = 40; const FINAL_SCALE = FINAL_ICON_SIZE / ICON_SIZE; const CORNER_MARGIN = 40; // O card de login se dissolve enquanto o overlay (já na cor final, // #121017) surge por cima — só depois dessa transição (mesma duração, // PID_INTRO_FADE_MS) é que o ícone começa a se desenhar; sem isso, o // overlay aparecia de repente por cima do card ainda visível. const loginCard = document.querySelector(".login-card"); if (loginCard) loginCard.classList.add("is-leaving"); overlay.hidden = false; void overlay.offsetWidth; overlay.classList.add("is-visible"); let start; function frame(now) { const T = Math.min((now - start) / 1000, PID_INTRO_TOTAL); const bodyDraw = bodyDrawFn(T); body.style.strokeDashoffset = String(100 * (1 - bodyDraw)); body.style.strokeOpacity = bodyDraw > 0.001 ? "1" : "0"; body.style.fillOpacity = String(bodyFillFn(T)); const tabDrop = tabDropFn(T); tab.setAttribute("transform", `translate(0 ${tabDrop})`); tab.style.opacity = String(tabFadeFn(T)); const screenP = screenFn(T); const screenScaleY = pidIntroClamp(screenP * 1.2, 0, 1); screen.setAttribute("transform", `translate(32 41) scale(${screenP} ${screenScaleY}) translate(-32 -41)`); screen.style.opacity = String(pidIntroClamp(screenP * 2, 0, 1)); const blink = Math.min( pidIntroBlinkAt(CUES.Face + 0.8, T), pidIntroBlinkAt(CUES.Wordmark + 1.1, T) ); const sL = eyeLFn(T); const sR = eyeRFn(T); eyeL.setAttribute("transform", `translate(24 38) scale(${sL} ${sL * blink})`); eyeL.style.opacity = String(pidIntroClamp(sL, 0, 1)); eyeR.setAttribute("transform", `translate(40 38) scale(${sR} ${sR * blink})`); eyeR.style.opacity = String(pidIntroClamp(sR, 0, 1)); const smileP = smileFn(T); smile.style.strokeDashoffset = String(100 * (1 - smileP)); smile.style.strokeOpacity = smileP > 0.001 ? "1" : "0"; const shift = shiftFn(T); const fly = flyFn(T); const lockupX = SHIFT_X * shift; const lockupScale = 1 - 0.2 * shift; const target = pidIntroCornerTarget(ICON_SIZE, FINAL_ICON_SIZE, CORNER_MARGIN); const logoX = lockupX + (target.x - lockupX) * fly; const logoY = (target.y - 0) * fly; const logoScale = lockupScale + (FINAL_SCALE - lockupScale) * fly; icon.style.transform = `translate(${logoX}px, ${logoY}px) scale(${logoScale})`; wordmark.style.opacity = String(pidIntroClamp(shift * 1.4, 0, 1) * wordmarkOutFn(T)); letters.forEach((el, i) => { el.style.opacity = String(letterFns[i].o(T)); el.style.transform = `translateY(${letterFns[i].y(T)}px)`; }); rule.style.transform = `scaleX(${ruleFn(T)})`; const tag = tagFn(T); tagline.style.opacity = String(tag); tagline.style.transform = `translateY(${(1 - tag) * 12}px)`; stage.style.opacity = String(stageFadeFn(T)); if (T < PID_INTRO_TOTAL) { requestAnimationFrame(frame); } else { onDone(); } } setTimeout(() => { start = performance.now(); requestAnimationFrame(frame); }, PID_INTRO_FADE_MS); }