primocode 10.16.0 → 10.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +28 -8
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +36 -8
- package/lib/catalogo.js +4 -0
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto.js +45 -4
- package/lib/estudio/index.js +8 -5
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +61 -7
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +60 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/local/cerebro.json +192 -6
- package/lib/local/motor.js +5 -2
- package/lib/mcp.js +105 -7
- package/lib/nuvem.js +11 -1
- package/lib/outra-pasta.js +1 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +29 -12
- package/lib/protecao.js +134 -0
- package/lib/sandbox.js +11 -4
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +3 -0
- package/lib/ui.js +1 -1
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/app.py +43 -4
- package/voz/primo_voz/chat.html +17 -6
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR"><head><meta charset="UTF-8"/><title>k2d · esquema que cresce com a fala</title></head>
|
|
3
|
+
<body>
|
|
4
|
+
<!--
|
|
5
|
+
Kit 2D (b): um esquema que vai ganhando partes enquanto a pessoa explica. Cada parte entra na palavra dela, já
|
|
6
|
+
no lugar onde fica com o esquema completo (o espaço de todas é reservado desde o começo: nada se reorganiza), e
|
|
7
|
+
as anteriores ficam paradas. A cor diz a função da parte e não muda. Seta só na palavra em que a ligação é
|
|
8
|
+
dita. No exemplo, acende só o trecho por onde o exemplo passa, no próprio esquema, e depois apaga. No fim,
|
|
9
|
+
o esquema inteiro fica parado, sem nada novo, pelo tempo de "final".
|
|
10
|
+
Serve para etapas de um processo, categorias de um assunto, causas que levam a um efeito.
|
|
11
|
+
Guia: references/animacao.md ("Kit 2D"). Para usar: copie para compositions/cenas/<nome>.html, troque
|
|
12
|
+
"k2d_esquema" (template, raiz e ID) e o prefixo "k2dq-", mude os PARÂMETROS e ligue no projeto.json em
|
|
13
|
+
"cenas_proprias".
|
|
14
|
+
Fora do 9:16, a raiz leva o tamanho do vídeo (data-width="1920" data-height="1080" no 16:9); o check
|
|
15
|
+
acusa se esquecer.
|
|
16
|
+
-->
|
|
17
|
+
<template id="k2d_esquema-template">
|
|
18
|
+
<style>
|
|
19
|
+
@font-face{font-family:Montserrat;src:url("assets/fontes/Montserrat.ttf") format("truetype");font-weight:100 900;font-display:block}
|
|
20
|
+
#root{position:absolute;inset:0;overflow:hidden;font-family:Montserrat}
|
|
21
|
+
#k2dq-fundo{position:absolute;inset:0;opacity:0}
|
|
22
|
+
#k2dq-palco{position:absolute;inset:0;opacity:0}
|
|
23
|
+
#k2dq-setas{position:absolute;left:0;top:0;overflow:visible}
|
|
24
|
+
.k2dq-halo{position:absolute;opacity:0}
|
|
25
|
+
.k2dq-no{position:absolute;box-sizing:border-box;border-style:solid;display:flex;align-items:center;justify-content:center;opacity:0}
|
|
26
|
+
.k2dq-txt{position:relative;display:block;font-weight:800;text-align:center;line-height:1.12;overflow-wrap:break-word}
|
|
27
|
+
.k2dq-num{position:absolute;display:flex;align-items:center;justify-content:center;border-radius:50%;font-weight:900;color:#fff}
|
|
28
|
+
.k2dq-seta{fill:none;stroke-linecap:round;opacity:0}
|
|
29
|
+
.k2dq-ponta{opacity:0}
|
|
30
|
+
#k2dq-titulo{position:absolute;display:flex;align-items:center;justify-content:center;opacity:0}
|
|
31
|
+
#k2dq-titulo-txt{display:block;font-weight:900;text-align:center;line-height:1.1}
|
|
32
|
+
</style>
|
|
33
|
+
<div id="root" data-composition-id="k2d_esquema" data-width="1080" data-height="1920" data-duration="9">
|
|
34
|
+
<div id="k2dq-fundo"></div>
|
|
35
|
+
<div id="k2dq-palco">
|
|
36
|
+
<div id="k2dq-halos"></div>
|
|
37
|
+
<svg id="k2dq-setas" xmlns="http://www.w3.org/2000/svg" width="1080" height="1920" viewBox="0 0 1080 1920"></svg>
|
|
38
|
+
<div id="k2dq-nos"></div>
|
|
39
|
+
<div id="k2dq-titulo"><span id="k2dq-titulo-txt">O que a IA fez</span></div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<script>
|
|
43
|
+
(function () {
|
|
44
|
+
const ID = "k2d_esquema";
|
|
45
|
+
// ------------------------------------------------------------------ PARÂMETROS (troque aqui)
|
|
46
|
+
// Tempo: número (segundos da cena), "palavra" ou {palavra, depois, padrao}. UMA palavra, procurada na fala do
|
|
47
|
+
// trecho sem acento e pelo começo; "depois" pula as repetições anteriores; "padrao" vale quando a palavra não
|
|
48
|
+
// aparece (cena aberta sozinha no estúdio, transcrição que mudou).
|
|
49
|
+
const PARAM = {
|
|
50
|
+
// a estrutura que a própria fala tem (não force três partes):
|
|
51
|
+
// "etapas": uma depois da outra; a seta liga a parte anterior a esta
|
|
52
|
+
// "grupos": categorias do mesmo nível, sem ordem e sem setas
|
|
53
|
+
// "causa": causas de um lado, o efeito do outro; a seta liga a causa ao efeito
|
|
54
|
+
forma: "etapas",
|
|
55
|
+
titulo: { texto: "O que a IA fez", em: { palavra: "primeiro", padrao: 0.2 } }, // null = sem título
|
|
56
|
+
// as partes, na ordem em que a fala cita. rotulo: 1 a 3 palavras, como foram ditas. funcao: uma chave de
|
|
57
|
+
// "funcoes". em: quando a parte aparece. seta: quando a ligação desta parte é dita (etapas: com a anterior;
|
|
58
|
+
// causa: desta causa com o efeito); null = sem seta
|
|
59
|
+
partes: [
|
|
60
|
+
{ rotulo: "Ouviu tudo", funcao: "faz", em: { palavra: "ouviu", padrao: 0.7 }, seta: null },
|
|
61
|
+
{ rotulo: "Palavra por palavra", funcao: "faz", em: { palavra: "palavra", padrao: 2.1 }, seta: { palavra: "palavra", padrao: 2.1 } },
|
|
62
|
+
{ rotulo: "Achou os erros", funcao: "acha", em: { palavra: "achou", padrao: 3.5 }, seta: { palavra: "achou", padrao: 3.5 } },
|
|
63
|
+
{ rotulo: "Cortou", funcao: "resultado", em: { palavra: "cortou", padrao: 6.7 }, seta: { palavra: "cortou", padrao: 6.7 } },
|
|
64
|
+
],
|
|
65
|
+
efeito: null, // forma "causa": o índice da parte que é o efeito (null = a última)
|
|
66
|
+
// cor com função fixa: nome de cor do estilo (destaque, base, verde, vermelho, tinta) ou "#rrggbb". Mesma
|
|
67
|
+
// função, mesma cor; nunca troque a cor de uma parte no meio
|
|
68
|
+
funcoes: { faz: "destaque", acha: "vermelho", resultado: "verde" },
|
|
69
|
+
numeros: true, // etapas: o número da etapa dentro de cada parte
|
|
70
|
+
// o exemplo: acende o trajeto dele (índices das partes, na ordem) no próprio esquema, sem abrir outro, e apaga
|
|
71
|
+
// depois. ate: quando apaga (null = "aceso" segundos, e sempre a tempo da ação seguinte). null = sem exemplo
|
|
72
|
+
exemplo: { em: { palavra: "pausa", padrao: 5.0 }, caminho: [2], ate: null, aceso: 1.4 },
|
|
73
|
+
corCaminho: "#f2b705", // cor do caminho aceso (amarelo marca-texto), diferente das cores de função
|
|
74
|
+
apaga: 0.7, // opacidade das partes fora do caminho durante o exemplo (1 = não apagam; abaixo de 0,7 o check reclama do contraste)
|
|
75
|
+
final: 2.0, // segundos no fim com o esquema inteiro parado, sem nada novo (antes da saída)
|
|
76
|
+
pausa: 0.5, // tudo parado pelo menos isso depois de cada ação (parte, seta, exemplo)
|
|
77
|
+
direcao: "auto", // etapas: "coluna", "linha" ou "auto" (coluna quando a área não é bem mais larga que alta)
|
|
78
|
+
legenda: true, // tela cheia: deixa livre a faixa da legenda (a cena usa os 70% de cima da zona segura)
|
|
79
|
+
fundo: "auto", // "auto" (papel em tela cheia, transparente na moldura), "papel", "#rrggbb" ou null
|
|
80
|
+
area: null, // [x0, y0, x1, y1] em px; null = área livre da moldura ou a zona segura
|
|
81
|
+
entrada: 0.2,
|
|
82
|
+
saida: 0.25,
|
|
83
|
+
};
|
|
84
|
+
// ------------------------------------------------------------------ dados do build.py (padrões para abrir sozinha)
|
|
85
|
+
const C = Object.assign({ palavras: [], duracao: 9, largura: 1080, altura: 1920, seguro: [140, 230, 940, 1530], rosto: null, moldura: null,
|
|
86
|
+
cores: { papel: "#fffdfd", linha: "#e6ddf8", base: "#c7b7ef", destaque: "#665080", tinta: "#352c40", verde: "#2e9e5b", vermelho: "#e5484d" } },
|
|
87
|
+
(window.__cenas || {})[ID] || {});
|
|
88
|
+
const DUR = C.duracao, U = Math.min(C.largura, C.altura) / 1080;
|
|
89
|
+
// a raiz precisa do tamanho do vídeo: em 16:9 (ou outro formato) troque data-width e data-height da raiz, senão a cena sai cortada
|
|
90
|
+
const raiz = document.getElementById("k2dq-fundo").parentElement;
|
|
91
|
+
if (raiz.offsetWidth && (Math.abs(raiz.offsetWidth - C.largura) > 2 || Math.abs(raiz.offsetHeight - C.altura) > 2))
|
|
92
|
+
console.error(`[${ID}] a raiz da cena tem ${raiz.offsetWidth} x ${raiz.offsetHeight} e o vídeo ${C.largura} x ${C.altura}: troque data-width e data-height da raiz`);
|
|
93
|
+
// ------------------------------------------------------------------ ajudantes do kit 2D (iguais em todas as cenas do kit; não precisa mexer)
|
|
94
|
+
const sem = (s) => String(s).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
95
|
+
const palavra = (txt, depois, padrao) => { const a = sem(txt), p = a && (C.palavras || []).find((w) => w.t >= depois - 1e-3 && sem(w.texto).startsWith(a)); return p ? p.t : padrao; };
|
|
96
|
+
const momento = (m, padrao) => (typeof m === "number" ? m : typeof m === "string" ? palavra(m, 0, padrao)
|
|
97
|
+
: m && typeof m === "object" ? palavra(m.palavra || "", m.depois == null ? 0 : momento(m.depois, 0), m.padrao == null ? padrao : m.padrao) : padrao);
|
|
98
|
+
// fila: uma ação de cada vez; a próxima espera a anterior terminar e ficar parada PARAM.pausa segundos
|
|
99
|
+
let fimUltima = C.moldura ? Math.max(0, C.moldura.fechada_em || 0) : 0, livre = fimUltima + PARAM.entrada;
|
|
100
|
+
function agenda(quer, dur, colada) {
|
|
101
|
+
const t = Math.max(quer, colada ? fimUltima : livre);
|
|
102
|
+
if (t - quer > 0.4) console.warn(`[${ID}] uma ação entrou ${(t - quer).toFixed(2)} s depois da palavra: afaste as palavras ou diminua "pausa"`);
|
|
103
|
+
fimUltima = t + dur; livre = fimUltima + PARAM.pausa; return t;
|
|
104
|
+
}
|
|
105
|
+
const cor = (n) => C.cores[n] || n;
|
|
106
|
+
const rgb = (h) => { h = String(h).replace("#", ""); if (h.length === 3) h = h.replace(/./g, "$&$&"); return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)); };
|
|
107
|
+
const mistura = (a, b, k) => "#" + rgb(a).map((v, i) => Math.round(v + (rgb(b)[i] - v) * k).toString(16).padStart(2, "0")).join("");
|
|
108
|
+
// texto: largura estimada da Montserrat até o peso 900, com folga (a fonte ainda não carregou quando a cena monta), quebra e o maior tamanho que cabe
|
|
109
|
+
const peso = (ch) => { const b = ch.normalize("NFD")[0]; return b === " " ? 0.28 : "MW".includes(b) ? 1 : "mw".includes(b) ? 0.92 : "iljI.,:;'!|".includes(b) ? 0.32 : /[A-Z]/.test(b) ? 0.76 : /[0-9]/.test(b) ? 0.68 : 0.62; };
|
|
110
|
+
const larg = (t, px) => [...String(t)].reduce((s, ch) => s + peso(ch), 0) * px * 1.1;
|
|
111
|
+
const linhas = (t, px, w) => String(t).split(/\s+/).filter(Boolean).reduce((ls, p) => { const u = ls[ls.length - 1]; if (u !== undefined && larg(u + " " + p, px) <= w) ls[ls.length - 1] = u + " " + p; else ls.push(p); return ls; }, []);
|
|
112
|
+
const cabe = (t, w, h, maior, menor, max) => { for (let px = Math.round(maior); px > menor; px--) { const ls = linhas(t, px, w); if (ls.length <= max && ls.every((l) => larg(l, px) <= w) && ls.length * px * 1.16 <= h) return px; } return Math.round(menor); };
|
|
113
|
+
// onde a cena fica: PARAM.area, a área livre da moldura ou a zona segura (sem a faixa da legenda, com PARAM.legenda)
|
|
114
|
+
const A = PARAM.area ? PARAM.area.slice() : C.moldura && C.moldura.area_livre ? C.moldura.area_livre.slice()
|
|
115
|
+
: [C.seguro[0], C.seguro[1], C.seguro[2], PARAM.legenda ? Math.round(C.seguro[1] + (C.seguro[3] - C.seguro[1]) * 0.7) : C.seguro[3]];
|
|
116
|
+
const AW = A[2] - A[0], AH = A[3] - A[1];
|
|
117
|
+
const fundo = PARAM.fundo === "auto" ? (C.moldura ? null : "papel") : PARAM.fundo;
|
|
118
|
+
function pintaFundo(el) {
|
|
119
|
+
if (fundo === "papel" && C.fundo_css) el.style.cssText += ";" + C.fundo_css; // estilo escuro: o fundo do próprio estilo (grade e brilho)
|
|
120
|
+
else if (fundo === "papel") { const g = Math.round(60 * U); Object.assign(el.style, { backgroundColor: C.cores.papel, backgroundSize: `${g}px ${g}px`, backgroundImage: `linear-gradient(${C.cores.linha} 2px,transparent 2px),linear-gradient(90deg,${C.cores.linha} 2px,transparent 2px)` }); }
|
|
121
|
+
else if (fundo) el.style.backgroundColor = fundo;
|
|
122
|
+
}
|
|
123
|
+
const novo = (tag, id, cls, pai, estilo, texto) => { const e = document.createElement(tag); if (id) e.id = id; if (cls) e.className = cls; if (estilo) Object.assign(e.style, estilo); if (texto != null) e.textContent = texto; pai.appendChild(e); return e; };
|
|
124
|
+
const px = (v) => `${v}px`;
|
|
125
|
+
// letra sobre uma cor: branca, como sempre; só num estilo escuro (tinta clara) a cor clara leva letra escura (o papel)
|
|
126
|
+
const claro = (h) => { const [r, g, b] = rgb(h); return 0.299 * r + 0.587 * g + 0.114 * b > 150; };
|
|
127
|
+
const sobre = (fundo) => (claro(C.cores.tinta) && claro(fundo) ? C.cores.papel : "#ffffff");
|
|
128
|
+
|
|
129
|
+
// ------------------------------------------------------------------ lugar final de cada parte (reservado desde o começo)
|
|
130
|
+
const N = PARAM.partes.length, emPe = AW < AH * 1.25;
|
|
131
|
+
const efeito = PARAM.forma === "causa" ? (PARAM.efeito == null ? N - 1 : PARAM.efeito) : -1;
|
|
132
|
+
const titH = PARAM.titulo ? Math.round(Math.min(120 * U, AH * 0.14)) : 0;
|
|
133
|
+
const topo = A[1] + (titH ? titH + 24 * U : 0), altura = A[3] - topo;
|
|
134
|
+
const caixas = new Array(N).fill(null), pares = []; // pares: setas [de, para, parte que diz a ligação]
|
|
135
|
+
const grade = (lista, cols, w, h, gap, y0) => lista.forEach((i, k) => {
|
|
136
|
+
const r = Math.floor(k / cols), naLinha = Math.min(cols, lista.length - r * cols), x0 = A[0] + (AW - (naLinha * w + (naLinha - 1) * gap)) / 2;
|
|
137
|
+
caixas[i] = { x: x0 + (k - r * cols) * (w + gap), y: y0 + r * (h + gap), w, h };
|
|
138
|
+
});
|
|
139
|
+
const todas = PARAM.partes.map((_, i) => i);
|
|
140
|
+
if (PARAM.forma === "etapas") {
|
|
141
|
+
// "auto": coluna na área em pé, linha na deitada; quando a linha fica estreita (1:1, 4:5, moldura) ou a
|
|
142
|
+
// coluna baixa demais, duas colunas (a leitura segue da esquerda para a direita, de cima para baixo)
|
|
143
|
+
const gapL = Math.min(90 * U, AW * 0.06), wLinha = (AW - (N - 1) * gapL) / N, hColuna = (altura - (N - 1) * 56 * U) / N;
|
|
144
|
+
let modo = PARAM.direcao === "coluna" || (PARAM.direcao === "auto" && emPe) ? "coluna" : "linha";
|
|
145
|
+
if (PARAM.direcao === "auto" && N > 2 && ((modo === "linha" && wLinha < 300 * U) || hColuna < 96 * U)) modo = hColuna >= 96 * U ? "coluna" : "grade";
|
|
146
|
+
if (modo === "grade") {
|
|
147
|
+
const rows = Math.ceil(N / 2), gap = Math.min(84 * U, AW * 0.08), w = (AW - gap) / 2, h = Math.min(170 * U, (altura - (rows - 1) * gap) / rows);
|
|
148
|
+
grade(todas, 2, w, h, gap, topo + (altura - (rows * h + (rows - 1) * gap)) / 2);
|
|
149
|
+
} else if (modo === "coluna") {
|
|
150
|
+
let gap = 84 * U, h = Math.min(170 * U, (altura - (N - 1) * gap) / N);
|
|
151
|
+
if (h < 96 * U) { gap = 56 * U; h = (altura - (N - 1) * gap) / N; }
|
|
152
|
+
grade(todas, 1, Math.min(AW, 680 * U), h, gap, topo + (altura - (N * h + (N - 1) * gap)) / 2);
|
|
153
|
+
} else {
|
|
154
|
+
const gap = Math.min(90 * U, AW * 0.06), w = (AW - (N - 1) * gap) / N, h = Math.min(w * 0.7, 240 * U, altura);
|
|
155
|
+
grade(todas, N, w, h, gap, topo + (altura - h) / 2);
|
|
156
|
+
}
|
|
157
|
+
for (let i = 1; i < N; i++) if (PARAM.partes[i].seta) pares.push([i - 1, i, i]);
|
|
158
|
+
} else if (PARAM.forma === "grupos") {
|
|
159
|
+
const cols = emPe ? Math.min(N, 2) : Math.min(N, 4), rows = Math.ceil(N / cols), gap = 36 * U;
|
|
160
|
+
const w = (AW - (cols - 1) * gap) / cols, h = Math.min(200 * U, (altura - (rows - 1) * gap) / rows);
|
|
161
|
+
grade(todas, cols, w, h, gap, topo + (altura - (rows * h + (rows - 1) * gap)) / 2);
|
|
162
|
+
} else {
|
|
163
|
+
const causas = todas.filter((i) => i !== efeito), nc = causas.length, gap = 30 * U;
|
|
164
|
+
if (emPe) {
|
|
165
|
+
const cols = nc <= 3 ? nc : 2, rows = Math.ceil(nc / cols), w = (AW - (cols - 1) * gap) / cols, vao = Math.max(110 * U, altura * 0.16);
|
|
166
|
+
const h = Math.min(170 * U, (altura - vao - (rows - 1) * gap) / (rows + 1.1)), he = h * 1.1, we = Math.min(AW, 600 * U);
|
|
167
|
+
const bloco = rows * h + (rows - 1) * gap + vao + he, y0 = topo + (altura - bloco) / 2;
|
|
168
|
+
grade(causas, cols, w, h, gap, y0);
|
|
169
|
+
caixas[efeito] = { x: A[0] + (AW - we) / 2, y: y0 + bloco - he, w: we, h: he };
|
|
170
|
+
} else {
|
|
171
|
+
const wc = AW * 0.4, we = AW * 0.34, h = Math.min(160 * U, (altura - (nc - 1) * gap) / nc), he = Math.min(altura, h * 1.4);
|
|
172
|
+
const y0 = topo + (altura - (nc * h + (nc - 1) * gap)) / 2;
|
|
173
|
+
causas.forEach((ic, k) => { caixas[ic] = { x: A[0], y: y0 + k * (h + gap), w: wc, h }; });
|
|
174
|
+
caixas[efeito] = { x: A[2] - we, y: topo + (altura - he) / 2, w: we, h: he };
|
|
175
|
+
}
|
|
176
|
+
causas.forEach((ic) => { if (PARAM.partes[ic].seta) pares.push([ic, efeito, ic]); });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ------------------------------------------------------------------ monta os elementos
|
|
180
|
+
const fundoEl = document.getElementById("k2dq-fundo");
|
|
181
|
+
pintaFundo(fundoEl);
|
|
182
|
+
const svg = document.getElementById("k2dq-setas"), svgNS = "http://www.w3.org/2000/svg";
|
|
183
|
+
svg.setAttribute("width", C.largura); svg.setAttribute("height", C.altura); svg.setAttribute("viewBox", `0 0 ${C.largura} ${C.altura}`);
|
|
184
|
+
const raio = 26 * U, borda = Math.max(4, Math.round(6 * U)), comNumero = PARAM.forma === "etapas" && PARAM.numeros;
|
|
185
|
+
const bola = (r) => Math.min(r.h * 0.46, 64 * U);
|
|
186
|
+
// um tamanho de letra só para as partes do mesmo nível (o que cabe na mais apertada); o efeito tem o dele, maior
|
|
187
|
+
const espaco = (r) => r.w - 2 * borda - 52 * U - (comNumero ? bola(r) + 12 * U : 0);
|
|
188
|
+
const cabeNa = (i, maior) => cabe(PARAM.partes[i].rotulo, espaco(caixas[i]), caixas[i].h - 2 * borda - 16 * U, maior, 26 * U, 2);
|
|
189
|
+
const letraEfeito = efeito >= 0 ? cabeNa(efeito, 64 * U) : 0;
|
|
190
|
+
const letra = N > (efeito >= 0 ? 1 : 0) ? Math.min(...PARAM.partes.map((p, i) => (i === efeito ? Infinity : cabeNa(i, 58 * U)))) : letraEfeito;
|
|
191
|
+
const halos = [], nos = [];
|
|
192
|
+
PARAM.partes.forEach((p, i) => {
|
|
193
|
+
const r = caixas[i], c = cor(PARAM.funcoes[p.funcao] || "destaque"), f = 14 * U;
|
|
194
|
+
halos.push(novo("div", `k2dq-halo-${i}`, "k2dq-halo", document.getElementById("k2dq-halos"),
|
|
195
|
+
{ left: px(r.x - f), top: px(r.y - f), width: px(r.w + 2 * f), height: px(r.h + 2 * f), borderRadius: px(raio + f), background: PARAM.corCaminho }));
|
|
196
|
+
const no = novo("div", `k2dq-no-${i}`, "k2dq-no", document.getElementById("k2dq-nos"), { left: px(r.x), top: px(r.y), width: px(r.w), height: px(r.h),
|
|
197
|
+
borderRadius: px(raio), borderWidth: px(borda), borderColor: c, background: mistura(c, C.cores.papel, 0.86), boxShadow: `0 ${Math.round(10 * U)}px 0 -2px ${mistura(c, C.cores.papel, 0.55)}` });
|
|
198
|
+
const d = bola(r), esq = comNumero ? 22 * U + d + 16 * U : 26 * U;
|
|
199
|
+
if (comNumero) novo("span", `k2dq-num-${i}`, "k2dq-num", no, { left: px(22 * U), top: px((r.h - 2 * borda - d) / 2), width: px(d), height: px(d), background: mistura(c, C.cores.tinta, 0.22), color: sobre(mistura(c, C.cores.tinta, 0.22)), fontSize: px(Math.round(d * 0.55)) }, String(i + 1));
|
|
200
|
+
novo("span", `k2dq-txt-${i}`, "k2dq-txt", no, { width: px(espaco(r)), marginLeft: px(esq - 26 * U), fontSize: px(i === efeito ? letraEfeito : letra), color: C.cores.tinta }, p.rotulo);
|
|
201
|
+
nos.push(no);
|
|
202
|
+
});
|
|
203
|
+
if (!PARAM.titulo) document.getElementById("k2dq-titulo").style.display = "none";
|
|
204
|
+
else {
|
|
205
|
+
Object.assign(document.getElementById("k2dq-titulo").style, { left: px(A[0]), top: px(A[1]), width: px(AW), height: px(titH) });
|
|
206
|
+
Object.assign(document.getElementById("k2dq-titulo-txt"), { textContent: PARAM.titulo.texto });
|
|
207
|
+
Object.assign(document.getElementById("k2dq-titulo-txt").style, { color: C.cores.tinta, fontSize: px(cabe(PARAM.titulo.texto, AW, titH, 68 * U, 30 * U, 2)) });
|
|
208
|
+
}
|
|
209
|
+
// seta de uma parte a outra pela linha entre os centros, parando um pouco antes das bordas
|
|
210
|
+
const naBorda = (r, dx, dy) => { const t = Math.min(dx ? r.w / 2 / Math.abs(dx) : Infinity, dy ? r.h / 2 / Math.abs(dy) : Infinity); return [r.x + r.w / 2 + dx * t, r.y + r.h / 2 + dy * t]; };
|
|
211
|
+
function novaSeta(a, b, corLinha, grossura, id) {
|
|
212
|
+
const ra = caixas[a], rb = caixas[b], folga = 8 * U, cab = 30 * U;
|
|
213
|
+
let dx = rb.x + rb.w / 2 - ra.x - ra.w / 2, dy = rb.y + rb.h / 2 - ra.y - ra.h / 2;
|
|
214
|
+
const n = Math.hypot(dx, dy) || 1; dx /= n; dy /= n;
|
|
215
|
+
const p1 = naBorda(ra, dx, dy), p2 = naBorda(rb, -dx, -dy), x1 = p1[0] + dx * folga, y1 = p1[1] + dy * folga, x2 = p2[0] - dx * folga, y2 = p2[1] - dy * folga;
|
|
216
|
+
const xs = x2 - dx * cab * 0.7, ys = y2 - dy * cab * 0.7, L = Math.hypot(xs - x1, ys - y1), lg = cab * 0.62, f = (v) => v.toFixed(1);
|
|
217
|
+
const haste = document.createElementNS(svgNS, "path"), ponta = document.createElementNS(svgNS, "path");
|
|
218
|
+
[["class", "k2dq-seta"], ["id", `${id}-haste`], ["d", `M ${f(x1)} ${f(y1)} L ${f(xs)} ${f(ys)}`], ["stroke", corLinha], ["stroke-width", grossura]].forEach(([k, v]) => haste.setAttribute(k, v));
|
|
219
|
+
Object.assign(haste.style, { strokeDasharray: f(L), strokeDashoffset: f(L) });
|
|
220
|
+
const bx = x2 - dx * cab, by = y2 - dy * cab;
|
|
221
|
+
[["class", "k2dq-ponta"], ["id", `${id}-ponta`], ["fill", corLinha], ["d", `M ${f(x2)} ${f(y2)} L ${f(bx - dy * lg)} ${f(by + dx * lg)} L ${f(bx + dy * lg)} ${f(by - dx * lg)} Z`]].forEach(([k, v]) => ponta.setAttribute(k, v));
|
|
222
|
+
svg.appendChild(haste); svg.appendChild(ponta);
|
|
223
|
+
return { haste, ponta, L };
|
|
224
|
+
}
|
|
225
|
+
const setas = pares.map(([a, b, dono], k) => ({ a, b, dono, ...novaSeta(a, b, C.cores.tinta, Math.max(5, 8 * U), `k2dq-seta-${k}`) }));
|
|
226
|
+
const ex = PARAM.exemplo, caminho = ex ? ex.caminho.filter((i) => i >= 0 && i < N) : [];
|
|
227
|
+
const liga = (a, b) => setas.findIndex((q) => (q.a === a && q.b === b) || (q.a === b && q.b === a));
|
|
228
|
+
// o caminho aceso: cópias das setas do caminho na cor do caminho, desenhadas por cima
|
|
229
|
+
const setasCaminho = caminho.slice(1).map((b, k) => ({ k: k + 1, s: liga(caminho[k], b) })).filter((q) => q.s >= 0)
|
|
230
|
+
.map((q) => ({ k: q.k, ...novaSeta(setas[q.s].a, setas[q.s].b, PARAM.corCaminho, Math.max(8, 13 * U), `k2dq-caminho-${q.k}`) }));
|
|
231
|
+
|
|
232
|
+
// ------------------------------------------------------------------ a linha do tempo
|
|
233
|
+
const tl = gsap.timeline({ paused: true });
|
|
234
|
+
tl.fromTo(fundoEl, { opacity: 0 }, { opacity: 1, duration: Math.max(0.01, PARAM.entrada), ease: "power1.out" }, 0);
|
|
235
|
+
tl.fromTo("#k2dq-palco", { opacity: 0 }, { opacity: 1, duration: 0.01 }, 0);
|
|
236
|
+
// eventos com o que cada um precisa antes (a seta depois das duas partes; o exemplo depois do caminho inteiro)
|
|
237
|
+
const ev = [];
|
|
238
|
+
if (PARAM.titulo) ev.push({ tipo: "titulo", quer: momento(PARAM.titulo.em, 0.2) - 0.1, pre: [] });
|
|
239
|
+
PARAM.partes.forEach((p, i) => ev.push({ tipo: "no", i, quer: momento(p.em, 0.6 + i * 1.4) - 0.1, pre: [] }));
|
|
240
|
+
setas.forEach((s, k) => ev.push({ tipo: "seta", k, quer: momento(PARAM.partes[s.dono].seta, 0), pre: [s.a, s.b] }));
|
|
241
|
+
if (caminho.length) ev.push({ tipo: "exemplo", quer: momento(ex.em, DUR * 0.6), pre: caminho.slice() });
|
|
242
|
+
const noEv = (i) => ev.find((e) => e.tipo === "no" && e.i === i);
|
|
243
|
+
ev.forEach((e) => { e.ef = Math.max(e.quer, ...e.pre.map((i) => noEv(i).quer + 0.01)); if (e.tipo === "exemplo") setasCaminho.forEach((q) => { e.ef = Math.max(e.ef, momento(PARAM.partes[setas[liga(caminho[q.k - 1], caminho[q.k])].dono].seta, 0) + 0.02); }); });
|
|
244
|
+
const ordem = { titulo: 0, no: 1, seta: 2, exemplo: 3 };
|
|
245
|
+
ev.sort((a, b) => a.ef - b.ef || ordem[a.tipo] - ordem[b.tipo]);
|
|
246
|
+
const pousou = {}, POP = 0.35, RISCA = 0.32;
|
|
247
|
+
ev.forEach((e, n) => {
|
|
248
|
+
if (e.tipo === "titulo") {
|
|
249
|
+
tl.fromTo("#k2dq-titulo", { opacity: 0, y: 24 * U }, { opacity: 1, y: 0, duration: POP, ease: "power2.out" }, agenda(e.quer, POP));
|
|
250
|
+
} else if (e.tipo === "no") {
|
|
251
|
+
const t = agenda(e.quer, POP);
|
|
252
|
+
// sobe um pouco e aparece (nada anima escala no kit: o Chrome guarda o desenho da escala antiga e o render deixa de sair igual)
|
|
253
|
+
tl.fromTo(nos[e.i], { opacity: 0, y: 28 * U }, { opacity: 1, y: 0, duration: POP, ease: "back.out(1.6)" }, t);
|
|
254
|
+
pousou[e.i] = t + POP;
|
|
255
|
+
} else if (e.tipo === "seta") {
|
|
256
|
+
const s = setas[e.k], pronta = Math.max(pousou[s.a] || 0, pousou[s.b] || 0), quer = Math.max(e.quer, pronta);
|
|
257
|
+
// a seta dita junto com a parte nova sai colada nela: as duas são uma ação só
|
|
258
|
+
const t = agenda(quer, RISCA + 0.12, quer <= pronta + 0.15 && pronta >= fimUltima - 1e-3);
|
|
259
|
+
// a haste só aparece quando começa a se desenhar (antes, a ponta redonda da linha escondida ficava como um pontinho)
|
|
260
|
+
tl.fromTo(s.haste, { opacity: 0 }, { opacity: 1, duration: 0.01, ease: "none" }, t);
|
|
261
|
+
tl.fromTo(s.haste, { strokeDashoffset: s.L }, { strokeDashoffset: 0, duration: RISCA, ease: "power2.inOut", immediateRender: false }, t);
|
|
262
|
+
tl.fromTo(s.ponta, { opacity: 0 }, { opacity: 1, duration: 0.12, ease: "none" }, t + RISCA);
|
|
263
|
+
} else {
|
|
264
|
+
// acende as partes do caminho uma depois da outra, com a seta entre elas; as outras que já estão na tela perdem força
|
|
265
|
+
const passo = 0.28, dur = caminho.length * passo + 0.1, t = agenda(e.quer, dur);
|
|
266
|
+
const fora = nos.filter((_, i) => !caminho.includes(i) && (pousou[i] || Infinity) <= t);
|
|
267
|
+
caminho.forEach((i, k) => tl.fromTo(halos[i], { opacity: 0 }, { opacity: 1, duration: 0.2, ease: "power1.out" }, t + k * passo));
|
|
268
|
+
setasCaminho.forEach((q) => {
|
|
269
|
+
tl.fromTo(q.haste, { opacity: 0 }, { opacity: 1, duration: 0.01, ease: "none" }, t + (q.k - 1) * passo + 0.1);
|
|
270
|
+
tl.fromTo(q.haste, { strokeDashoffset: q.L }, { strokeDashoffset: 0, duration: passo * 0.8, ease: "none", immediateRender: false }, t + (q.k - 1) * passo + 0.1);
|
|
271
|
+
tl.fromTo(q.ponta, { opacity: 0 }, { opacity: 1, duration: 0.08 }, t + q.k * passo);
|
|
272
|
+
});
|
|
273
|
+
if (fora.length && PARAM.apaga < 1) tl.fromTo(fora, { opacity: 1 }, { opacity: PARAM.apaga, duration: 0.3, ease: "power1.inOut", immediateRender: false }, t);
|
|
274
|
+
// apaga (uma ação à parte): o esquema volta ao normal antes da ação seguinte
|
|
275
|
+
const prox = ev[n + 1];
|
|
276
|
+
const quer = ex.ate != null ? momento(ex.ate, t + dur + ex.aceso) : Math.min(t + dur + (ex.aceso || 1.4), prox ? prox.ef - 0.3 - PARAM.pausa : Infinity);
|
|
277
|
+
const tA = agenda(Math.max(quer, t + dur + 0.6), 0.3, true), volta = { opacity: 0, duration: 0.3, ease: "power1.inOut", immediateRender: false };
|
|
278
|
+
tl.to(caminho.map((i) => halos[i]).concat(...setasCaminho.map((q) => [q.haste, q.ponta])), volta, tA);
|
|
279
|
+
if (fora.length && PARAM.apaga < 1) tl.to(fora, { ...volta, opacity: 1 }, tA);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
const parado = DUR - PARAM.saida - fimUltima;
|
|
283
|
+
if (parado < PARAM.final - 0.05) console.warn(`[${ID}] o esquema completo fica parado só ${parado.toFixed(2)} s no fim (pedido: ${PARAM.final} s): estenda o "ate" da cena ou use segura_fim`);
|
|
284
|
+
if (PARAM.saida > 0) tl.fromTo(["#k2dq-palco", fundoEl], { opacity: 1 }, { opacity: 0, duration: PARAM.saida, ease: "power1.in", immediateRender: false }, Math.max(0, DUR - PARAM.saida));
|
|
285
|
+
window.__timelines["k2d_esquema"] = tl;
|
|
286
|
+
})();
|
|
287
|
+
</script>
|
|
288
|
+
</template>
|
|
289
|
+
</body></html>
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR"><head><meta charset="UTF-8"/><title>k2d · medidor com agulha (a leitura muda com a fala)</title></head>
|
|
3
|
+
<body>
|
|
4
|
+
<!--
|
|
5
|
+
Kit 2D (a): um medidor semicircular, como um velocímetro. O cartão aparece parado com o rótulo do topo, o arco
|
|
6
|
+
dividido em zonas coloridas e a agulha no valor de começo. A cada ação dita, só a agulha e a leitura mudam: a
|
|
7
|
+
agulha dá um pequeno vai e volta na palavra, gira até o valor novo e a leitura de baixo troca de texto, com a cor
|
|
8
|
+
da zona onde a agulha parou. Uma ação de cada vez, com tudo parado depois de cada uma. O arco e o cartão nunca
|
|
9
|
+
mudam de desenho nem de lugar; várias ações em sequência fazem a agulha subir e descer conforme a fala.
|
|
10
|
+
Serve para "custo mínimo", "risco alto", "nível", "nota": qualquer coisa que a fala põe numa escala. Valor e
|
|
11
|
+
texto de leitura só os que existem (a fala, o que foi medido): a cena não inventa número nem conta sozinha.
|
|
12
|
+
Guia: references/animacao.md ("Kit 2D"). Para usar: copie para compositions/cenas/<nome>.html, troque
|
|
13
|
+
"k2d_medidor" (template, raiz e ID) e o prefixo "k2dg-", mude os PARÂMETROS e ligue no projeto.json em
|
|
14
|
+
"cenas_proprias".
|
|
15
|
+
Por cima da pessoa: com area: "acima_do_rosto" (e a pessoa em tela cheia) o medidor vira um cartão pequeno acima da
|
|
16
|
+
cabeça, sem fundo; em área baixa e larga o desenho deita (arco à esquerda, texto à direita). O cartão nunca cobre o
|
|
17
|
+
rosto: se não houver espaço, ou se uma "area" escrita à mão tocar o rosto, a cena avisa no console (hf check).
|
|
18
|
+
Fora do 9:16, a raiz leva o tamanho do vídeo (data-width="1920" data-height="1080" no 16:9); o check
|
|
19
|
+
acusa se esquecer.
|
|
20
|
+
-->
|
|
21
|
+
<template id="k2d_medidor-template">
|
|
22
|
+
<style>
|
|
23
|
+
@font-face{font-family:Montserrat;src:url("assets/fontes/Montserrat.ttf") format("truetype");font-weight:100 900;font-display:block}
|
|
24
|
+
#root{position:absolute;inset:0;overflow:hidden;font-family:Montserrat}
|
|
25
|
+
#k2dg-fundo{position:absolute;inset:0;opacity:0}
|
|
26
|
+
#k2dg-cartao{position:absolute;box-sizing:border-box;opacity:0}
|
|
27
|
+
#k2dg-rotulo{position:absolute;display:block;font-weight:900;letter-spacing:.08em;text-align:center;white-space:nowrap}
|
|
28
|
+
.k2dg-arco{position:absolute;opacity:0;z-index:1}
|
|
29
|
+
#k2dg-agulha{position:absolute;opacity:0;z-index:2;clip-path:polygon(42% 0,58% 0,100% 100%,0 100%)}
|
|
30
|
+
#k2dg-cubo{position:absolute;opacity:0;z-index:3;border-radius:50%}
|
|
31
|
+
#k2dg-cubo-miolo{position:absolute;border-radius:50%}
|
|
32
|
+
.k2dg-leitura{position:absolute;display:block;font-weight:900;letter-spacing:.02em;text-align:center;white-space:nowrap;opacity:0}
|
|
33
|
+
#k2dg-selo{position:absolute;opacity:0}
|
|
34
|
+
#k2dg-selo-txt{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-weight:900;letter-spacing:.06em;white-space:nowrap}
|
|
35
|
+
</style>
|
|
36
|
+
<div id="root" data-composition-id="k2d_medidor" data-width="1080" data-height="1920" data-duration="7">
|
|
37
|
+
<div id="k2dg-fundo"></div>
|
|
38
|
+
<div id="k2dg-cartao">
|
|
39
|
+
<span id="k2dg-rotulo">CUSTO</span>
|
|
40
|
+
<div id="k2dg-agulha"></div>
|
|
41
|
+
<div id="k2dg-cubo"><div id="k2dg-cubo-miolo"></div></div>
|
|
42
|
+
<div id="k2dg-selo"></div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<script>
|
|
46
|
+
(function () {
|
|
47
|
+
const ID = "k2d_medidor";
|
|
48
|
+
// ------------------------------------------------------------------ PARÂMETROS (troque aqui)
|
|
49
|
+
// Tempo: número (segundos da cena), "palavra" ou {palavra, depois, padrao}. UMA palavra, procurada na fala do
|
|
50
|
+
// trecho sem acento e pelo começo; "depois" pula as repetições anteriores; "padrao" vale quando a palavra não
|
|
51
|
+
// aparece (cena aberta sozinha no estúdio, transcrição que mudou).
|
|
52
|
+
const PARAM = {
|
|
53
|
+
rotulo: "CUSTO", // o texto do topo
|
|
54
|
+
// o arco, da esquerda (0) para a direita (1): cada zona vai até "ate"; cor: nome do estilo ou "#rrggbb"
|
|
55
|
+
zonas: [{ ate: 0.34, cor: "verde" }, { ate: 0.67, cor: "destaque" }, { ate: 1, cor: "vermelho" }],
|
|
56
|
+
inicio: 0.84, // onde a agulha está quando o medidor aparece (0 a 1)
|
|
57
|
+
leitura: null, // texto de leitura já na tela no começo (null: a leitura só aparece na 1ª ação)
|
|
58
|
+
selo: null, // opcional: { texto: "NO AR", cor: "verde", em: {palavra, padrao} }, entra depois da última ação
|
|
59
|
+
// as ações, uma de cada vez, na ordem da fala. Cada uma gira a agulha até "valor" (0 a 1) e, se tiver "leitura",
|
|
60
|
+
// troca o texto de baixo (cor: a da zona onde a agulha para; "cor" na ação escolhe outra). A palavra vai em
|
|
61
|
+
// "em" (ou "palavra"), como nas outras cenas do kit.
|
|
62
|
+
acoes: [
|
|
63
|
+
{ valor: 0.12, leitura: "MÍNIMO", em: { palavra: "minimo", padrao: 2.2 } },
|
|
64
|
+
],
|
|
65
|
+
entra: 0, // quando o medidor aparece (fica parado uns "pausa" segundos antes da 1ª ação)
|
|
66
|
+
pausa: 0.6, // tudo parado pelo menos isso depois de cada ação
|
|
67
|
+
legenda: true, // tela cheia: deixa livre a faixa da legenda (a cena usa os 70% de cima da zona segura)
|
|
68
|
+
// Com false o medidor fica maior: esconda a legenda no trecho ("esconde_legenda": true)
|
|
69
|
+
fundo: "auto", // "auto" (papel em tela cheia, transparente na moldura), "papel", "#rrggbb" ou null
|
|
70
|
+
area: null, // [x0, y0, x1, y1] em px; "acima_do_rosto" = cartão pequeno acima da cabeça, sem fundo (a pessoa em tela cheia);
|
|
71
|
+
// null = área livre da moldura ou a zona segura
|
|
72
|
+
entrada: 0.2,
|
|
73
|
+
saida: 0.25,
|
|
74
|
+
};
|
|
75
|
+
// ------------------------------------------------------------------ dados do build.py (padrões para abrir sozinha)
|
|
76
|
+
const C = Object.assign({ palavras: [], duracao: 7, largura: 1080, altura: 1920, seguro: [140, 230, 940, 1530], rosto: null, moldura: null,
|
|
77
|
+
cores: { papel: "#fffdfd", linha: "#e6ddf8", base: "#c7b7ef", destaque: "#665080", tinta: "#352c40", verde: "#2e9e5b", vermelho: "#e5484d" } },
|
|
78
|
+
(window.__cenas || {})[ID] || {});
|
|
79
|
+
const DUR = C.duracao, U = Math.min(C.largura, C.altura) / 1080;
|
|
80
|
+
// a raiz precisa do tamanho do vídeo: em 16:9 (ou outro formato) troque data-width e data-height da raiz, senão a cena sai cortada
|
|
81
|
+
const raiz = document.getElementById("k2dg-fundo").parentElement;
|
|
82
|
+
if (raiz.offsetWidth && (Math.abs(raiz.offsetWidth - C.largura) > 2 || Math.abs(raiz.offsetHeight - C.altura) > 2))
|
|
83
|
+
console.error(`[${ID}] a raiz da cena tem ${raiz.offsetWidth} x ${raiz.offsetHeight} e o vídeo ${C.largura} x ${C.altura}: troque data-width e data-height da raiz`);
|
|
84
|
+
// ------------------------------------------------------------------ "acima_do_rosto": o cartão acima da cabeça, sem cobrir o rosto
|
|
85
|
+
// area: "acima_do_rosto" vira [seguro.x0, seguro.y0 + 20, seguro.x1, rosto.topo + 40] (sem rosto, a faixa de cima da zona segura,
|
|
86
|
+
// 25% da altura) e o fundo "auto" vira transparente (a pessoa aparece por baixo). O "topo" é uma estimativa com folga (uns 80 a 100 px
|
|
87
|
+
// acima do cabelo de verdade): 40 px abaixo dele o cartão ainda acaba uns 40 a 60 px acima do cabelo e cresce até esse limite.
|
|
88
|
+
// Na moldura o lugar já é a área livre acima do quadro.
|
|
89
|
+
const ZS = C.seguro, ROSTO = C.rosto, ACIMA = PARAM.area === "acima_do_rosto" && !C.moldura;
|
|
90
|
+
if (PARAM.area === "acima_do_rosto") {
|
|
91
|
+
if (!ACIMA) PARAM.area = null;
|
|
92
|
+
else {
|
|
93
|
+
const ya = ZS[1] + 20, yb = ROSTO ? ROSTO.topo + 40 : Math.round(ZS[1] + (ZS[3] - ZS[1]) * 0.25);
|
|
94
|
+
if (yb - ya < 200) console.warn(`[${ID}] não há espaço acima do rosto (só ${Math.max(0, Math.round(yb - ya))} px livres, o cartão pede uns 200): é melhor tela cheia com fundo ou moldura neste trecho. Desenhei o cartão com o que sobra (no mínimo 160 px) na zona segura de cima, o melhor que coube`);
|
|
95
|
+
PARAM.area = [ZS[0], ya, ZS[2], Math.max(yb, ya + 160)];
|
|
96
|
+
if (PARAM.fundo === "auto") PARAM.fundo = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// o retângulo do cartão (x0, y0, x1, y1) encosta no rosto, do topo da cabeça ao queixo, com 20 px de folga? Só vale com o fundo transparente
|
|
100
|
+
// (a pessoa aparece por baixo) e quando a área não é a "acima_do_rosto", que já fica acima da cabeça
|
|
101
|
+
const avisaRosto = (x0, y0, x1, y1) => { if (!fundo && !ACIMA && ROSTO && x0 < ROSTO.x1 + 20 && x1 > ROSTO.x0 - 20 && y0 < ROSTO.y1 + 20 && y1 > Math.min(ROSTO.y0, ROSTO.topo ?? ROSTO.y0) - 20)
|
|
102
|
+
console.warn(`[${ID}] o cartão cobre o rosto: use area: "acima_do_rosto" ou desça/suba a area`); };
|
|
103
|
+
// ------------------------------------------------------------------ ajudantes do kit 2D (iguais em todas as cenas do kit; não precisa mexer)
|
|
104
|
+
const sem = (s) => String(s).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
105
|
+
const palavra = (txt, depois, padrao) => { const a = sem(txt), p = a && (C.palavras || []).find((w) => w.t >= depois - 1e-3 && sem(w.texto).startsWith(a)); return p ? p.t : padrao; };
|
|
106
|
+
const momento = (m, padrao) => (typeof m === "number" ? m : typeof m === "string" ? palavra(m, 0, padrao)
|
|
107
|
+
: m && typeof m === "object" ? palavra(m.palavra || "", m.depois == null ? 0 : momento(m.depois, 0), m.padrao == null ? padrao : m.padrao) : padrao);
|
|
108
|
+
// fila: uma ação de cada vez; a próxima espera a anterior terminar e ficar parada PARAM.pausa segundos
|
|
109
|
+
let fimUltima = C.moldura ? Math.max(0, C.moldura.fechada_em || 0) : 0, livre = fimUltima + PARAM.entrada;
|
|
110
|
+
function agenda(quer, dur, colada) {
|
|
111
|
+
const t = Math.max(quer, colada ? fimUltima : livre);
|
|
112
|
+
if (t - quer > 0.4) console.warn(`[${ID}] uma ação entrou ${(t - quer).toFixed(2)} s depois da palavra: afaste as palavras ou diminua "pausa"`);
|
|
113
|
+
fimUltima = t + dur; livre = fimUltima + PARAM.pausa; return t;
|
|
114
|
+
}
|
|
115
|
+
const cor = (n) => C.cores[n] || n;
|
|
116
|
+
const rgb = (h) => { h = String(h).replace("#", ""); if (h.length === 3) h = h.replace(/./g, "$&$&"); return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)); };
|
|
117
|
+
const mistura = (a, b, k) => "#" + rgb(a).map((v, i) => Math.round(v + (rgb(b)[i] - v) * k).toString(16).padStart(2, "0")).join("");
|
|
118
|
+
// texto: largura estimada da Montserrat até o peso 900, com folga (a fonte ainda não carregou quando a cena monta), quebra e o maior tamanho que cabe
|
|
119
|
+
const peso = (ch) => { const b = ch.normalize("NFD")[0]; return b === " " ? 0.28 : "MW".includes(b) ? 1 : "mw".includes(b) ? 0.92 : "iljI.,:;'!|".includes(b) ? 0.32 : /[A-Z]/.test(b) ? 0.76 : /[0-9]/.test(b) ? 0.68 : 0.62; };
|
|
120
|
+
const larg = (t, px) => [...String(t)].reduce((s, ch) => s + peso(ch), 0) * px * 1.1;
|
|
121
|
+
const linhas = (t, px, w) => String(t).split(/\s+/).filter(Boolean).reduce((ls, p) => { const u = ls[ls.length - 1]; if (u !== undefined && larg(u + " " + p, px) <= w) ls[ls.length - 1] = u + " " + p; else ls.push(p); return ls; }, []);
|
|
122
|
+
const cabe = (t, w, h, maior, menor, max) => { for (let px = Math.round(maior); px > menor; px--) { const ls = linhas(t, px, w); if (ls.length <= max && ls.every((l) => larg(l, px) <= w) && ls.length * px * 1.16 <= h) return px; } return Math.round(menor); };
|
|
123
|
+
// onde a cena fica: PARAM.area, a área livre da moldura ou a zona segura (sem a faixa da legenda, com PARAM.legenda)
|
|
124
|
+
const A = PARAM.area ? PARAM.area.slice() : C.moldura && C.moldura.area_livre ? C.moldura.area_livre.slice()
|
|
125
|
+
: [C.seguro[0], C.seguro[1], C.seguro[2], PARAM.legenda ? Math.round(C.seguro[1] + (C.seguro[3] - C.seguro[1]) * 0.7) : C.seguro[3]];
|
|
126
|
+
const AW = A[2] - A[0], AH = A[3] - A[1];
|
|
127
|
+
const fundo = PARAM.fundo === "auto" ? (C.moldura ? null : "papel") : PARAM.fundo;
|
|
128
|
+
function pintaFundo(el) {
|
|
129
|
+
if (fundo === "papel" && C.fundo_css) el.style.cssText += ";" + C.fundo_css; // estilo escuro: o fundo do próprio estilo (grade e brilho)
|
|
130
|
+
else if (fundo === "papel") { const g = Math.round(60 * U); Object.assign(el.style, { backgroundColor: C.cores.papel, backgroundSize: `${g}px ${g}px`, backgroundImage: `linear-gradient(${C.cores.linha} 2px,transparent 2px),linear-gradient(90deg,${C.cores.linha} 2px,transparent 2px)` }); }
|
|
131
|
+
else if (fundo) el.style.backgroundColor = fundo;
|
|
132
|
+
}
|
|
133
|
+
const novo = (tag, id, cls, pai, estilo, texto) => { const e = document.createElement(tag); if (id) e.id = id; if (cls) e.className = cls; if (estilo) Object.assign(e.style, estilo); if (texto != null) e.textContent = texto; pai.appendChild(e); return e; };
|
|
134
|
+
const px = (v) => `${v}px`;
|
|
135
|
+
const claro = (h) => { const [r, g, b] = rgb(h); return 0.299 * r + 0.587 * g + 0.114 * b > 150; }; // letra escura em fundo claro
|
|
136
|
+
// letra sobre uma cor: escura em fundo claro, branca em fundo escuro. "escuro" é a tinta do estilo (a letra escura de
|
|
137
|
+
// papelaria e noite) ou, num estilo escuro (tinta clara), o papel
|
|
138
|
+
const escuro = claro(C.cores.tinta) ? C.cores.papel : C.cores.tinta;
|
|
139
|
+
const sobre = (fundo) => (claro(fundo) ? escuro : "#ffffff");
|
|
140
|
+
|
|
141
|
+
// ------------------------------------------------------------------ o medidor: medidas (u cresce ou encolhe tudo junto, pela área)
|
|
142
|
+
const $ = (id) => document.getElementById(id);
|
|
143
|
+
pintaFundo($("k2dg-fundo"));
|
|
144
|
+
const S = PARAM.selo, NZ = PARAM.zonas.length, f = (v) => v.toFixed(1), clamp = (v) => Math.min(1, Math.max(0, Number(v) || 0));
|
|
145
|
+
const P0 = 44, HL = 72, VAO = 18, W0 = 58, HUB0 = 32, GAP = 26, HR = 112, HS = 64, SOBE = 22, CS = S ? SOBE + HS : 0;
|
|
146
|
+
// dois desenhos: em pé (rótulo em cima, arco, leitura embaixo) e deitado (arco à esquerda; rótulo, leitura e selo numa coluna à
|
|
147
|
+
// direita), que a área baixa e larga (o cartão acima da cabeça) pede. Vale o que deixa o desenho bem maior que o outro
|
|
148
|
+
const EP = { R0: 300, P0, larg0: 600 + 2 * P0, alt0: P0 + HL + VAO + 300 + HUB0 + 6 + GAP + HR + CS + P0 };
|
|
149
|
+
const ED = { R0: 210, P0: 36, larg0: 72 + 420 + 44 + 440, alt0: 72 + Math.max(210 + HUB0 + 6, HL + 12 + HR + CS) };
|
|
150
|
+
const esc = (m) => Math.min(1.12, AW / ((m.larg0 + 8) * U), AH / ((m.alt0 + 22) * U)); // + a borda e a sombra dura, que passam do cartão
|
|
151
|
+
const deitado = esc(ED) > 1.3 * esc(EP), M = deitado ? ED : EP;
|
|
152
|
+
const u = U * esc(M);
|
|
153
|
+
const bd = Math.max(3, 4 * u), P = M.P0 * u, R = M.R0 * u, w = W0 * u, hub = HUB0 * u, r = R - w / 2;
|
|
154
|
+
const W = M.larg0 * u + 2 * bd, H = M.alt0 * u + 2 * bd, iw = W - 2 * bd, X = A[0] + (AW - W) / 2, Y = A[1] + (AH - H) / 2;
|
|
155
|
+
// o centro do arco e a coluna de texto (rótulo, leitura, selo), em coordenadas do cartão
|
|
156
|
+
const colH = (HL + 12 + HR + CS) * u, cx0 = deitado ? P + 2 * R + 44 * u : 0, cw = deitado ? 440 * u : iw, folga = deitado ? 24 * u : 2 * P;
|
|
157
|
+
const cy0 = deitado ? (M.alt0 * u - colH) / 2 : P;
|
|
158
|
+
const gx = deitado ? P + R : iw / 2, gy = deitado ? (M.alt0 * u - (M.R0 + HUB0 + 6) * u) / 2 + R : P + (HL + VAO) * u + R;
|
|
159
|
+
const ry = deitado ? cy0 + (HL + 12) * u : gy + (HUB0 + 6 + GAP) * u;
|
|
160
|
+
avisaRosto(X, Y, X + W, Y + H);
|
|
161
|
+
const cartao = $("k2dg-cartao");
|
|
162
|
+
Object.assign(cartao.style, { left: px(X), top: px(Y), width: px(W), height: px(H), borderRadius: px(34 * u), background: C.cores.papel,
|
|
163
|
+
border: `${bd}px solid ${C.cores.base}`, boxShadow: `0 ${14 * u}px 0 -2px ${C.cores.base}` }); // sombra dura (a desfocada varia de um render para outro)
|
|
164
|
+
$("k2dg-rotulo").textContent = PARAM.rotulo;
|
|
165
|
+
Object.assign($("k2dg-rotulo").style, { left: px(cx0), top: px(cy0), width: px(cw), height: px(HL * u), lineHeight: px(HL * u), color: C.cores.tinta,
|
|
166
|
+
fontSize: px(cabe(PARAM.rotulo, cw - folga, HL * u, 58 * u, 26 * u, 1)) });
|
|
167
|
+
// o arco: uma peça por zona, com pontas redondas e um vão entre as zonas. Cada peça nasce invisível e entra da esquerda para a direita
|
|
168
|
+
// (um clip-path que abre; escala deixaria dois renders diferentes). Ângulo em graus: 0 = para cima, -90 = esquerda, 90 = direita
|
|
169
|
+
const rad = Math.PI / 180, ponta = (w / 2 / r) / rad, vao = 1.6, pad = 6 * u, bw = 2 * R + 2 * pad, bh = R + w / 2 + 2 * pad;
|
|
170
|
+
const ponto = (g, rr) => [pad + R + rr * Math.sin(g * rad), pad + R - rr * Math.cos(g * rad)];
|
|
171
|
+
const caixa = (el) => Object.assign(el.style, { left: px(gx - R - pad), top: px(gy - R - pad), width: px(bw), height: px(bh) });
|
|
172
|
+
const arcos = PARAM.zonas.map((z, k) => {
|
|
173
|
+
const de = k ? PARAM.zonas[k - 1].ate : 0, g0 = -90 + 180 * de + (k ? vao : 0) + ponta;
|
|
174
|
+
const g1 = Math.max(g0 + 0.2, -90 + 180 * z.ate - (k < NZ - 1 ? vao : 0) - ponta), p0 = ponto(g0, r), p1 = ponto(g1, r);
|
|
175
|
+
const el = novo("div", `k2dg-arco-${k}`, "k2dg-arco", cartao);
|
|
176
|
+
caixa(el);
|
|
177
|
+
el.innerHTML = `<svg width="${f(bw)}" height="${f(bh)}" fill="none"><path d="M ${f(p0[0])} ${f(p0[1])} A ${f(r)} ${f(r)} 0 0 1 ${f(p1[0])} ${f(p1[1])}" stroke="${cor(z.cor)}" stroke-width="${f(w)}" stroke-linecap="round"/></svg>`;
|
|
178
|
+
// onde a peça começa, em % da largura: o clip-path abre a partir dali
|
|
179
|
+
const x0 = Math.max(0, Math.min(...[r - w / 2, r + w / 2].map((rr) => ponto(g0 - ponta, rr)[0]))) / bw;
|
|
180
|
+
return { el, ini: `inset(0% ${f(100 - 100 * x0)}% 0% 0%)` };
|
|
181
|
+
});
|
|
182
|
+
// a régua: marcas pequenas por dentro do arco, sem número (a escala é só um desenho)
|
|
183
|
+
const regua = novo("div", "k2dg-arco-regua", "k2dg-arco", cartao);
|
|
184
|
+
caixa(regua);
|
|
185
|
+
let marcas = "";
|
|
186
|
+
for (let g = -90; g <= 90.01; g += 18) {
|
|
187
|
+
const a = ponto(g, r - w / 2 - 14 * u), b = ponto(g, r - w / 2 - 30 * u);
|
|
188
|
+
marcas += `<line x1="${f(a[0])}" y1="${f(a[1])}" x2="${f(b[0])}" y2="${f(b[1])}"/>`;
|
|
189
|
+
}
|
|
190
|
+
regua.innerHTML = `<svg width="${f(bw)}" height="${f(bh)}" fill="none" stroke="${mistura(C.cores.tinta, C.cores.papel, 0.6)}" stroke-width="${f(Math.max(3, 4 * u))}" stroke-linecap="round">${marcas}</svg>`;
|
|
191
|
+
arcos.push({ el: regua, ini: "inset(0% 100% 0% 0%)" });
|
|
192
|
+
// a agulha: a ponta encosta na borda de dentro do arco; gira em torno do cubo (transformOrigin é posto pelo GSAP)
|
|
193
|
+
const nw = 30 * u, ln = R - w + 10 * u;
|
|
194
|
+
Object.assign($("k2dg-agulha").style, { left: px(gx - nw / 2), top: px(gy - ln), width: px(nw), height: px(ln), background: C.cores.tinta });
|
|
195
|
+
Object.assign($("k2dg-cubo").style, { left: px(gx - hub), top: px(gy - hub), width: px(2 * hub), height: px(2 * hub), background: C.cores.tinta });
|
|
196
|
+
Object.assign($("k2dg-cubo-miolo").style, { left: px(hub * 0.62), top: px(hub * 0.62), width: px(hub * 0.76), height: px(hub * 0.76), background: C.cores.papel });
|
|
197
|
+
// as leituras: todas no mesmo lugar, a da vez por cima; a cor é a da zona onde a agulha para
|
|
198
|
+
const zonaDe = (v) => PARAM.zonas.find((z) => v <= z.ate + 1e-6) || PARAM.zonas[NZ - 1];
|
|
199
|
+
const todas = (PARAM.leitura ? [{ texto: PARAM.leitura, valor: clamp(PARAM.inicio) }] : [])
|
|
200
|
+
.concat(PARAM.acoes.filter((a) => a.leitura).map((a) => ({ texto: a.leitura, valor: clamp(a.valor), cor: a.cor })));
|
|
201
|
+
const fsL = todas.length ? Math.min(...todas.map((l) => cabe(l.texto, cw - folga, HR * u, 96 * u, 34 * u, 1))) : 0;
|
|
202
|
+
const leituras = todas.map((l, k) => novo("span", `k2dg-leitura-${k}`, "k2dg-leitura", cartao, { left: px(cx0), top: px(ry), width: px(cw), height: px(HR * u),
|
|
203
|
+
lineHeight: px(HR * u), fontSize: px(fsL), color: cor(l.cor || zonaDe(l.valor).cor) }, l.texto));
|
|
204
|
+
const $selo = $("k2dg-selo");
|
|
205
|
+
if (!S) $selo.style.display = "none";
|
|
206
|
+
else {
|
|
207
|
+
const fs = 34 * u, ws = Math.min(larg(S.texto, fs) + 64 * u, cw), c = mistura(cor(S.cor || "verde"), C.cores.tinta, 0.18);
|
|
208
|
+
Object.assign($selo.style, { left: px(cx0 + (cw - ws) / 2), top: px(ry + (HR + SOBE) * u), width: px(ws), height: px(HS * u), borderRadius: px(HS * u / 2), background: c });
|
|
209
|
+
novo("span", "k2dg-selo-txt", null, $selo, { fontSize: px(fs), color: sobre(c) }, S.texto);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ------------------------------------------------------------------ a linha do tempo
|
|
213
|
+
const tl = gsap.timeline({ paused: true });
|
|
214
|
+
const rot = (v) => -90 + 180 * clamp(v);
|
|
215
|
+
let ang = rot(PARAM.inicio);
|
|
216
|
+
tl.fromTo("#k2dg-fundo", { opacity: 0 }, { opacity: 1, duration: Math.max(0.01, PARAM.entrada), ease: "power1.out" }, 0);
|
|
217
|
+
tl.set("#k2dg-agulha", { transformOrigin: "50% 100%", rotation: ang }, 0);
|
|
218
|
+
const tE = agenda(momento(PARAM.entra, 0), 0.9);
|
|
219
|
+
// nada anima escala no kit: o Chrome guarda o desenho da escala antiga por um tempo e o render deixa de sair igual
|
|
220
|
+
tl.fromTo(cartao, { opacity: 0, y: 60 * u }, { opacity: 1, y: 0, duration: 0.45, ease: "back.out(1.5)" }, tE);
|
|
221
|
+
arcos.forEach((a, k) => tl.fromTo(a.el, { opacity: 1, clipPath: a.ini }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.28, ease: "power1.out" }, tE + 0.22 + 0.11 * k));
|
|
222
|
+
tl.fromTo("#k2dg-cubo", { opacity: 0 }, { opacity: 1, duration: 0.2, ease: "power1.out" }, tE + 0.45);
|
|
223
|
+
tl.fromTo("#k2dg-agulha", { opacity: 0, rotation: ang - 30 }, { opacity: 1, rotation: ang, duration: 0.4, ease: "power2.out", immediateRender: false }, tE + 0.5);
|
|
224
|
+
let nLeit = 0;
|
|
225
|
+
if (PARAM.leitura) tl.fromTo(leituras[nLeit++], { opacity: 0, y: 16 * u }, { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }, tE + 0.6);
|
|
226
|
+
PARAM.acoes.forEach((a, n) => {
|
|
227
|
+
const quer = momento(a.em != null ? a.em : a.palavra, 1 + n * 1.5) - 0.3, alvo = rot(a.valor), dir = alvo >= ang ? 1 : -1;
|
|
228
|
+
const t = agenda(quer, 0.95), recua = ang - dir * 6;
|
|
229
|
+
if (Math.abs(alvo - ang) >= 1) {
|
|
230
|
+
// o vai e volta: recua um pouco contra o sentido e gira até o valor, passando um pouco e voltando (mola)
|
|
231
|
+
tl.fromTo("#k2dg-agulha", { rotation: ang }, { rotation: recua, duration: 0.14, ease: "power2.out", immediateRender: false }, t);
|
|
232
|
+
tl.fromTo("#k2dg-agulha", { rotation: recua }, { rotation: alvo, duration: 0.75, ease: "elastic.out(1, 0.55)", immediateRender: false }, t + 0.16);
|
|
233
|
+
ang = alvo;
|
|
234
|
+
}
|
|
235
|
+
if (a.leitura) {
|
|
236
|
+
const k = nLeit++;
|
|
237
|
+
// a leitura velha sai antes da nova entrar (os dois textos nunca ficam juntos no mesmo lugar)
|
|
238
|
+
if (k > 0) tl.to(leituras[k - 1], { opacity: 0, y: -12 * u, duration: 0.14, ease: "power1.in", immediateRender: false }, t + 0.36);
|
|
239
|
+
tl.fromTo(leituras[k], { opacity: 0, y: 18 * u }, { opacity: 1, y: 0, duration: 0.32, ease: "back.out(1.7)" }, t + 0.54);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
if (S) tl.fromTo($selo, { opacity: 0, y: 16 * u }, { opacity: 1, y: 0, duration: 0.35, ease: "back.out(1.7)" }, agenda(momento(S.em, DUR - 1.6) - 0.1, 0.5, true));
|
|
243
|
+
if (DUR - PARAM.saida - fimUltima < 0.6) console.warn(`[${ID}] a última ação termina a ${(DUR - PARAM.saida - fimUltima).toFixed(2)} s do fim da cena: deixe o resultado parado pelo menos 0,6 s (estenda o "ate" da cena)`);
|
|
244
|
+
if (PARAM.saida > 0) tl.fromTo(["#k2dg-fundo", cartao], { opacity: 1 }, { opacity: 0, duration: PARAM.saida, ease: "power1.in", immediateRender: false }, Math.max(0, DUR - PARAM.saida));
|
|
245
|
+
window.__timelines["k2d_medidor"] = tl;
|
|
246
|
+
})();
|
|
247
|
+
</script>
|
|
248
|
+
</template>
|
|
249
|
+
</body></html>
|