175 lines
7.9 KiB
JavaScript
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);
|
|
}
|