primocode 10.15.2 → 10.17.0

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.
Files changed (245) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +74 -9
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +38 -8
  198. package/lib/app-chat.js +97 -0
  199. package/lib/catalogo.js +7 -1
  200. package/lib/chamada-torta.js +81 -0
  201. package/lib/editor-pro.js +786 -0
  202. package/lib/entrega.js +58 -0
  203. package/lib/estudio/conferencia-visual.js +76 -0
  204. package/lib/estudio/diretor.js +6 -1
  205. package/lib/estudio/exportar.js +3 -1
  206. package/lib/estudio/gosto/tela.html +1349 -0
  207. package/lib/estudio/gosto.js +305 -0
  208. package/lib/estudio/index.js +12 -1
  209. package/lib/estudio/kit-ae/base.jsx +119 -13
  210. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  211. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  212. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  213. package/lib/estudio/maquina-diretor.js +70 -12
  214. package/lib/estudio/maquina-foto.js +177 -0
  215. package/lib/estudio/maquina-guarda.js +207 -0
  216. package/lib/estudio/maquina-layout.js +22 -1
  217. package/lib/estudio/maquina.js +64 -11
  218. package/lib/estudio/referencias.js +1 -1
  219. package/lib/estudio/visao/visao.swift +97 -0
  220. package/lib/estudio/visao.js +60 -0
  221. package/lib/headless.js +15 -3
  222. package/lib/local/cerebro.json +285 -7
  223. package/lib/local/motor.js +5 -2
  224. package/lib/memoria-usuario.js +164 -0
  225. package/lib/nuvem.js +33 -2
  226. package/lib/passagem.js +50 -0
  227. package/lib/perigo.js +19 -11
  228. package/lib/protecao.js +134 -0
  229. package/lib/repeticao.js +1 -1
  230. package/lib/sandbox.js +57 -9
  231. package/lib/titulo-conversa.js +46 -0
  232. package/lib/tools.js +23 -4
  233. package/lib/ui.js +2 -2
  234. package/package.json +3 -2
  235. package/skills/editar-meu-video/SKILL.md +6 -2
  236. package/skills/editar-video/SKILL.md +113 -0
  237. package/studio/pessoa.py +52 -0
  238. package/voz/primo_voz/__main__.py +55 -40
  239. package/voz/primo_voz/app.py +500 -4
  240. package/voz/primo_voz/chat.html +1773 -0
  241. package/voz/primo_voz/conversa.py +0 -26
  242. package/voz/primo_voz/hud.html +8 -2
  243. package/voz/primo_voz/hud.py +129 -5
  244. package/voz/primo_voz/identidade.py +1 -1
  245. 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>