541 lines
29 KiB
JavaScript
541 lines
29 KiB
JavaScript
/*
|
||
* Temas sazonais do Portal — trocam a marca P.I.D. (favicon, ícone do login,
|
||
* ícone/assinatura da sidebar) por uma variante temática durante uma janela
|
||
* de datas. Dois hoje: "Halloween" (vampiro, 16 a 31/10) e "Aniversário"
|
||
* (56 anos da De Paula Contadores, só 15/10 — balões + intro de festa) — as
|
||
* duas janelas são mutuamente exclusivas por desenho (15 vs 16-31), nunca
|
||
* ativas ao mesmo tempo. Fora de qualquer janela, tudo volta ao P.I.D. normal
|
||
* (`pid-icon-eye`/`is-blinking`). Ver `docs/temas-sazonais/temas-sazonais.md`.
|
||
* Consumido por `sidebar-brand.js`, `login-logo-blink.js` e `auth.js` (troca
|
||
* de intro); incluído em toda página (mesmo padrão de `confirm-modal.js`)
|
||
* por isso mora aqui, não num arquivo específico de página.
|
||
*
|
||
* PID_HALLOWEEN_PREVIEW_FORCE/PID_ANIVERSARIO_PREVIEW_FORCE forçam o tema
|
||
* ligado o ano todo pra revisão visual; os dois já foram validados e
|
||
* desligados pelo usuário — a partir daqui só a checagem de data abaixo
|
||
* decide (todo ano). As constantes ficam aqui de propósito, em vez de
|
||
* removidas, pra revisar o visual de novo sem ter que mexer no relógio.
|
||
*
|
||
* O Halloween só passou a começar em 16/10 (antes: outubro inteiro) porque
|
||
* 15/10 é o aniversário da empresa e ganhou tema próprio — os dois nunca
|
||
* disputavam o mesmo dia antes disso por acaso, agora é por definição.
|
||
*/
|
||
const PID_HALLOWEEN_PREVIEW_FORCE = false;
|
||
const PID_ANIVERSARIO_PREVIEW_FORCE = false;
|
||
const PID_HALLOWEEN_MES = 9; // outubro, 0-indexed (Date#getMonth())
|
||
const PID_HALLOWEEN_DIA_INICIO = 16; // 16 a 31/10 — dia 15 é do tema de aniversário
|
||
const PID_ANIVERSARIO_MES = 9; // outubro, 0-indexed
|
||
const PID_ANIVERSARIO_DIA = 15; // aniversário da De Paula Contadores
|
||
const PID_HALLOWEEN_SCARE_MS = 900;
|
||
const PID_ANIVERSARIO_PARTY_MS = 650;
|
||
|
||
/* Preferência do usuário (menu da conta → "Temas sazonais") pra desligar
|
||
qualquer tema sazonal (hoje só o Halloween, mas vale pra futuros também)
|
||
e manter sempre o P.I.D. padrão — motivos: gosto pessoal ou, no caso do
|
||
Halloween, aracnofobia. Mesmo padrão de armazenamento do tema claro/escuro
|
||
(`theme.js`, só `localStorage`, nunca migrado pro banco — é preferência
|
||
de navegador, não dado de negócio). Tem prioridade sobre tudo, inclusive
|
||
`PID_HALLOWEEN_PREVIEW_FORCE`: se o usuário desligou, fica desligado. */
|
||
const PID_SEASONAL_OPT_OUT_KEY = "pid_seasonal_theme_opt_out";
|
||
|
||
function pidSeasonalThemesOptedOut() {
|
||
try {
|
||
return localStorage.getItem(PID_SEASONAL_OPT_OUT_KEY) === "1";
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function pidSetSeasonalThemesOptedOut(optedOut) {
|
||
try {
|
||
if (optedOut) {
|
||
localStorage.setItem(PID_SEASONAL_OPT_OUT_KEY, "1");
|
||
} else {
|
||
localStorage.removeItem(PID_SEASONAL_OPT_OUT_KEY);
|
||
}
|
||
} catch (e) {
|
||
// localStorage indisponível (modo privado, storage bloqueado etc.) — a
|
||
// preferência simplesmente não persiste, mas a página continua funcionando.
|
||
}
|
||
}
|
||
|
||
function pidIsHalloweenActive(data) {
|
||
if (pidSeasonalThemesOptedOut()) return false;
|
||
if (PID_HALLOWEEN_PREVIEW_FORCE) return true;
|
||
const hoje = data || new Date();
|
||
return hoje.getMonth() === PID_HALLOWEEN_MES && hoje.getDate() >= PID_HALLOWEEN_DIA_INICIO;
|
||
}
|
||
|
||
function pidIsAniversarioActive(data) {
|
||
if (pidSeasonalThemesOptedOut()) return false;
|
||
if (PID_ANIVERSARIO_PREVIEW_FORCE) return true;
|
||
const hoje = data || new Date();
|
||
return hoje.getMonth() === PID_ANIVERSARIO_MES && hoje.getDate() === PID_ANIVERSARIO_DIA;
|
||
}
|
||
|
||
/* `iconFile` é o nome do arquivo em `static/img/` a usar como favicon
|
||
(`pid-halloween-icone-escuro.svg` ou `pid-aniversario-icone-escuro.svg`) —
|
||
sempre a variante "ícone/cores completas" de cada tema, nunca um favicon
|
||
dedicado simplificado: o favicon "sem balões"/"sem capa" pensado só pra
|
||
tamanhos bem pequenos destoava do resto da marca sazonal na aba do
|
||
navegador (mesmo motivo pelo qual o P.I.D. normal também usa
|
||
`pid-icone-escuro.svg` como favicon em vez do `pid-favicon.svg` dedicado —
|
||
ver "Logos em `static/img/`" no CLAUDE.md). */
|
||
function pidApplySeasonalFavicon(iconFile) {
|
||
const link = document.querySelector('link[rel="icon"]');
|
||
if (link && link.href.indexOf("pid-icone-escuro.svg") !== -1) {
|
||
link.href = link.href.replace("pid-icone-escuro.svg", iconFile);
|
||
}
|
||
}
|
||
|
||
const PID_VAMP_FACE_FRIENDLY = `
|
||
<circle cx="30" cy="58" r="5" fill="#F2EDE3"></circle>
|
||
<circle cx="46" cy="58" r="5" fill="#F2EDE3"></circle>
|
||
<circle cx="32" cy="56.2" r="1.6" fill="#241C33"></circle>
|
||
<circle cx="48" cy="56.2" r="1.6" fill="#241C33"></circle>
|
||
<path d="M26 63.2 q4 2.6 8 -0.5" stroke="#7A2733" stroke-width="1.3" fill="none" stroke-linecap="round"></path>
|
||
<path d="M42 63.2 q4 2.6 8 -0.5" stroke="#7A2733" stroke-width="1.3" fill="none" stroke-linecap="round"></path>
|
||
<path d="M31 67 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
|
||
<path d="M33.6 68.6 L36.4 68.9 L35 72 Z" fill="#F2EDE3"></path>
|
||
<path d="M40.2 68.9 L43 68.6 L41.6 72 Z" fill="#F2EDE3"></path>`;
|
||
|
||
const PID_VAMP_FACE_SCARE = `
|
||
<circle cx="30" cy="57.4" r="5.6" fill="#F2EDE3"></circle>
|
||
<circle cx="46" cy="57.4" r="5.6" fill="#F2EDE3"></circle>
|
||
<circle cx="30" cy="57.4" r="2.1" fill="#9E3241"></circle>
|
||
<circle cx="46" cy="57.4" r="2.1" fill="#9E3241"></circle>
|
||
<path d="M24.6 51.4 L34.6 54.2" stroke="#7A2733" stroke-width="1.9" fill="none" stroke-linecap="round"></path>
|
||
<path d="M51.4 51.4 L41.4 54.2" stroke="#7A2733" stroke-width="1.9" fill="none" stroke-linecap="round"></path>
|
||
<path d="M29 66.4 q9 7.4 18 0 q-9 3 -18 0 Z" fill="#0C0814"></path>
|
||
<path d="M29 66.4 q9 7.4 18 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
|
||
<path d="M31.4 67.4 L34.6 67.9 L32.9 73 Z" fill="#F2EDE3"></path>
|
||
<path d="M41.4 67.9 L44.6 67.4 L43.1 73 Z" fill="#F2EDE3"></path>`;
|
||
|
||
function pidVampireIconRigMarkup() {
|
||
return `
|
||
<g class="pid-vamp-cape-group">
|
||
<path d="M4 9 L20 19 Q38 24 56 19 L72 9 C74 32 74 56 71 78 L5 78 C2 56 2 32 4 9 Z" fill="#2E2740"></path>
|
||
<path class="pid-vamp-collar" d="M9 16 L21 22 Q38 26 55 22 L67 16 C68 26 68 31 66 36 L10 36 C8 31 8 26 9 16 Z" fill="#7A2733"></path>
|
||
</g>
|
||
<path d="M16 22 h34 l14 14 v40 a2 2 0 0 1 -2 2 H14 a2 2 0 0 1 -2 -2 V24 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
|
||
<rect x="28" y="22" width="20" height="16" fill="#C6A24A"></rect>
|
||
<rect x="40" y="25" width="5" height="10" fill="#7B5BA8"></rect>
|
||
<rect x="18" y="46" width="40" height="28" rx="2" fill="#241C33"></rect>
|
||
<g class="pid-vamp-face pid-vamp-face--friendly">${PID_VAMP_FACE_FRIENDLY}</g>
|
||
<g class="pid-vamp-face pid-vamp-face--scare">${PID_VAMP_FACE_SCARE}</g>`;
|
||
}
|
||
|
||
/* Substitui o conteúdo de um <svg> de ícone (viewBox 0 0 64 64, o P.I.D. de
|
||
sempre) pelo vampiro (viewBox 0 0 76 80) — usado tanto no ícone da sidebar
|
||
colapsada quanto no botão da logo do login. `dataset.vampApplied` evita
|
||
reaplicar se a função rodar de novo (não deveria, mas por segurança). */
|
||
function pidApplyVampireIcon(svg) {
|
||
if (!svg || svg.dataset.vampApplied) return;
|
||
svg.setAttribute("viewBox", "0 0 76 80");
|
||
svg.innerHTML = `<g class="pid-vamp-icon-rig">${pidVampireIconRigMarkup()}</g>`;
|
||
svg.dataset.vampApplied = "1";
|
||
}
|
||
|
||
/* Idem, pra assinatura horizontal (ícone + "P.I.D." + tagline) da sidebar
|
||
expandida — viewBox 0 0 320 96 (a versão vampiro é um pouco mais larga que
|
||
os 300x80 do P.I.D. normal, por causa da capa). */
|
||
function pidApplyVampireFull(svg) {
|
||
if (!svg || svg.dataset.vampApplied) return;
|
||
svg.setAttribute("viewBox", "0 0 320 96");
|
||
svg.innerHTML = `
|
||
<g transform="translate(4,8)"><g class="pid-vamp-icon-rig">${pidVampireIconRigMarkup()}</g></g>
|
||
<text x="96" y="50" font-family="'Space Grotesk',Helvetica,Arial,sans-serif" font-size="34" font-weight="700" letter-spacing="1.4" fill="#F2EDE3">P.I.D.</text>
|
||
<text x="98" y="69" font-family="'DM Mono',ui-monospace,monospace" font-size="9.5" letter-spacing="2.6" fill="#C6A24A">PORTAL INTERNO DA DE PAULA</text>`;
|
||
svg.dataset.vampApplied = "1";
|
||
}
|
||
|
||
/* Dispara a animação de "susto" (`.is-scaring`/`pidVampShake` etc. em
|
||
base.css) em todo `.pid-vamp-icon-rig` dentro de `root` — substitui a
|
||
piscada de olhos (`.pid-icon-eye`/`is-blinking`) enquanto o tema Halloween
|
||
está ativo. Remove e reaplica a classe (em vez de só adicionar) pra
|
||
permitir cliques repetidos re-tocarem a animação do zero. */
|
||
function pidTriggerVampireScare(root) {
|
||
root.querySelectorAll(".pid-vamp-icon-rig").forEach((rig) => {
|
||
rig.classList.remove("is-scaring");
|
||
void rig.getBoundingClientRect();
|
||
rig.classList.add("is-scaring");
|
||
});
|
||
}
|
||
|
||
/* Tema "Aniversário" (56 anos da De Paula Contadores, só 15/10) — porta da
|
||
marca comemorativa autorada em "P.I.D. Logo Design Aniversário/pid-marca-56"
|
||
(pasta de logos, fora do repositório; ver leiame.md lá: "o personagem
|
||
continua o mesmo, o acento é o número"). Ao contrário do Halloween, a cara
|
||
de repouso não muda (mesmos olhos/sorriso do P.I.D. normal, por isso o
|
||
markup abaixo preserva os dois `.pid-icon-eye` — a piscada ao clicar
|
||
continua funcionando fora do clique de festa) — só ganha três balões
|
||
(azul/âmbar/rosa) segurados por um braço saindo da lateral direita do
|
||
corpo, mais um bico-de-pato/confete escondidos que só aparecem no clique
|
||
de festa (ver `pidTriggerAniversarioParty()` abaixo). Cópias dos SVGs de
|
||
referência vivem em `static/img/pid-aniversario-*.svg` (mesmo papel de
|
||
`pid-halloween-*.svg`: não usadas diretamente pela UI, que injeta o markup
|
||
inline — só o favicon aponta pro arquivo de verdade). "56 ANOS" é
|
||
específico deste ano — ano que vem, atualizar o número aqui, no rótulo da
|
||
pílula abaixo e nos 3 arquivos `pid-aniversario-*.svg` a partir de uma
|
||
pasta `pid-marca-57` nova (mesmo espírito de "marca temporária" do
|
||
leiame.md original).
|
||
Estrutura em dois `<g>` aninhados: `.pid-aniv-hop` (sem nenhum `transform`
|
||
de atributo XML — recebe o "pulo" via CSS/`is-partying`, ver base.css) por
|
||
fora, `.pid-aniv-icon-rig` (o `transform="translate(...) scale(...)"`
|
||
estático que posiciona o desenho dentro da caixa 64×64/300×80) por dentro
|
||
— separar os dois evita o mesmo problema já documentado em `.pid-icon-eye`
|
||
(base.css): um `transform` de CSS aplicado ao MESMO elemento que já tem um
|
||
`transform` de atributo substitui o atributo inteiro, perdendo a posição. */
|
||
function pidAniversarioIconRigMarkup() {
|
||
return `
|
||
<g class="pid-aniv-balloons">
|
||
<path d="M43.5 17 Q47 23.5 53.5 29.5" stroke="#B9AECB" stroke-width="0.99" fill="none" stroke-linecap="round"></path>
|
||
<path d="M53 11.5 Q53.5 20 53.5 29.5" stroke="#B9AECB" stroke-width="0.99" fill="none" stroke-linecap="round"></path>
|
||
<path d="M59 20 Q57 24.5 53.5 29.5" stroke="#B9AECB" stroke-width="0.99" fill="none" stroke-linecap="round"></path>
|
||
<ellipse cx="43.5" cy="11" rx="4.6" ry="5.6" fill="#2E86DE"></ellipse>
|
||
<path d="M42.2 16.4 h2.6 l-1.3 1.9 z" fill="#2E86DE"></path>
|
||
<ellipse cx="53" cy="5.5" rx="4.6" ry="5.6" fill="#F2A93B"></ellipse>
|
||
<path d="M51.7 10.9 h2.6 l-1.3 1.9 z" fill="#F2A93B"></path>
|
||
<ellipse cx="59" cy="14" rx="4.6" ry="5.6" fill="#E8446E"></ellipse>
|
||
<path d="M57.7 19.4 h2.6 l-1.3 1.9 z" fill="#E8446E"></path>
|
||
</g>
|
||
<g class="pid-aniv-arm">
|
||
<path d="M42 45 Q50.5 43 53.5 32" stroke="#7B5BA8" stroke-width="3.5" fill="none" stroke-linecap="round"></path>
|
||
<circle cx="53.5" cy="30" r="3.4" fill="#7B5BA8"></circle>
|
||
</g>
|
||
<g class="pid-aniv-confetti">
|
||
<rect x="47" y="12" width="2.6" height="2.6" fill="#C6A24A"></rect>
|
||
<rect x="60" y="8" width="2.2" height="2.2" fill="#2E86DE"></rect>
|
||
<circle cx="52" cy="4" r="1.4" fill="#E8446E"></circle>
|
||
<rect x="63" y="19" width="2" height="2" fill="#F2A93B"></rect>
|
||
<circle cx="41" cy="7" r="1.2" fill="#F2EDE3"></circle>
|
||
<rect x="55" y="24" width="2.2" height="2.2" fill="#E8446E"></rect>
|
||
<circle cx="65" cy="13" r="1.3" fill="#C6A24A"></circle>
|
||
<rect x="37" y="13" width="1.8" height="1.8" fill="#2E86DE"></rect>
|
||
</g>
|
||
<g transform="translate(2,18) scale(0.72)">
|
||
<path d="M10 4 h34 l14 14 v40 a2 2 0 0 1 -2 2 H8 a2 2 0 0 1 -2 -2 V6 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
|
||
<rect x="22" y="4" width="20" height="16" fill="#C6A24A"></rect>
|
||
<rect x="34" y="7" width="5" height="10" fill="#7B5BA8"></rect>
|
||
<rect x="12" y="26" width="40" height="30" rx="2" fill="#241C33"></rect>
|
||
<g class="pid-icon-eye">
|
||
<circle cx="24" cy="38" r="5" fill="#F2EDE3"></circle>
|
||
<circle cx="26" cy="36.2" r="1.6" fill="#241C33"></circle>
|
||
</g>
|
||
<g class="pid-icon-eye">
|
||
<circle cx="40" cy="38" r="5" fill="#F2EDE3"></circle>
|
||
<circle cx="42" cy="36.2" r="1.6" fill="#241C33"></circle>
|
||
</g>
|
||
<path class="pid-aniv-smile" d="M25 47 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
|
||
<ellipse class="pid-aniv-pucker-outer" cx="32" cy="48.5" rx="4" ry="3.4" fill="#C6A24A"></ellipse>
|
||
<ellipse class="pid-aniv-pucker-inner" cx="32" cy="48.5" rx="2" ry="1.7" fill="#241C33"></ellipse>
|
||
</g>
|
||
<g class="pid-aniv-horn">
|
||
<path class="pid-aniv-horn-tube" d="M25 53 q12.6 -5 30 -11" stroke="#E8446E" stroke-width="2.4" fill="none" stroke-linecap="round"></path>
|
||
<path class="pid-aniv-horn-dash" d="M25 53 q12.6 -5 30 -11" stroke="#F2EDE3" stroke-width="1" stroke-dasharray="1.6 2.4" fill="none" stroke-linecap="round"></path>
|
||
<circle class="pid-aniv-horn-puff" cx="58" cy="41" r="2.2" fill="#F2A93B"></circle>
|
||
<circle class="pid-aniv-horn-puff" cx="63" cy="38" r="1.4" fill="#C6A24A"></circle>
|
||
</g>`;
|
||
}
|
||
|
||
/* Substitui o conteúdo de um <svg> de ícone (viewBox 0 0 64 64, mesmo do
|
||
P.I.D. normal — ao contrário do vampiro, não precisa alargar a caixa) pelo
|
||
ícone com balões. `dataset.aniversarioApplied` evita reaplicar (mesmo
|
||
padrão de `dataset.vampApplied`). */
|
||
function pidApplyAniversarioIcon(svg) {
|
||
if (!svg || svg.dataset.aniversarioApplied) return;
|
||
svg.innerHTML = `<g class="pid-aniv-hop"><g class="pid-aniv-icon-rig" transform="translate(0.1,4.1) scale(0.913)">${pidAniversarioIconRigMarkup()}</g></g>`;
|
||
svg.dataset.aniversarioApplied = "1";
|
||
}
|
||
|
||
/* Idem, pra assinatura horizontal da sidebar expandida — viewBox 0 0 300 80,
|
||
igual ao P.I.D. normal (os balões cabem na mesma caixa, sem alargar como
|
||
o vampiro precisou por causa da capa). Ganha uma pílula dourada "56 ANOS"
|
||
ao lado do wordmark, no lugar da tagline normal. */
|
||
function pidApplyAniversarioFull(svg) {
|
||
if (!svg || svg.dataset.aniversarioApplied) return;
|
||
svg.innerHTML = `
|
||
<g transform="translate(8,8)"><g class="pid-aniv-hop"><g class="pid-aniv-icon-rig" transform="translate(0.1,4.1) scale(0.913)">${pidAniversarioIconRigMarkup()}</g></g></g>
|
||
<text x="86" y="42" font-family="'Space Grotesk',Helvetica,Arial,sans-serif" font-size="34" font-weight="700" letter-spacing="1.4" fill="#F2EDE3">P.I.D.</text>
|
||
<rect x="192" y="21" width="86" height="23" rx="11.5" fill="#C6A24A"></rect>
|
||
<text x="235" y="36.5" text-anchor="middle" font-family="'DM Mono',ui-monospace,monospace" font-size="10.5" font-weight="500" letter-spacing="1.8" fill="#4B2E75">56 ANOS</text>
|
||
<text x="88" y="61" font-family="'DM Mono',ui-monospace,monospace" font-size="9.5" letter-spacing="2.15" fill="#C6A24A">PORTAL INTERNO DA DE PAULA</text>`;
|
||
svg.dataset.aniversarioApplied = "1";
|
||
}
|
||
|
||
/* Dispara o "clique de festa" (`.is-partying`, keyframes em base.css) em
|
||
todo `.pid-aniv-hop` dentro de `root` — o personagem pula, o braço joga
|
||
confete pro alto e a língua-de-sogra desenrola da boca, num ciclo só de
|
||
`PID_ANIVERSARIO_PARTY_MS`. Ao contrário do susto do vampiro, isso não
|
||
substitui a piscada de olhos — os dois convivem (a piscada continua
|
||
disparando `.pid-icon-eye`/`is-blinking` de dentro do próprio clique de
|
||
festa, ver `sidebar-brand.js`/`login-logo-blink.js`). Mesmo truque de
|
||
remover+reaplicar a classe (em vez de só adicionar) de
|
||
`pidTriggerVampireScare` pra permitir cliques repetidos re-tocarem do
|
||
zero. */
|
||
function pidTriggerAniversarioParty(root) {
|
||
root.querySelectorAll(".pid-aniv-hop").forEach((hop) => {
|
||
hop.classList.remove("is-partying");
|
||
void hop.getBoundingClientRect();
|
||
hop.classList.add("is-partying");
|
||
});
|
||
}
|
||
|
||
/* Teia de aranha decorativa de canto (`static/css/halloween-cobweb.css`) —
|
||
markup só existe em memória (nunca hardcoded no template) pra não sobrar
|
||
nada no HTML fora da janela do Halloween. `.pw-spider`/`.pw-legs` (mesmas
|
||
classes usadas pela animação em CSS) andam/mexem sozinhas via
|
||
`@keyframes` contínuo, sem precisar de mais JS depois de injetadas. */
|
||
const PID_COBWEB_SVG = `<svg viewBox="0 0 200 200" role="img" aria-hidden="true">
|
||
<g class="pw-strands">
|
||
<line x1="0" y1="0" x2="200.0" y2="10.5"></line>
|
||
<line x1="0" y1="0" x2="200.0" y2="53.6"></line>
|
||
<line x1="0" y1="0" x2="200.0" y2="106.3"></line>
|
||
<line x1="0" y1="0" x2="200.0" y2="180.1"></line>
|
||
<line x1="0" y1="0" x2="134.9" y2="200.0"></line>
|
||
<line x1="0" y1="0" x2="76.8" y2="200.0"></line>
|
||
<line x1="0" y1="0" x2="35.3" y2="200.0"></line>
|
||
<line x1="0" y1="0" x2="7.0" y2="200.0"></line>
|
||
<line class="pw-strand-stray" x1="0" y1="0" x2="296.3" y2="46.9"></line>
|
||
<path class="pw-ring" d="M 30.0,1.6 Q 32.4,5.2 30.0,8.0 Q 32.4,13.0 30.0,16.0 Q 32.4,23.2 30.0,27.0 Q 27.1,30.8 20.2,30.0 Q 17.1,32.4 11.5,30.0 Q 9.1,32.4 5.3,30.0 Q 3.4,32.4 1.0,30.0"></path>
|
||
<path class="pw-ring" d="M 58.0,3.0 Q 62.6,10.0 58.0,15.5 Q 62.6,25.0 58.0,30.8 Q 62.6,44.9 58.0,52.2 Q 52.4,59.5 39.1,58.0 Q 33.1,62.6 22.3,58.0 Q 17.5,62.6 10.2,58.0 Q 6.6,62.6 2.0,58.0"></path>
|
||
<path class="pw-ring" d="M 90.0,4.7 Q 97.2,15.6 90.0,24.1 Q 97.2,38.9 90.0,47.9 Q 97.2,69.6 90.0,81.0 Q 81.4,92.4 60.7,90.0 Q 51.4,97.2 34.5,90.0 Q 27.2,97.2 15.9,90.0 Q 10.3,97.2 3.1,90.0"></path>
|
||
<path class="pw-ring" d="M 126.0,6.6 Q 136.1,21.8 126.0,33.8 Q 136.1,54.4 126.0,67.0 Q 136.1,97.4 126.0,113.5 Q 113.9,129.3 85.0,126.0 Q 72.0,136.1 48.4,126.0 Q 38.1,136.1 22.2,126.0 Q 14.4,136.1 4.4,126.0"></path>
|
||
<path class="pw-ring" d="M 166.0,8.7 Q 179.3,28.7 166.0,44.5 Q 179.3,71.7 166.0,88.3 Q 179.3,128.4 166.0,149.5 Q 150.1,170.4 112.0,166.0 Q 94.9,179.3 63.7,166.0 Q 50.2,179.3 29.3,166.0 Q 18.9,179.3 5.8,166.0"></path>
|
||
</g>
|
||
<g class="pw-spider">
|
||
<g class="pw-legs">
|
||
<path d="M-3,-2 L-14,-8 L-22,-6"></path>
|
||
<path d="M-2,-1 L-13,2 L-21,5"></path>
|
||
<path d="M-2,1 L-13,7 L-20,13"></path>
|
||
<path d="M-3,2 L-12,11 L-18,18"></path>
|
||
<path d="M3,-2 L14,-8 L22,-6"></path>
|
||
<path d="M2,-1 L13,2 L21,5"></path>
|
||
<path d="M2,1 L13,7 L20,13"></path>
|
||
<path d="M3,2 L12,11 L18,18"></path>
|
||
</g>
|
||
<ellipse class="pw-abdomen" cx="0" cy="6" rx="7" ry="9"></ellipse>
|
||
<circle class="pw-head" cx="0" cy="-6" r="4.5"></circle>
|
||
<circle class="pw-eye" cx="-1.6" cy="-7" r="0.9"></circle>
|
||
<circle class="pw-eye" cx="1.6" cy="-7" r="0.9"></circle>
|
||
</g>
|
||
<g class="pw-skull">
|
||
<circle cx="0" cy="0" r="6.5"></circle>
|
||
<ellipse class="pw-skull-socket" cx="-2.4" cy="-0.6" rx="1.5" ry="1.9"></ellipse>
|
||
<ellipse class="pw-skull-socket" cx="2.4" cy="-0.6" rx="1.5" ry="1.9"></ellipse>
|
||
<path d="M-1.2 3.4 L-0.4 2.2 L0.4 3.4 L1.2 2.2"></path>
|
||
</g>
|
||
</svg>`;
|
||
|
||
/* Quais aranhas já "morreram" (clicadas numa visita anterior) — persistido
|
||
pra sobreviver a um F5/nova navegação, pedido explícito do usuário
|
||
("guardar que a aranha foi morta... até executar o comando tab+b que
|
||
reseta a aranha"). Cada aranha tem um id próprio (`${containerId}__tl`/
|
||
`__br` — os dois cantos de cada uma das 2 páginas, 4 ids possíveis no
|
||
total) guardado como array JSON num único `localStorage`, mesmo padrão
|
||
de armazenamento das outras preferências sazonais (`pid_theme`,
|
||
`pid_seasonal_theme_opt_out`) — nunca migrado pro banco. */
|
||
const PID_DEAD_SPIDERS_KEY = "pid_seasonal_dead_spiders";
|
||
|
||
function pidGetDeadSpiders() {
|
||
try {
|
||
const raw = JSON.parse(localStorage.getItem(PID_DEAD_SPIDERS_KEY) || "[]");
|
||
return Array.isArray(raw) ? raw : [];
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function pidMarkSpiderDead(spiderId) {
|
||
try {
|
||
const dead = pidGetDeadSpiders();
|
||
if (!dead.includes(spiderId)) {
|
||
dead.push(spiderId);
|
||
localStorage.setItem(PID_DEAD_SPIDERS_KEY, JSON.stringify(dead));
|
||
}
|
||
} catch (e) {
|
||
// localStorage indisponível — a aranha "morre" só nesta visita, sem persistir
|
||
}
|
||
}
|
||
|
||
function pidResetDeadSpiders() {
|
||
try {
|
||
localStorage.removeItem(PID_DEAD_SPIDERS_KEY);
|
||
} catch (e) {
|
||
// ignorado
|
||
}
|
||
}
|
||
|
||
/* Preenche o container (já presente e `hidden` no template, `#pw-cobwebs-login`
|
||
em index.html ou `#pw-cobwebs-portal` em portal.html) com os 2 cantos
|
||
(superior-esquerdo/inferior-direito, pedido explícito do usuário) e revela.
|
||
Se a aranha daquele canto já morreu numa visita anterior
|
||
(`pidGetDeadSpiders()`), nasce direto com `.is-dead` (sem tocar a
|
||
animação de novo — só um "estado final" instantâneo). Senão, liga o
|
||
clique (`.pw-spider`, `pointer-events:auto` só nela — o resto da teia
|
||
continua `pointer-events:none`): "morre" (cai girando e some, pernas
|
||
encolhem — `.is-dying`/`pwSpiderDie` em halloween-cobweb.css) e uma
|
||
caveirinha sobe da teia (`.pw-skull`, `.is-rising`/`pwSkullRise`),
|
||
deixando a teia vazia — e grava o óbito (`pidMarkSpiderDead()`), que só
|
||
é desfeito pelo atalho de teclado Tab+B (ver `pidBindSeasonalResetShortcut()`
|
||
abaixo). É um no-op seguro em qualquer outra página (o id não existe ali). */
|
||
function pidApplyHalloweenCobwebs(containerId) {
|
||
const el = document.getElementById(containerId);
|
||
if (!el || el.dataset.cobwebApplied) return;
|
||
el.innerHTML =
|
||
`<div class="pw-corner pw-corner--tl">${PID_COBWEB_SVG}</div>` +
|
||
`<div class="pw-corner pw-corner--br">${PID_COBWEB_SVG}</div>`;
|
||
el.hidden = false;
|
||
el.dataset.cobwebApplied = "1";
|
||
|
||
const deadSpiders = pidGetDeadSpiders();
|
||
|
||
el.querySelectorAll(".pw-corner").forEach((cornerEl) => {
|
||
const corner = cornerEl.classList.contains("pw-corner--tl") ? "tl" : "br";
|
||
const spiderId = `${containerId}__${corner}`;
|
||
const spider = cornerEl.querySelector(".pw-spider");
|
||
if (!spider) return;
|
||
|
||
if (deadSpiders.includes(spiderId)) {
|
||
spider.classList.add("is-dead");
|
||
return;
|
||
}
|
||
|
||
spider.addEventListener(
|
||
"click",
|
||
() => {
|
||
const skull = spider.parentElement.querySelector(".pw-skull");
|
||
spider.classList.add("is-dying");
|
||
if (skull) skull.classList.add("is-rising");
|
||
pidMarkSpiderDead(spiderId);
|
||
},
|
||
{ once: true }
|
||
);
|
||
});
|
||
}
|
||
|
||
/* Bolo de aniversário decorativo, tema sazonal Aniversário — só na tela
|
||
Principal (`portal.html`), fixo no canto inferior-direito da JANELA
|
||
(`position:fixed`, não da página — sempre visível, mesmo com scroll).
|
||
Clicar apaga as velinhas (`.is-blowing`/`pidCakeFlameOut` +
|
||
`pidCakeSmokeRise` em `aniversario-decor.css`, 1x só), e elas ficam
|
||
apagadas até o próximo carregamento da página.
|
||
|
||
**O estado apagado NÃO é persistido**, ao contrário da aranha morta do
|
||
Halloween — e isso é deliberado, não esquecimento. A persistência chegou a
|
||
existir aqui (copiada de `pidMarkSpiderDead`) e foi removida: as velas
|
||
nasciam apagadas pro resto da vida depois do primeiro clique, sem nenhuma
|
||
pista disso na tela, e só voltavam com o atalho Tab+B. O usuário relatou
|
||
como bug ("não aparece o fogo das velinhas"), porque é um easter egg que
|
||
some sozinho e não volta. A aranha persiste porque o usuário PEDIU
|
||
explicitamente; aqui ele pediu só "ao clicar nele as velas se apagam". */
|
||
|
||
/* As chamas nascem exatamente no topo de cada vela (`y` do `<rect>`), não
|
||
alguns pontos acima — na primeira versão flutuavam soltas bem longe do
|
||
pavio. O gotejamento de cada camada é da cor da camada DE CIMA (creme
|
||
escorrendo sobre o dourado, dourado escorrendo sobre o roxo), senão fica
|
||
invisível contra a própria camada.
|
||
|
||
`.pid-cake-halo` é o halo de luz no ar em volta das velas (o brilho colado
|
||
em cada chama vem de um `drop-shadow` no CSS) — fica DEPOIS das camadas do
|
||
bolo e ANTES das velas, pra a luz lavar o topo do bolo sem desbotar as
|
||
chamas, que continuam nítidas por cima. O `<radialGradient>` mora num
|
||
`<defs>` com id fixo: o bolo é injetado uma vez só, em portal.html, então
|
||
não há risco de id duplicado (mesmo padrão do `clipPath` da intro do
|
||
Halloween em index.html). */
|
||
const PID_CAKE_SVG = `<svg viewBox="0 0 120 110" role="img" aria-hidden="true">
|
||
<defs>
|
||
<radialGradient id="pidCakeHaloGrad">
|
||
<stop offset="0%" stop-color="#F2A93B" stop-opacity="0.42"></stop>
|
||
<stop offset="45%" stop-color="#F2A93B" stop-opacity="0.14"></stop>
|
||
<stop offset="100%" stop-color="#F2A93B" stop-opacity="0"></stop>
|
||
</radialGradient>
|
||
</defs>
|
||
<ellipse class="pid-cake-shadow" cx="60" cy="102" rx="42" ry="6"></ellipse>
|
||
<path class="pid-cake-tier2" d="M18 74 h84 v24 a2 2 0 0 1 -2 2 H20 a2 2 0 0 1 -2 -2 z"></path>
|
||
<path class="pid-cake-drip2" d="M18 74 q7 9 14 0 q7 9 14 0 q7 9 14 0 q7 9 14 0 q7 9 14 0 q7 9 14 0 Z"></path>
|
||
<circle class="pid-cake-dot pid-cake-dot--blue" cx="32" cy="88" r="2.4"></circle>
|
||
<circle class="pid-cake-dot pid-cake-dot--pink" cx="60" cy="91" r="2.4"></circle>
|
||
<circle class="pid-cake-dot pid-cake-dot--amber" cx="88" cy="88" r="2.4"></circle>
|
||
<path class="pid-cake-tier1" d="M32 50 h56 v24 H32 z"></path>
|
||
<path class="pid-cake-drip1" d="M32 50 q5.6 7 11.2 0 q5.6 7 11.2 0 q5.6 7 11.2 0 q5.6 7 11.2 0 q5.6 7 11.2 0 Z"></path>
|
||
<ellipse class="pid-cake-halo" cx="60" cy="30" rx="48" ry="30" fill="url(#pidCakeHaloGrad)"></ellipse>
|
||
<g class="pid-cake-candle">
|
||
<rect x="42" y="34" width="4" height="16" fill="#2E86DE"></rect>
|
||
<path class="pid-cake-smoke" d="M44 32 q4 -5 0 -10"></path>
|
||
<path class="pid-cake-flame" d="M44 34 q4 -5 0 -9 q-4 4 0 9 z"></path>
|
||
</g>
|
||
<g class="pid-cake-candle">
|
||
<rect x="58" y="30" width="4" height="20" fill="#C6A24A"></rect>
|
||
<path class="pid-cake-smoke" d="M60 28 q4 -5 0 -10"></path>
|
||
<path class="pid-cake-flame" d="M60 30 q4 -5 0 -9 q-4 4 0 9 z"></path>
|
||
</g>
|
||
<g class="pid-cake-candle">
|
||
<rect x="74" y="34" width="4" height="16" fill="#E8446E"></rect>
|
||
<path class="pid-cake-smoke" d="M76 32 q4 -5 0 -10"></path>
|
||
<path class="pid-cake-flame" d="M76 34 q4 -5 0 -9 q-4 4 0 9 z"></path>
|
||
</g>
|
||
</svg>`;
|
||
|
||
/* Preenche o container (já presente e `hidden` no template,
|
||
`#pid-aniv-cake` em portal.html) e liga o clique — botão porque é
|
||
interativo (`aria-label`, foco por teclado funcionam de graça). O clique
|
||
adiciona `.is-blowing` (toca `pidCakeFlameOut`/`pidCakeSmokeRise` uma vez)
|
||
e, depois de `PID_CAKE_BLOW_MS`, troca pra `.is-blown` (estado final em
|
||
repouso, sem `animation` nenhuma rodando) — que dura só até o próximo
|
||
carregamento, ver o comentário acima. É um no-op seguro em qualquer outra
|
||
página (o id não existe ali). */
|
||
const PID_CAKE_BLOW_MS = 1400;
|
||
|
||
function pidApplyAniversarioCake(containerId) {
|
||
const el = document.getElementById(containerId);
|
||
if (!el || el.dataset.cakeApplied) return;
|
||
el.innerHTML = `<button type="button" class="pid-aniv-cake__btn" aria-label="Assoprar as velinhas">${PID_CAKE_SVG}</button>`;
|
||
el.hidden = false;
|
||
el.dataset.cakeApplied = "1";
|
||
|
||
const btn = el.querySelector(".pid-aniv-cake__btn");
|
||
if (!btn) return;
|
||
|
||
btn.addEventListener(
|
||
"click",
|
||
() => {
|
||
el.classList.add("is-blowing");
|
||
setTimeout(() => {
|
||
el.classList.remove("is-blowing");
|
||
el.classList.add("is-blown");
|
||
}, PID_CAKE_BLOW_MS);
|
||
},
|
||
{ once: true }
|
||
);
|
||
}
|
||
|
||
/* Atalho de teclado Tab+B (segurar Tab e apertar B) — reseta as aranhas
|
||
"mortas" (`pidResetDeadSpiders()`) salvas e recarrega a página, mesmo
|
||
raciocínio do toggle de opt-out (`account.js`): o estado já foi aplicado
|
||
no carregamento atual, não dá pra desfazer in-place. Não chama
|
||
`preventDefault()` no Tab (continuaria navegando o foco normalmente,
|
||
acessibilidade intacta) — só rastreia se ele está pressionado no momento
|
||
em que o B chega. Incluído em toda página (mesmo padrão de
|
||
`confirm-modal.js`), mas só tem efeito onde há aranha salva como morta. O
|
||
bolo do tema de Aniversário não entra aqui: as velinhas reacendem sozinhas
|
||
a cada carregamento, então não há o que resetar. */
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
let tabHeld = false;
|
||
|
||
document.addEventListener("keydown", (event) => {
|
||
if (event.key === "Tab") {
|
||
tabHeld = true;
|
||
return;
|
||
}
|
||
if (tabHeld && (event.key === "b" || event.key === "B") && pidGetDeadSpiders().length) {
|
||
pidResetDeadSpiders();
|
||
window.location.reload();
|
||
}
|
||
});
|
||
|
||
document.addEventListener("keyup", (event) => {
|
||
if (event.key === "Tab") tabHeld = false;
|
||
});
|
||
});
|