portal_publico/static/js/login-intro-halloween.js

175 lines
7.9 KiB
JavaScript

/*
* 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);
}