/* * Intro pós-login sazonal "Halloween" — variante vampiro de * pidPlayLoginIntro (login-intro.js), portada da composição autorada em * "Logo intro animation Halloween/export-halloween/pid-intro-halloween.html" * (pasta de logos, fora do repositório; cenas Rise/Scare/Settle/Wordmark, * 1.3s/1.5s/1.4s/1.8s). Reaproveita PID_INTRO_EASING/pidIntroAnimate/ * pidIntroClamp/pidIntroCornerTarget já definidos em login-intro.js (mesma * página, index.html, sempre presente) em vez de duplicá-los — só a * coreografia (cues/timing/elementos) é própria daqui. Só é chamada quando * `pidIsHalloweenActive()` (seasonal-theme.js); fora da janela de outubro, * pidPlayLoginIntro() (arquivo original, intocado) continua sendo a única * usada. * * Diferença em relação à composição original: ela termina com a wordmark e * o ícone sumindo juntos (sem "pouso" nenhum, pensada como vídeo solto); aqui * foi acrescentada uma cena extra "Fly" (mesma ideia/matemática do * `pidIntroCornerTarget` de login-intro.js) pra o ícone voar até o canto da * sidebar antes de tudo desaparecer, mesmo espírito de adaptação já usado no * intro normal ("só o destino final do voo foi recalibrado"). */ const PID_INTRO_H_CUES = { Rise: 0, Scare: 1.3, Settle: 2.8, Wordmark: 4.2, Fly: 6.0 }; const PID_INTRO_H_TOTAL = 6.9; const PID_INTRO_H_ICON_K = 200 / 460; // coreografia original autorada pra um ícone de 460px; reescala pros nossos 200px function pidPlayLoginIntroHalloween(onDone) { const overlay = document.getElementById("login-intro-halloween"); const icon = document.getElementById("login-intro-h-icon"); const stage = document.getElementById("login-intro-h-stage"); const wordmark = document.getElementById("login-intro-h-wordmark"); const flash = document.getElementById("login-intro-h-flash"); if (!overlay || !icon || !stage || !wordmark || !flash) { onDone(); return; } const cape = icon.querySelector(".login-intro-h__cape"); const collar = icon.querySelector(".login-intro-h__collar"); const faceScare = icon.querySelector(".login-intro-h__face--scare"); const faceFriendly = icon.querySelector(".login-intro-h__face--friendly"); const flapWrap = icon.querySelector(".login-intro-h__flap-wrap"); const flapRotate = document.getElementById("login-intro-h-flap-rotate"); const squint = icon.querySelector(".login-intro-h__squint"); const letters = Array.from(wordmark.querySelectorAll(".login-intro-h__letter")); const rule = wordmark.querySelector(".login-intro-h__rule"); const tagline = wordmark.querySelector(".login-intro-h__tagline"); const { easeOutCubic, easeInOutQuad, easeOutBack } = PID_INTRO_EASING; const easeOutExpo = (t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t)); const CUES = PID_INTRO_H_CUES; const K = PID_INTRO_H_ICON_K; const riseFn = pidIntroAnimate({ from: 46, to: 0, start: CUES.Rise + 0.1, end: CUES.Rise + 1.1, ease: easeOutCubic }); const appearFn = pidIntroAnimate({ start: CUES.Rise + 0.1, end: CUES.Rise + 0.75, ease: easeOutCubic }); const openFn = pidIntroAnimate({ start: CUES.Scare + 0.02, end: CUES.Scare + 0.42, ease: easeOutExpo }); const settleSwayFn = pidIntroAnimate({ start: CUES.Settle + 0.3, end: CUES.Settle + 1.0, ease: easeInOutQuad }); const flapGoneFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Scare + 0.42, end: CUES.Scare + 0.95, ease: easeInOutQuad }); const lungeFn = pidIntroAnimate({ start: CUES.Scare + 0.05, end: CUES.Scare + 0.32, ease: easeOutExpo }); const relaxFn = pidIntroAnimate({ start: CUES.Scare + 0.75, end: CUES.Settle + 0.55, ease: easeInOutQuad }); const toFriendlyFn = pidIntroAnimate({ start: CUES.Settle + 0.2, end: CUES.Settle + 0.85, ease: easeInOutQuad }); const shiftFn = pidIntroAnimate({ start: CUES.Wordmark - 0.3, end: CUES.Wordmark + 0.55, ease: easeOutCubic }); const ruleFn = pidIntroAnimate({ start: CUES.Wordmark + 0.55, end: CUES.Wordmark + 1.1, ease: easeInOutQuad }); const tagFn = pidIntroAnimate({ start: CUES.Wordmark + 0.7, end: CUES.Wordmark + 1.25, ease: easeOutCubic }); const letterFns = letters.map((_, i) => { const s = CUES.Wordmark + 0.05 + i * 0.14; return { o: pidIntroAnimate({ start: s, end: s + 0.45, ease: easeOutCubic }), y: pidIntroAnimate({ from: 30, to: 0, start: s, end: s + 0.6, ease: easeOutBack }), }; }); const flyFn = pidIntroAnimate({ start: CUES.Fly + 0.05, end: CUES.Fly + 0.55, ease: easeOutCubic }); const wordmarkOutFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Fly, end: CUES.Fly + 0.3, ease: easeOutCubic }); const stageFadeFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Fly + 0.5, end: CUES.Fly + 0.9, ease: easeOutCubic }); const SHIFT_X = -110; const ICON_SIZE = 200; const FINAL_ICON_SIZE = 40; const FINAL_SCALE = FINAL_ICON_SIZE / ICON_SIZE; const CORNER_MARGIN = 40; 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_H_TOTAL); const rise = riseFn(T); const appear = appearFn(T); const open = openFn(T); const settleSway = settleSwayFn(T); const flapGone = flapGoneFn(T); const flapAngle = -64 * open + settleSway * 6; const lunge = lungeFn(T); const relax = relaxFn(T); const push = pidIntroClamp(lunge - relax, 0, 1); const shakeWin = pidIntroClamp(1 - Math.abs(T - (CUES.Scare + 0.34)) / 0.4, 0, 1); const shake = Math.sin(T * 78) * 4.2 * shakeWin; const toFriendly = toFriendlyFn(T); const scare = pidIntroClamp(open * 1.6, 0, 1) * (1 - toFriendly); const friendly = toFriendly; const squintOpacity = pidIntroClamp(1 - open * 2.4, 0, 1); const glow = pidIntroClamp(push * 1.1, 0, 1); icon.style.opacity = String(appear); const shift = shiftFn(T); const fly = flyFn(T); const lockupX = SHIFT_X * shift; const lockupScale = 1 - 0.15 * shift; const target = pidIntroCornerTarget(ICON_SIZE, FINAL_ICON_SIZE, CORNER_MARGIN); const flyX = lockupX + (target.x - lockupX) * fly; const flyY = target.y * fly; const flyScale = lockupScale + (FINAL_SCALE - lockupScale) * fly; const baseX = shake * K; const baseY = (rise - push * 26) * K; const baseScale = 1 + push * 0.14; icon.style.transform = `translate(${baseX + flyX}px, ${baseY + flyY}px) scale(${baseScale * flyScale})`; icon.style.filter = `drop-shadow(0 ${(10 * K).toFixed(1)}px ${(24 * K).toFixed(1)}px rgba(0,0,0,0.5)) drop-shadow(0 0 ${((18 + glow * 46) * K).toFixed(1)}px rgba(158,50,65,${(0.18 + glow * 0.5).toFixed(2)}))`; cape.setAttribute("transform", `translate(38 44) scale(${1 + push * 0.2} ${1 + push * 0.06}) translate(-38 -44)`); collar.style.fill = glow > 0.05 ? "#9e3241" : "#7a2733"; faceScare.style.opacity = String(scare); faceFriendly.style.opacity = String(friendly); squint.style.opacity = String(squintOpacity); flapWrap.style.opacity = String(flapGone); flapRotate.setAttribute("transform", `rotate(${flapAngle} 62 46)`); 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)`; const flashWin = pidIntroClamp(1 - Math.abs(T - (CUES.Scare + 0.16)) / 0.26, 0, 1); flash.style.opacity = String(flashWin * 0.34); stage.style.opacity = String(stageFadeFn(T)); if (T < PID_INTRO_H_TOTAL) { requestAnimationFrame(frame); } else { onDone(); } } setTimeout(() => { start = performance.now(); requestAnimationFrame(frame); }, PID_INTRO_FADE_MS); }