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

279 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Intro pós-login sazonal "Aniversário" (56 anos da De Paula Contadores) —
* variante de pidPlayLoginIntro (login-intro.js), portada da composição
* autorada em "P.I.D. Logo Design Aniversário/Logo intro animation/export-56/
* pid-56-anos.html" (pasta de logos, fora do repositório — um componente
* React/JSX, `pid-56.jsx`, extraído do bundle "Design Canvas" com o mesmo
* processo ad-hoc de gzip+base64 por uuid já usado pra extrair a composição
* do Halloween). Cenas originais Chegada/Confete/Texto/Serpentina
* (1.1s/1.4s/1.6s/1.8s, total 5.9s): o personagem cai com os balões e
* quica ao pousar, dá um pulo e joga confete pro alto, desliza pra esquerda
* enquanto "Parabéns" + a logo da De Paula + "56 Anos" entram, e por fim
* assopra uma língua-de-sogra que desenrola e recolhe enquanto o slogan
* pousa. Só é chamada quando `pidIsAniversarioActive()` (seasonal-theme.js,
* só 15/10); fora desse dia, `pidPlayLoginIntro()` (ou a variante Halloween)
* continua sendo a usada. Reaproveita PID_INTRO_EASING/pidIntroAnimate/
* pidIntroClamp/pidIntroCornerTarget já globais de login-intro.js, mesma
* página — só a coreografia é própria daqui.
*
* Diferenças em relação à composição original:
* - A composição original foi autorada num canvas de vídeo 1920×1080 com o
* ícone e o texto posicionados por coordenadas absolutas; aqui, seguindo o
* mesmo padrão de `.login-intro__stage`/`.login-intro-h__stage`, os dois
* viram uma única linha flexível (ícone + mensagem lado a lado) — o
* "deslizar pra esquerda" do ícone (cena Texto) é reproduzido pela mesma
* técnica de SHIFT_X já usada nas outras duas intros (o ícone se desloca
* por `transform`, a mensagem só aparece no lugar fixo ao lado).
* - O "zoom" de fundo do canvas original (respiro sutil de 1.05→1.0→1.02) e
* os dois gradientes de vinheta foram descartados — floreios de canvas de
* vídeo sem efeito perceptível numa janela de overlay pequena.
* - A composição original termina com tudo esmaecendo junto (pensada como
* vídeo solto); foi acrescentada uma cena final "Fly" (mesma ideia/
* matemática do `pidIntroCornerTarget`, já usada pelas outras duas intros)
* pra o ícone voar até o canto real da sidebar antes de tudo desaparecer.
* - "56 ANOS" é específico deste ano (56º aniversário) — ano que vem,
* atualizar o número aqui e em `seasonal-theme.js`/`index.html` a partir
* de uma composição nova (mesmo espírito de "marca temporária" do
* `leiame.md` original).
*/
const PID_INTRO_A_CUES = { Chegada: 0, Confete: 1.1, Texto: 2.5, Serpentina: 4.1, Fly: 5.9 };
const PID_INTRO_A_TOTAL = 6.8;
const PID_INTRO_A_ICON_SIZE = 260; // maior que os 200px das outras 2 intros — pedido explícito do usuário, "mais fácil sua visualização"
const PID_INTRO_A_ICON_K = PID_INTRO_A_ICON_SIZE / 420; // coreografia original autorada pra um ícone de 420px
const PID_INTRO_A_CONFETTI_COLORS = ["#C6A24A", "#2E86DE", "#F2A93B", "#E8446E", "#F2EDE3", "#7B5BA8"];
// mesmo gerador pseudo-aleatório determinístico do original (pid-56.jsx),
// pra toda visita produzir a mesma explosão de confete.
function pidIntroARnd(i, salt) {
const x = Math.sin(i * 127.1 + salt * 311.7) * 43758.5453;
return x - Math.floor(x);
}
function pidIntroABuildConfettiPieces() {
return Array.from({ length: 58 }, (_, i) => {
const a = (pidIntroARnd(i, 1) - 0.5) * 2.5 - Math.PI / 2;
const speed = 300 + pidIntroARnd(i, 2) * 620;
return {
i,
vx: Math.cos(a) * speed * (0.75 + pidIntroARnd(i, 7) * 0.6),
vy: Math.sin(a) * speed,
delay: pidIntroARnd(i, 3) * 0.24,
size: 9 + pidIntroARnd(i, 4) * 15,
long: pidIntroARnd(i, 8) > 0.5,
spin: (pidIntroARnd(i, 5) - 0.5) * 1500,
color: PID_INTRO_A_CONFETTI_COLORS[i % PID_INTRO_A_CONFETTI_COLORS.length],
sway: (pidIntroARnd(i, 6) - 0.5) * 90,
};
});
}
function pidPlayLoginIntroAniversario(onDone) {
const overlay = document.getElementById("login-intro-aniversario");
const icon = document.getElementById("login-intro-a-icon");
const iconWrap = document.getElementById("login-intro-a-iconwrap");
const stage = document.getElementById("login-intro-a-stage");
const message = document.getElementById("login-intro-a-message");
const flash = document.getElementById("login-intro-a-flash");
const confettiHost = document.getElementById("login-intro-a-confetti");
if (!overlay || !icon || !iconWrap || !stage || !message || !flash || !confettiHost) {
onDone();
return;
}
const balloons = icon.querySelector(".login-intro-a__balloons");
const arm = icon.querySelector(".login-intro-a__arm");
const smile = document.getElementById("login-intro-a-smile");
const puckerOuter = document.getElementById("login-intro-a-pucker-outer");
const puckerInner = document.getElementById("login-intro-a-pucker-inner");
const hornGroup = document.getElementById("login-intro-a-horn");
const hornTube = document.getElementById("login-intro-a-horn-tube");
const hornDash = document.getElementById("login-intro-a-horn-dash");
const puff1 = document.getElementById("login-intro-a-puff1");
const puff2 = document.getElementById("login-intro-a-puff2");
const parabens = document.getElementById("login-intro-a-parabens");
const wordmarkImg = document.getElementById("login-intro-a-wordmark-img");
const rule = document.getElementById("login-intro-a-rule");
const num = document.getElementById("login-intro-a-num");
const numLabel = document.getElementById("login-intro-a-num-label");
const tagline = document.getElementById("login-intro-a-tagline");
// Peças de confete criadas uma vez (mesmo padrão de `pidApplyHalloweenCobwebs`
// pro markup só existir em memória) e reaproveitadas se a intro tocar de novo.
if (!confettiHost.dataset.piecesBuilt) {
const pieces = pidIntroABuildConfettiPieces();
pieces.forEach((p) => {
const el = document.createElement("div");
el.className = "login-intro-a__confetti-piece";
el.style.width = `${(p.size * PID_INTRO_A_ICON_K).toFixed(1)}px`;
el.style.height = `${((p.long ? p.size * 0.42 : p.size) * PID_INTRO_A_ICON_K).toFixed(1)}px`;
el.style.background = p.color;
el.style.borderRadius = p.long ? "1px" : "50%";
el.style.opacity = "0";
confettiHost.appendChild(el);
p.el = el;
});
confettiHost._pidPieces = pieces;
confettiHost.dataset.piecesBuilt = "1";
}
const confettiPieces = confettiHost._pidPieces;
const { easeOutCubic, easeInOutQuad, easeOutBack } = PID_INTRO_EASING;
const easeOutExpo = (t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
const CUES = PID_INTRO_A_CUES;
const K = PID_INTRO_A_ICON_K;
// ── cena Chegada: o personagem cai e quica ao pousar ──────────────────
const dropFn = pidIntroAnimate({ from: -260 * K, to: 0, start: CUES.Chegada + 0.05, end: CUES.Chegada + 0.95, ease: easeOutBack });
const appearFn = pidIntroAnimate({ start: CUES.Chegada + 0.05, end: CUES.Chegada + 0.5, ease: easeOutCubic });
// ── cena Confete: pulo + arremesso + explosão ─────────────────────────
const hopFn = pidIntroAnimate({ start: CUES.Confete - 0.12, end: CUES.Confete + 0.12, ease: easeOutExpo });
const hopDownFn = pidIntroAnimate({ start: CUES.Confete + 0.2, end: CUES.Confete + 0.6, ease: easeInOutQuad });
const confettiT0 = CUES.Confete + 0.05;
// ── cena Texto: ícone desliza, "Parabéns" + logo + "56 Anos" entram ──
const shiftFn = pidIntroAnimate({ start: CUES.Texto - 0.2, end: CUES.Texto + 0.6, ease: easeOutCubic });
const B = CUES.Texto + 0.1;
const parabensOFn = pidIntroAnimate({ start: B + 0.05, end: B + 0.45, ease: easeOutCubic });
const parabensYFn = pidIntroAnimate({ from: 28, to: 0, start: B + 0.05, end: B + 0.6, ease: easeOutBack });
const logoInFn = pidIntroAnimate({ start: B + 0.28, end: B + 0.85, ease: easeOutBack });
const ruleFn = pidIntroAnimate({ start: B + 0.45, end: B + 0.95, ease: easeInOutQuad });
const numFn = pidIntroAnimate({ start: B + 0.55, end: B + 1.15, ease: easeOutBack });
const tagFn = pidIntroAnimate({ start: B + 1.5, end: B + 2.0, ease: easeOutCubic });
// ── cena Serpentina: boca faz bico, língua-de-sogra desenrola e recolhe ─
const blowFn = pidIntroAnimate({ start: CUES.Serpentina + 0.05, end: CUES.Serpentina + 0.5, ease: easeOutExpo });
const recoilFn = pidIntroAnimate({ start: CUES.Serpentina + 0.65, end: CUES.Serpentina + 1.0, ease: easeInOutQuad });
// ── cena Fly (acrescentada): ícone voa até o canto real da sidebar ────
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 = -90;
const ICON_SIZE = PID_INTRO_A_ICON_SIZE;
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_A_TOTAL);
const drop = dropFn(T);
const appear = appearFn(T);
const land = pidIntroClamp(1 - Math.abs(T - (CUES.Chegada + 0.92)) / 0.28, 0, 1);
const squash = 1 - land * 0.1;
const bob = Math.sin((T - CUES.Chegada) * 2.1) * 1.6;
const swing = Math.sin((T - CUES.Chegada) * 1.5) * 3.4;
const throwUp = pidIntroClamp(1 - Math.abs(T - (CUES.Confete + 0.1)) / 0.45, 0, 1);
const hop = hopFn(T);
const hopDown = hopDownFn(T);
const lift = pidIntroClamp(hop - hopDown, 0, 1) * 38 * K;
const blow = blowFn(T);
const recoil = recoilFn(T);
const horn = pidIntroClamp(blow - recoil, 0, 1);
const hornLen = 30 * horn;
const puff = pidIntroClamp(blow * 1.4 - recoil * 2, 0, 1);
const blowing = pidIntroClamp(blow * 1.8 - recoil * 2.2, 0, 1);
icon.style.opacity = String(appear);
icon.style.transform = `translateY(${(drop - lift).toFixed(2)}px) scaleY(${squash.toFixed(3)})`;
icon.style.filter = `drop-shadow(0 ${(26 * K).toFixed(1)}px ${(44 * K).toFixed(1)}px rgba(0,0,0,0.5))`;
balloons.setAttribute("transform", `translate(${swing.toFixed(2)} ${(bob - throwUp * 16).toFixed(2)}) rotate(${(swing * 0.5).toFixed(2)} 53 34)`);
arm.setAttribute("transform", `rotate(${(-throwUp * 22).toFixed(2)} 42 49)`);
smile.style.opacity = String(1 - blowing);
puckerOuter.style.opacity = String(blowing);
puckerInner.style.opacity = String(blowing);
const hornOpacity = pidIntroClamp(blow * 2.2, 0, 1) * (1 - recoil);
hornGroup.style.opacity = String(hornOpacity);
const hornD = `M25 57 q${(hornLen * 0.42).toFixed(2)} ${(-3 - horn * 2).toFixed(2)} ${hornLen.toFixed(2)} ${(-6 - horn * 5).toFixed(2)}`;
hornTube.setAttribute("d", hornD);
hornDash.setAttribute("d", hornD);
hornTube.style.strokeWidth = String(4 - horn * 1.6);
puff1.setAttribute("cx", String(25 + hornLen + 3));
puff1.setAttribute("cy", String(51 - horn * 5));
puff1.setAttribute("r", String(2.2 + puff * 2.4));
puff1.style.opacity = String(puff * 0.85);
puff2.setAttribute("cx", String(25 + hornLen + 8));
puff2.setAttribute("cy", String(48 - horn * 5));
puff2.setAttribute("r", String(1.4 + puff * 1.8));
puff2.style.opacity = String(puff * 0.6);
confettiPieces.forEach((p) => {
const e = T - confettiT0 - p.delay;
if (e <= 0) {
p.el.style.opacity = "0";
return;
}
const x = (p.vx * e + Math.sin(e * 3.1 + p.i) * p.sway) * K;
const y = (p.vy * e + 520 * e * e) * K;
const fade = pidIntroClamp(1 - (e - 1.5) / 0.9, 0, 1);
p.el.style.opacity = String(fade);
p.el.style.transform = `translate(${x.toFixed(1)}px, ${y.toFixed(1)}px) rotate(${(p.spin * e).toFixed(1)}deg)`;
});
const burstWin = pidIntroClamp(1 - Math.abs(T - (CUES.Confete + 0.1)) / 0.3, 0, 1);
flash.style.opacity = String(burstWin * 0.1);
const shift = shiftFn(T);
const fly = flyFn(T);
const lockupX = SHIFT_X * shift;
const lockupScale = 1 - 0.26 * 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;
iconWrap.style.transform = `translate(${flyX.toFixed(1)}px, ${flyY.toFixed(1)}px) scale(${flyScale.toFixed(3)})`;
message.style.opacity = String(pidIntroClamp(shift * 1.5, 0, 1) * wordmarkOutFn(T));
const parabensY = parabensYFn(T);
parabens.style.opacity = String(parabensOFn(T));
parabens.style.transform = `translateY(${parabensY.toFixed(1)}px)`;
const logoIn = logoInFn(T);
wordmarkImg.style.opacity = String(logoIn);
wordmarkImg.style.transform = `translateY(${(-14 + (1 - logoIn) * 12).toFixed(1)}px) scale(${(0.9 + logoIn * 0.1).toFixed(3)})`;
rule.style.transform = `scaleX(${ruleFn(T).toFixed(3)})`;
const numVal = numFn(T);
num.style.opacity = String(pidIntroClamp(numVal, 0, 1));
num.style.transform = `scale(${(0.7 + numVal * 0.3).toFixed(3)})`;
numLabel.style.opacity = String(pidIntroClamp(numVal * 1.1 - 0.15, 0, 1));
const tag = tagFn(T);
tagline.style.opacity = String(tag);
tagline.style.transform = `translateY(${((1 - tag) * 12).toFixed(1)}px)`;
stage.style.opacity = String(stageFadeFn(T));
if (T < PID_INTRO_A_TOTAL) {
requestAnimationFrame(frame);
} else {
onDone();
}
}
setTimeout(() => {
start = performance.now();
requestAnimationFrame(frame);
}, PID_INTRO_FADE_MS);
}