markup.html · 160 linhas · 10,1 KB
<!-- ==========================================================================
RÉGUA DE LIQUIDEZ — objetivos arrastáveis numa linha do tempo de 30 anos
Prefixo: liq-
Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js
Usa as primitivas .wrap .sec-pad .sec-head .eyebrow .sec-title .sec-lead
.btn .btn-ghost do core.
Três camadas alinhadas pelo mesmo eixo, todas filhas de .liq-plot:
.liq-track as cinco fichas, posicionadas por --liq-ano (0 a 30)
.liq-rail a régua: linha, marcas de 5 em 5 anos e rótulos
.liq-graf a área em SVG, viewBox 0 0 300 100 (x = ano × 10)
A posição de cada ficha vive no HTML, em duas variáveis:
style="--liq-ano:5;--liq-fila:0"
--liq-ano é o ano, de 0 a 30 — o eixo x
--liq-fila é a faixa vertical, de 0 para cima — só muda quando duas
fichas ficam perto demais e o script as desempilha
O valor de cada objetivo vive em data-liq-valor, em MILHARES de reais.
É de lá que saem as três leituras de horizonte — e é lá que o consumidor
troca os números ilustrativos, não no script (o estado sem JavaScript
precisa do mesmo dado, e duas cópias divergiriam no primeiro ajuste).
SEM JAVASCRIPT a peça fica inteira: as cinco fichas nas posições do HTML,
a área já desenhada, as três leituras corretas. Só não arrasta — e a
frase que promete o arrasto nasce `hidden`, revelada pelo script.
========================================================================== -->
<section class="liq-sec sec-dark sec-pad" id="liquidez">
<div class="wrap">
<div class="sec-head liq-head">
<span class="eyebrow" data-rv>Liquidez</span>
<h2 class="sec-title ttl">Cada objetivo tem um <em>prazo</em></h2>
<p class="sec-lead liq-lead" data-rv data-d="1">A carteira se organiza a partir de quando o dinheiro é necessário. Não a partir do que rende mais.</p>
</div>
<div class="liq-quadro" data-rv data-d="2" data-liq>
<p class="liq-ajuda">Cada ficha marca quando o dinheiro é necessário.<span class="liq-ajuda-js" hidden> Arraste uma ficha pela régua — ou dê foco nela e use as setas.</span></p>
<!-- CONTEÚDO ILUSTRATIVO — objetivos, anos e valores são exemplo -->
<div class="liq-scroll">
<div class="liq-plot">
<div class="liq-track">
<div class="liq-ficha" style="--liq-ano:0;--liq-fila:0"
data-liq-valor="240" data-liq-rotulo="Reserva de emergência"
aria-label="Reserva de emergência, R$ 240 mil. As setas movem o ano na régua.">
<span class="liq-ficha-topo">
<svg class="liq-icone" viewBox="0 0 18 18" aria-hidden="true" focusable="false"><path d="M9 2.2 3.6 4.2v4.1c0 3.2 2.2 5.6 5.4 7.5 3.2-1.9 5.4-4.3 5.4-7.5V4.2Z"/></svg>
<span class="liq-ficha-ano" aria-live="polite" aria-atomic="true">hoje</span>
</span>
<span class="liq-ficha-rot">Reserva de emergência</span>
<span class="liq-ficha-val">R$ 240 mil</span>
</div>
<div class="liq-ficha" style="--liq-ano:5;--liq-fila:0"
data-liq-valor="900" data-liq-rotulo="Troca de imóvel"
aria-label="Troca de imóvel, R$ 900 mil. As setas movem o ano na régua.">
<span class="liq-ficha-topo">
<svg class="liq-icone" viewBox="0 0 18 18" aria-hidden="true" focusable="false"><path d="M2.8 8.2 9 3l6.2 5.2M4.6 7v8h8.8V7"/></svg>
<span class="liq-ficha-ano" aria-live="polite" aria-atomic="true">em 5 anos</span>
</span>
<span class="liq-ficha-rot">Troca de imóvel</span>
<span class="liq-ficha-val">R$ 900 mil</span>
</div>
<div class="liq-ficha" style="--liq-ano:12;--liq-fila:0"
data-liq-valor="600" data-liq-rotulo="Faculdade dos filhos"
aria-label="Faculdade dos filhos, R$ 600 mil. As setas movem o ano na régua.">
<span class="liq-ficha-topo">
<svg class="liq-icone" viewBox="0 0 18 18" aria-hidden="true" focusable="false"><path d="M1.8 6.6 9 3.3l7.2 3.3L9 9.9ZM4.8 8.1v3.6c0 1.2 1.9 2.1 4.2 2.1s4.2-.9 4.2-2.1V8.1M15.6 7.4v3.3"/></svg>
<span class="liq-ficha-ano" aria-live="polite" aria-atomic="true">em 12 anos</span>
</span>
<span class="liq-ficha-rot">Faculdade dos filhos</span>
<span class="liq-ficha-val">R$ 600 mil</span>
</div>
<div class="liq-ficha" style="--liq-ano:18;--liq-fila:0"
data-liq-valor="1100" data-liq-rotulo="Venda da empresa"
aria-label="Venda da empresa, R$ 1,1 milhão. As setas movem o ano na régua.">
<span class="liq-ficha-topo">
<svg class="liq-icone" viewBox="0 0 18 18" aria-hidden="true" focusable="false"><path d="M4.2 15V3.8h9.6V15M2.4 15h13.2M6.7 6.6h1.4M9.9 6.6h1.4M6.7 9.4h1.4M9.9 9.4h1.4"/></svg>
<span class="liq-ficha-ano" aria-live="polite" aria-atomic="true">em 18 anos</span>
</span>
<span class="liq-ficha-rot">Venda da empresa</span>
<span class="liq-ficha-val">R$ 1,1 mi</span>
</div>
<div class="liq-ficha" style="--liq-ano:27;--liq-fila:0"
data-liq-valor="1800" data-liq-rotulo="Renda vitalícia"
aria-label="Renda vitalícia, R$ 1,8 milhão. As setas movem o ano na régua.">
<span class="liq-ficha-topo">
<svg class="liq-icone" viewBox="0 0 18 18" aria-hidden="true" focusable="false"><path d="M2.4 12.6h13.2M4.6 12.6a4.4 4.4 0 0 1 8.8 0"/></svg>
<span class="liq-ficha-ano" aria-live="polite" aria-atomic="true">em 27 anos</span>
</span>
<span class="liq-ficha-rot">Renda vitalícia</span>
<span class="liq-ficha-val">R$ 1,8 mi</span>
</div>
</div>
<div class="liq-rail" aria-hidden="true">
<span class="liq-marca" style="--liq-ano:0"><span class="liq-marca-txt">hoje</span></span>
<span class="liq-marca" style="--liq-ano:5"><span class="liq-marca-txt">5</span></span>
<span class="liq-marca" style="--liq-ano:10"><span class="liq-marca-txt">10</span></span>
<span class="liq-marca" style="--liq-ano:15"><span class="liq-marca-txt">15</span></span>
<span class="liq-marca" style="--liq-ano:20"><span class="liq-marca-txt">20</span></span>
<span class="liq-marca" style="--liq-ano:25"><span class="liq-marca-txt">25</span></span>
<span class="liq-marca" style="--liq-ano:30"><span class="liq-marca-txt">30 anos</span></span>
</div>
<!-- Decorativo: o dado desta seção está nas fichas e nas leituras.
preserveAspectRatio="none" estica o eixo x até a largura da
coluna; por isso os traços levam vector-effect. -->
<svg class="liq-graf" viewBox="0 0 300 100" preserveAspectRatio="none" aria-hidden="true" focusable="false">
<path class="liq-grade" d="M50 0V100M100 0V100M150 0V100M200 0V100M250 0V100" vector-effect="non-scaling-stroke"/>
<path class="liq-area" d="M0 65.1L5 75L10 93.4L15 100L20 100L25 99.7L30 89.3L35 67.6L40 42.4L45 22.4L50 14.9L55 39L60 84L65 100L70 100L75 100L80 98.6L85 95.1L90 90L95 83.8L100 77.3L105 71.2L110 66.3L115 63L120 61.9L125 71.7L130 89.1L135 91.8L140 85.9L145 79.2L150 71.9L155 64.6L160 57.8L165 52L170 47.5L175 44.7L180 43.8L185 59.7L190 89.4L195 100L200 100L205 100L210 100L215 98.4L220 93.8L225 86.5L230 77L235 65.9L240 54L245 42.1L250 31L255 21.5L260 14.2L265 9.6L270 8L275 34L280 82.7L285 100L290 100L295 100L300 100L0 100Z"/>
<path class="liq-linha" pathLength="1" vector-effect="non-scaling-stroke" d="M0 65.1L5 75L10 93.4L15 100L20 100L25 99.7L30 89.3L35 67.6L40 42.4L45 22.4L50 14.9L55 39L60 84L65 100L70 100L75 100L80 98.6L85 95.1L90 90L95 83.8L100 77.3L105 71.2L110 66.3L115 63L120 61.9L125 71.7L130 89.1L135 91.8L140 85.9L145 79.2L150 71.9L155 64.6L160 57.8L165 52L170 47.5L175 44.7L180 43.8L185 59.7L190 89.4L195 100L200 100L205 100L210 100L215 98.4L220 93.8L225 86.5L230 77L235 65.9L240 54L245 42.1L250 31L255 21.5L260 14.2L265 9.6L270 8L275 34L280 82.7L285 100L290 100L295 100L300 100"/>
</svg>
<p class="liq-eixo-nota">Exigência de liquidez ao longo do tempo — forma relativa, sem escala de valor.</p>
</div>
</div>
<div class="liq-saida">
<div class="liq-saida-col">
<h3 class="liq-saida-tit">Faixa por horizonte</h3>
<!-- CONTEÚDO ILUSTRATIVO — percentuais calculados sobre os valores acima -->
<ul class="liq-leituras">
<li class="liq-leitura" style="--liq-frac:.05">
<span class="liq-leitura-num"><span class="liq-leitura-pct">5</span>%</span>
<span class="liq-leitura-rot">nos próximos 12 meses</span>
<span class="liq-leitura-barra"></span>
</li>
<li class="liq-leitura" style="--liq-frac:.19">
<span class="liq-leitura-num"><span class="liq-leitura-pct">19</span>%</span>
<span class="liq-leitura-rot">no médio prazo, de 1 a 5 anos</span>
<span class="liq-leitura-barra"></span>
</li>
<li class="liq-leitura" style="--liq-frac:.76">
<span class="liq-leitura-num"><span class="liq-leitura-pct">76</span>%</span>
<span class="liq-leitura-rot">de longo prazo, acima de 5 anos</span>
<span class="liq-leitura-barra"></span>
</li>
</ul>
</div>
<div class="liq-saida-col">
<p class="nota liq-nota">Simulação ilustrativa, para orientar a conversa. As três faixas são a divisão aritmética dos objetivos que você mesmo posicionou na régua — não são recomendação de investimento nem promessa de rentabilidade. Cenários passados não garantem resultado futuro.</p>
<a class="btn btn-ghost liq-btn" href="#contato">Levar esta régua a uma conversa</a>
</div>
</div>
</div>
</div>
</section>
estilo.css · 255 linhas · 9,9 KB
/* ==========================================================================
RÉGUA DE LIQUIDEZ — objetivos arrastáveis numa linha do tempo de 30 anos
Prefixo: liq-
Tokens usados: --accent --bg --display --ease --fg --fg-2 --fg-muted
--radius-media --rule --rule-2
Primitivas usadas: .wrap .sec-pad .sec-head .eyebrow .sec-title .sec-lead
.btn .btn-ghost (core/base.css)
O eixo x é um só para as três camadas, e é por isso que elas são irmãs
dentro de .liq-plot: a ficha, a marca da régua e o pico da área caem na
mesma coluna de pixels porque todas leem a mesma largura de conteúdo.
O recuo lateral de .liq-plot é METADE da ficha. Sem ele, a ficha do ano 0
(centrada em left:0 com translateX(-50%)) sairia meia caixa para fora.
========================================================================== */
.liq-quadro{
/* ---- Parafusos públicos ---- */
--liq-ficha-w:122px; /* largura da ficha */
--liq-ficha-h:104px; /* altura FIXA da ficha — é o que alinha as filas */
--liq-fila-h:116px; /* passo vertical entre duas filas (altura + folga) */
--liq-haste:24px; /* px entre a ficha da fila 0 e a régua */
--liq-graf-h:132px; /* altura da área */
--liq-min:920px; /* largura mínima do gráfico antes de rolar de lado */
--liq-anos:30; /* fim da régua, em anos */
margin-top:clamp(38px,5vh,62px);
border:1px solid var(--rule);
padding:clamp(20px,2.2vw,28px);
}
.liq-ajuda{font-size:13.5px;line-height:1.5;color:var(--fg-muted)}
/* ---- o eixo ----
.liq-scroll rola de lado em tela estreita (ver a nota do mobile no fim).
O padding vertical existe para o anel de foco do core (2px a 3px de
distância) não ser cortado pelo overflow do scroller. */
.liq-scroll{
margin-top:22px;
overflow-x:auto;overflow-y:hidden;
overscroll-behavior-x:contain;
padding-block:10px;
}
.liq-plot{
min-width:var(--liq-min);
padding-inline:calc(var(--liq-ficha-w) / 2);
}
/* ---- as fichas ----
min-height cresce uma fila de cada vez, escrita pelo script em --liq-filas.
O padrão 1 no var() é o que mantém a régua com a altura certa sem
JavaScript, onde ninguém empilha nada. */
.liq-track{
position:relative;
min-height:calc(
var(--liq-ficha-h) + var(--liq-haste)
+ (var(--liq-filas,1) - 1) * var(--liq-fila-h)
);
transition:min-height .3s var(--ease);
}
.liq-ficha{
position:absolute;
left:calc(var(--liq-ano) / var(--liq-anos) * 100%);
bottom:calc(var(--liq-haste) + var(--liq-fila,0) * var(--liq-fila-h));
/* A fila 0 é a mais próxima da régua e tem de cobrir a haste das filas de
cima, que a atravessam para chegar à linha. Por isso o z-index DESCE
conforme a fila sobe. */
z-index:calc(9 - var(--liq-fila,0));
width:var(--liq-ficha-w);height:var(--liq-ficha-h);
transform:translateX(-50%);
display:flex;flex-direction:column;
padding:10px 11px;
border:1px solid var(--rule-2);
border-radius:var(--radius-media);
background:color-mix(in srgb,var(--fg) 6%,var(--bg));
/* pan-y deixa a página rolar no toque vertical: o componente só toma para
si o gesto horizontal. */
touch-action:pan-y;
user-select:none;-webkit-user-select:none;
transition:left .24s var(--ease),bottom .3s var(--ease),
border-color .3s var(--ease),background-color .3s var(--ease);
}
.liq-ficha:hover{border-color:color-mix(in srgb,var(--accent) 55%,transparent)}
/* A haste desce da ficha até a régua. Fica atrás da própria ficha
(z-index:-1 dentro do contexto que o transform da ficha cria) e atrás das
fichas das filas de baixo, pela regra de z-index acima. */
.liq-ficha::after{
content:"";position:absolute;left:50%;top:100%;
width:1px;height:calc(var(--liq-haste) + var(--liq-fila,0) * var(--liq-fila-h));
background:var(--rule-2);z-index:-1;
}
.liq-ficha.is-dragging{
transition:none;z-index:20;
border-color:var(--accent);
background:color-mix(in srgb,var(--fg) 10%,var(--bg));
}
.liq-ficha.is-dragging::after{background:var(--accent)}
/* Só promete o gesto quando o script está de pé: sem ele a ficha não arrasta
e uma mão aberta de cursor mentiria. */
.liq-quadro.is-live .liq-ficha{cursor:grab}
.liq-quadro.is-live .liq-ficha.is-dragging{cursor:grabbing}
@media(prefers-reduced-motion:reduce){
/* Reposicionar sem transição: o arrasto continua (é gesto pedido), mas a
ficha e a régua deixam de deslizar sozinhas depois do encaixe. */
.liq-ficha,.liq-track{transition:none}
}
.liq-ficha-topo{display:flex;align-items:center;justify-content:space-between;gap:8px;line-height:1.2}
.liq-icone{
width:16px;height:16px;flex:0 0 16px;
fill:none;stroke:var(--accent);stroke-width:1.2;
stroke-linecap:round;stroke-linejoin:round;
}
.liq-ficha-ano{
font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
color:var(--fg-muted);white-space:nowrap;
}
.liq-ficha-rot{margin-top:8px;font-size:12px;line-height:1.28;color:var(--fg)}
.liq-ficha-val{
margin-top:auto;
font-family:var(--display);font-weight:700;font-size:14px;line-height:1.25;
letter-spacing:-.01em;color:var(--fg-2);
}
/* ---- a régua ---- */
.liq-rail{position:relative;height:30px;border-top:1px solid var(--rule-2)}
.liq-marca{
position:absolute;top:0;
left:calc(var(--liq-ano) / var(--liq-anos) * 100%);
transform:translateX(-50%);
}
.liq-marca::before{
content:"";position:absolute;left:50%;top:0;
width:1px;height:7px;background:var(--rule-2);
}
.liq-marca-txt{
display:block;margin-top:12px;
font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
color:var(--fg-muted);white-space:nowrap;
}
/* ---- a área ----
O SVG é decorativo (aria-hidden): o dado está nas fichas e nas leituras.
A máscara é o degradê que o gradiente de preenchimento daria — e sem ela
seria preciso um <linearGradient> com id, que o contrato não quer. */
.liq-graf{
display:block;width:100%;height:var(--liq-graf-h);margin-top:16px;
mask-image:linear-gradient(to top,#000 40%,rgba(0,0,0,.55) 100%);
-webkit-mask-image:linear-gradient(to top,#000 40%,rgba(0,0,0,.55) 100%);
}
.liq-grade{fill:none;stroke:var(--rule);stroke-width:1}
.liq-area{fill:color-mix(in srgb,var(--accent) 20%,transparent);stroke:none}
/* pathLength="1" normaliza o traço: o dash não depende do comprimento real
do caminho, que muda a cada arrasto. */
.liq-linha{
fill:none;stroke:var(--accent);stroke-width:1.5;
stroke-linejoin:round;
stroke-dasharray:1;stroke-dashoffset:0;
transition:stroke-dashoffset 1.5s var(--ease);
}
/* A entrada é DELEGADA ao data-rv do quadro — o componente não escreve
observer próprio. Escondido só enquanto houver um data-rv que ainda não
entrou: quem tirar o data-rv do .liq-quadro fica com o traço pronto pela
regra acima, em vez de uma linha que nunca aparece. */
[data-rv] .liq-linha{stroke-dashoffset:1}
[data-rv].in .liq-linha{stroke-dashoffset:0}
@media(prefers-reduced-motion:reduce){
/* A forma nasce pronta: sem o traço se desenhando na entrada. */
.liq-linha{stroke-dashoffset:0;transition:none}
}
.liq-eixo-nota{
margin-top:12px;
font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;
color:var(--fg-muted);
}
/* ---- as leituras ---- */
.liq-saida{
display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);
gap:clamp(24px,3vw,52px);
margin-top:clamp(26px,3vh,38px);
padding-top:clamp(22px,2.6vh,30px);
border-top:1px solid var(--rule);
}
.liq-saida-tit{
font-family:var(--sans);font-weight:400;
font-size:11px;letter-spacing:.18em;text-transform:uppercase;
color:var(--fg-muted);
}
.liq-leituras{display:grid;gap:15px;margin-top:18px}
.liq-leitura{
display:grid;grid-template-columns:auto minmax(0,1fr);
column-gap:14px;align-items:center;
}
.liq-leitura-num{
grid-column:1;grid-row:1 / 3;
font-family:var(--display);font-weight:700;
font-size:27px;letter-spacing:-.03em;line-height:1;
color:var(--fg);min-width:3.4ch;text-align:right;
}
.liq-leitura-rot{grid-column:2;grid-row:1;font-size:13.5px;line-height:1.3;color:var(--fg-2)}
.liq-leitura-barra{
grid-column:2;grid-row:2;
position:relative;height:2px;margin-top:9px;background:var(--rule);
}
.liq-leitura-barra::after{
content:"";position:absolute;inset:0 auto 0 0;
width:calc(var(--liq-frac,0) * 100%);
background:var(--accent);
transition:width .3s var(--ease);
}
@media(prefers-reduced-motion:reduce){
/* A barra acompanha o arrasto sem deslizar: o valor aparece direto. */
.liq-leitura-barra::after{transition:none}
}
/* Tipografia pela primitiva .nota do core; 48ch porque a ressalva mora na
coluna estreita das leituras, ao lado da régua. */
.liq-nota{--nota-max:48ch}
.liq-btn{margin-top:22px}
/* ==========================================================================
TELAS ESTREITAS
A régua ROLA DE LADO, não comprime. Comprimir 30 anos nos 320px úteis de
um celular poria as cinco fichas a menos de 55px umas das outras — todas
colidiriam, todas subiriam de fila, e a régua viraria uma pilha vertical
de 592px onde a posição no tempo, que é a única coisa que a peça ensina,
deixaria de ser legível. Rolando, cada ano continua valendo os mesmos
pixels que no desktop, e o gesto horizontal já é o gesto da peça.
Medido: a rolagem interna começa por volta de 1010px de janela.
========================================================================== */
/* Faixa de notebook: a ficha encolhe para a régua caber sem rolar de lado.
Sem isto o piso de 920px do gráfico começaria a rolar já em 1130px de
janela — 60px de rolagem interna, o suficiente para parecer defeito. */
@media(max-width:1180px){
.liq-quadro{--liq-ficha-w:110px;--liq-min:840px}
}
@media(max-width:860px){
.liq-saida{grid-template-columns:minmax(0,1fr);gap:26px}
.liq-quadro{padding:18px 14px}
.liq-nota{--nota-max:none}
}
@media(max-width:560px){
.liq-quadro{--liq-graf-h:112px}
.liq-leitura-num{font-size:23px}
.liq-leitura-rot{font-size:13px}
.liq-btn{width:100%;justify-content:center}
}
script.js · 342 linhas · 13,2 KB
/* ==========================================================================
RÉGUA DE LIQUIDEZ — arrasto, teclado, empilhamento e redesenho da área
O que o script faz, em ordem de importância:
1. move a ficha no eixo do tempo (ponteiro ou teclado) e a encaixa no
ano inteiro mais próximo;
2. redesenha o `d` dos dois caminhos da área a cada mudança;
3. recalcula as três leituras de horizonte;
4. desempilha em filas as fichas que ficariam sobrepostas.
Sem ele a peça já está de pé: a posição das fichas, o `d` da área e os três
percentuais vêm escritos no markup, calculados por esta mesma conta.
---- A FORMA DA ÁREA, e por que ela não é o valor puro -------------------
A altura não é o valor do objetivo: é o valor DIVIDIDO PELO TEMPO que
existe para prepará-lo. R$ 900 mil daqui a cinco anos exige mais liquidez
agora do que R$ 1,8 milhão daqui a vinte e sete — e é exatamente isso que
a peça precisa ensinar.
janela de subida(ano) = min(subidaBase + subidaFator × ano, subidaTeto)
janela de descida = descida
altura(objetivo) = valor / (0,5 × (subida + descida))
Cada objetivo vira um sino de cosseno levantado sobre essas duas janelas,
e a área é a soma dos cinco. O eixo y é normalizado pelo próprio máximo:
é FORMA, não escala — não há número no eixo, e a peça não afirma nenhum.
---- COMPLIANCE ---------------------------------------------------------
Nada aqui projeta rentabilidade. As três leituras são divisão simples dos
valores que a pessoa posicionou, por faixa de prazo. Nenhum dado sai do
navegador, nada vai para localStorage, nada é coletado.
========================================================================== */
(function () {
var raizes = document.querySelectorAll('[data-liq]');
if (!raizes.length) return;
/* ---- Tabela de ajuste ---------------------------------------------------
Os VALORES de cada objetivo não moram aqui: moram no markup, em
data-liq-valor, porque sem JavaScript a régua já precisa mostrar as
leituras certas — duas cópias do mesmo número divergiriam no primeiro
ajuste. Aqui fica a matemática da curva e o corte dos horizontes. */
var AJUSTE = {
subidaBase: 1.2, /* anos de rampa antes de um objetivo no ano 0 */
subidaFator: 0.28, /* anos de rampa somados por ano de horizonte */
subidaTeto: 6, /* teto da rampa, em anos */
descida: 1.4, /* anos de queda depois do objetivo */
curto: 1, /* até N anos = "próximos 12 meses" */
medio: 5, /* até N anos = "médio prazo" */
passo: 0.5, /* amostragem da área, em anos (61 pontos em 30 anos) */
topo: 92, /* altura do pico, em unidades do viewBox de 100 */
folga: 8, /* px mínimos entre duas fichas da mesma fila */
limiar: 6, /* px de ponteiro que separam clique de arrasto */
encaixe: 200 /* ms da animação de encaixe no ano inteiro */
};
var ANOS = 30; /* fim da régua — o mesmo --liq-anos do CSS */
function reduzido() {
if (window.LIB && typeof window.LIB.reduce === 'boolean') return window.LIB.reduce;
return matchMedia('(prefers-reduced-motion: reduce)').matches &&
location.search.indexOf('motion=on') < 0;
}
function limitar(v, a, b) { return v < a ? a : (v > b ? b : v); }
function subida(ano) {
var s = AJUSTE.subidaBase + AJUSTE.subidaFator * ano;
return s > AJUSTE.subidaTeto ? AJUSTE.subidaTeto : s;
}
function altura(o) { return o.valor / (0.5 * (subida(o.ano) + AJUSTE.descida)); }
function nucleo(t, ano) {
var u = t <= ano ? (ano - t) / subida(ano) : (t - ano) / AJUSTE.descida;
if (u >= 1) return 0;
return 0.5 * (1 + Math.cos(Math.PI * u));
}
/* Monta os dois `d`: o traço de cima e a área fechada no rodapé. */
function caminhos(itens) {
var pts = [], t, i, v, max = 0;
for (t = 0; t <= ANOS + 1e-9; t += AJUSTE.passo) {
v = 0;
for (i = 0; i < itens.length; i++) v += altura(itens[i]) * nucleo(t, itens[i].ano);
if (v > max) max = v;
pts.push([t * 10, v]);
}
if (max <= 0) max = 1;
var d = '';
for (i = 0; i < pts.length; i++) {
d += (i ? 'L' : 'M') + (Math.round(pts[i][0] * 10) / 10) + ' ' +
(Math.round((100 - pts[i][1] / max * AJUSTE.topo) * 10) / 10);
}
var fim = 'L300 100';
return { linha: d, area: d + (d.slice(-fim.length) === fim ? '' : fim) + 'L0 100Z' };
}
/* Três faixas de horizonte, em percentual do total posicionado.
O arredondamento é por MAIOR RESTO: três arredondamentos independentes
fechariam em 99 ou 101, e a soma é justamente o que dá credibilidade à
leitura. Medido: 5,17 / 19,40 / 75,43 sai 5 / 19 / 76. */
function leituras(itens) {
var tot = 0, faixa = [0, 0, 0], i, ano;
for (i = 0; i < itens.length; i++) tot += itens[i].valor;
if (tot <= 0) return [0, 0, 0];
for (i = 0; i < itens.length; i++) {
ano = Math.round(itens[i].ano);
faixa[ano <= AJUSTE.curto ? 0 : (ano <= AJUSTE.medio ? 1 : 2)] += itens[i].valor;
}
var brutos = [], pisos = [], soma = 0, p;
for (i = 0; i < 3; i++) {
p = faixa[i] / tot * 100;
brutos.push(p);
pisos.push(Math.floor(p));
soma += Math.floor(p);
}
var ordem = [0, 1, 2].sort(function (x, y) {
return (brutos[y] - pisos[y]) - (brutos[x] - pisos[x]);
});
var falta = 100 - soma;
for (i = 0; i < falta; i++) pisos[ordem[i % 3]] += 1;
return pisos;
}
function textoAno(ano) {
var a = Math.round(ano);
if (a <= 0) return 'hoje';
return a === 1 ? 'em 1 ano' : 'em ' + a + ' anos';
}
function iniciar(raiz) {
var track = raiz.querySelector('.liq-track');
var area = raiz.querySelector('.liq-area');
var traco = raiz.querySelector('.liq-linha');
var lis = raiz.querySelectorAll('.liq-leitura');
var nos = raiz.querySelectorAll('.liq-ficha');
if (!track || !area || !traco || lis.length !== 3 || nos.length < 2) return;
var reduz = reduzido();
var itens = [], i, el;
for (i = 0; i < nos.length; i++) {
el = nos[i];
itens.push({
el: el,
anoEl: el.querySelector('.liq-ficha-ano'),
valor: parseFloat(el.getAttribute('data-liq-valor')) || 0,
ano: parseFloat(el.style.getPropertyValue('--liq-ano')) || 0,
fila: 0
});
el.setAttribute('tabindex', '0');
}
/* A frase que promete o gesto só aparece agora: sem script ela mentiria. */
var ajuda = raiz.querySelector('.liq-ajuda-js');
if (ajuda) ajuda.hidden = false;
raiz.classList.add('is-live');
function daFicha(el) {
for (var j = 0; j < itens.length; j++) if (itens[j].el === el) return itens[j];
return null;
}
function escrever(it) {
it.el.style.setProperty('--liq-ano', it.ano);
if (!it.anoEl) return;
/* Só toca no texto quando o ano inteiro muda: o elemento é aria-live, e
reescrevê-lo a cada quadro do arrasto inundaria o leitor de tela. */
var t = textoAno(it.ano);
if (it.anoEl.textContent !== t) it.anoEl.textContent = t;
}
function pintar() {
var c = caminhos(itens);
area.setAttribute('d', c.area);
traco.setAttribute('d', c.linha);
var p = leituras(itens), k, num;
for (k = 0; k < 3; k++) {
num = lis[k].querySelector('.liq-leitura-pct');
if (num) num.textContent = p[k];
lis[k].style.setProperty('--liq-frac', (p[k] / 100).toFixed(3));
}
}
/* Desempilhamento guloso, da esquerda para a direita: cada ficha desce
para a fila mais baixa em que ninguém a alcança. Duas fichas no mesmo
ano têm distância zero, logo caem sempre em filas diferentes. */
function empilhar() {
var largura = track.clientWidth || 1;
var passo = (itens[0].el.offsetWidth || 122) + AJUSTE.folga;
var ordem = itens.slice().sort(function (a, b) { return a.ano - b.ano; });
var filas = [], maior = 1, j, k, f, x, livre;
for (j = 0; j < ordem.length; j++) {
x = ordem[j].ano / ANOS * largura;
f = 0;
while (true) {
if (!filas[f]) filas[f] = [];
livre = true;
for (k = 0; k < filas[f].length; k++) {
if (Math.abs(x - filas[f][k]) < passo) { livre = false; break; }
}
if (livre) break;
f++;
}
filas[f].push(x);
ordem[j].fila = f;
if (f + 1 > maior) maior = f + 1;
}
for (j = 0; j < itens.length; j++) {
itens[j].el.style.setProperty('--liq-fila', itens[j].fila);
}
track.style.setProperty('--liq-filas', maior);
}
/* ---- o único rAF da instância ----
Ele dorme sempre que não há encaixe em curso: arrasto e teclado pedem
um quadro, o quadro faz o trabalho e devolve o laço ao repouso. */
var pedido = 0, ultimo = 0, encaixe = null, refila = false;
function pedir() { if (!pedido) pedido = requestAnimationFrame(passo); }
function passo(agora) {
pedido = 0;
var dt = ultimo ? Math.min(agora - ultimo, 50) : 16;
ultimo = agora;
var j, u, continua = false;
if (encaixe) {
encaixe.t += dt;
u = encaixe.t / AJUSTE.encaixe;
if (u >= 1) { encaixe.it.ano = encaixe.para; encaixe = null; refila = true; }
else {
encaixe.it.ano = encaixe.de + (encaixe.para - encaixe.de) * (1 - Math.pow(1 - u, 3));
continua = true;
}
}
for (j = 0; j < itens.length; j++) escrever(itens[j]);
pintar();
if (refila && !continua) { refila = false; empilhar(); }
if (continua) pedir();
else ultimo = 0;
}
function mandarPara(it, destino) {
destino = limitar(Math.round(destino), 0, ANOS);
/* Movimento reduzido: nada desliza sozinho — a ficha aparece no ano. */
if (reduz || Math.abs(destino - it.ano) < 0.02) {
it.ano = destino; refila = true; pedir(); return;
}
encaixe = { it: it, de: it.ano, para: destino, t: 0 };
pedir();
}
/* ---- ponteiro ----
A captura é pedida já no pointerdown, e não depois do limiar: sem ela
um movimento rápido que sai da caixa de 122px perde os eventos no meio
do gesto. O limiar de 6px decide só se aquilo virou arrasto. */
var arrasto = null;
function aoDescer(e) {
if (e.button > 0) return;
var it = daFicha(e.currentTarget);
if (!it) return;
if (encaixe && encaixe.it === it) encaixe = null;
arrasto = { it: it, id: e.pointerId, x0: e.clientX, ano0: it.ano, ativo: false };
try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
}
function aoMover(e) {
if (!arrasto || e.pointerId !== arrasto.id) return;
var dx = e.clientX - arrasto.x0;
if (!arrasto.ativo) {
if (Math.abs(dx) < AJUSTE.limiar) return;
arrasto.ativo = true;
arrasto.it.el.classList.add('is-dragging');
}
arrasto.it.ano = limitar(arrasto.ano0 + dx / (track.clientWidth || 1) * ANOS, 0, ANOS);
pedir();
}
function aoSubir(e) {
if (!arrasto || e.pointerId !== arrasto.id) return;
var it = arrasto.it, houve = arrasto.ativo;
try { it.el.releasePointerCapture(e.pointerId); } catch (err) {}
arrasto = null;
it.el.classList.remove('is-dragging');
if (houve) mandarPara(it, it.ano);
}
/* pointercancel = o navegador levou o gesto (rolagem vertical no toque).
Devolver a ficha ao ano de partida é mais honesto do que deixá-la onde
o dedo passou de raspão. */
function aoCancelar(e) {
if (!arrasto || e.pointerId !== arrasto.id) return;
var it = arrasto.it, volta = arrasto.ano0, houve = arrasto.ativo;
arrasto = null;
it.el.classList.remove('is-dragging');
if (!houve) return;
it.ano = volta;
refila = true;
pedir();
}
function aoTeclar(e) {
var it = daFicha(e.currentTarget);
if (!it) return;
var base = Math.round(it.ano), destino = null, k = e.key;
if (k === 'ArrowLeft' || k === 'ArrowDown' || k === 'Left' || k === 'Down') destino = base - 1;
else if (k === 'ArrowRight' || k === 'ArrowUp' || k === 'Right' || k === 'Up') destino = base + 1;
else if (k === 'PageDown') destino = base - 5;
else if (k === 'PageUp') destino = base + 5;
else if (k === 'Home') destino = 0;
else if (k === 'End') destino = ANOS;
if (destino === null) return;
/* Sem isto a seta rola a página de lado em vez de mover a ficha. */
e.preventDefault();
mandarPara(it, destino);
}
function semArrastoNativo(e) { e.preventDefault(); }
for (i = 0; i < itens.length; i++) {
el = itens[i].el;
el.addEventListener('pointerdown', aoDescer);
el.addEventListener('pointermove', aoMover);
el.addEventListener('pointerup', aoSubir);
el.addEventListener('pointercancel', aoCancelar);
el.addEventListener('keydown', aoTeclar);
el.addEventListener('dragstart', semArrastoNativo);
}
/* A largura da régua muda com a janela, e a colisão é medida em px. */
addEventListener('resize', function () { refila = true; pedir(); }, { passive: true });
empilhar();
pintar();
}
for (var r = 0; r < raizes.length; r++) iniciar(raizes[r]);
})();