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,304 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR"><head><meta charset="UTF-8"/><title>k2d · barra de comando que digita o pedido</title></head>
3
+ <body>
4
+ <!--
5
+ Kit 2D (a): uma barra de pedido para a IA. O texto é digitado letra por letra enquanto a pessoa fala, com o cursor
6
+ colado na última letra digitada. Todas as letras já existem no desenho desde o começo, invisíveis, e só aparecem em
7
+ ordem: cada palavra do texto cai dentro da palavra falada que a chama (ou reparte a duração, se a cena não tiver
8
+ fala). Quando o texto é longo, a linha quebra sozinha e a barra cresce para baixo, mostrando mais uma linha (um
9
+ recorte que abre, nunca a barra mudando de tamanho). Depois da última letra o botão de enviar dá um toque, três
10
+ anéis acendem em volta dele e o selo de resultado ("pronto em minutos") entra na palavra dita.
11
+ O texto do pedido é só parâmetro: o motor não tem nenhum fixo. Serve para "peço pra IA e ela faz", "um comando e
12
+ pronto". Só o que a pessoa de fato fala ou pede; nada de promessa que o vídeo não mostra.
13
+ Guia: references/animacao.md ("Kit 2D"). Para usar: copie para compositions/cenas/<nome>.html, troque "k2d_comando"
14
+ (template, raiz e ID) e o prefixo "k2dm-", mude os PARÂMETROS e ligue no projeto.json em "cenas_proprias".
15
+ Por cima da pessoa: com area: "acima_do_rosto" (e a pessoa em tela cheia) a barra vira um cartão pequeno acima da
16
+ cabeça, sem fundo, e se ajusta à altura que sobra. O cartão nunca cobre o rosto: se não houver espaço, ou se uma
17
+ "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_comando-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
+ #k2dm-fundo{position:absolute;inset:0;opacity:0}
26
+ #k2dm-bloco{position:absolute;opacity:0}
27
+ #k2dm-rotulo{position:absolute;left:0;right:0;top:0;display:flex;align-items:center;justify-content:center;font-weight:800;letter-spacing:.14em;white-space:nowrap;opacity:0}
28
+ #k2dm-rotulo-pil{display:flex;align-items:center;justify-content:center}
29
+ #k2dm-rotulo-ponto{display:block;border-radius:50%}
30
+ #k2dm-cartao{position:absolute;opacity:0}
31
+ #k2dm-borda,#k2dm-corpo{position:absolute}
32
+ #k2dm-conteudo{position:absolute;inset:0}
33
+ #k2dm-icone{position:absolute;display:flex;align-items:center;justify-content:center;border-radius:50%;font-weight:900;white-space:nowrap}
34
+ #k2dm-texto{position:absolute;display:flex;flex-wrap:wrap;align-content:flex-start;font-weight:800}
35
+ .k2dm-palavra{display:inline-block;white-space:nowrap}
36
+ .k2dm-letra{opacity:0}
37
+ #k2dm-cursor{position:absolute;left:0;top:0;opacity:0}
38
+ #k2dm-enviar{position:absolute;border-radius:50%}
39
+ #k2dm-enviar svg{position:absolute;left:27%;top:27%;width:46%;height:46%;fill:none;stroke:currentColor;stroke-width:5;stroke-linecap:round;stroke-linejoin:round}
40
+ #k2dm-enviar-cor{position:absolute;inset:0;border-radius:50%;opacity:0}
41
+ .k2dm-anel{position:absolute;box-sizing:border-box;border-radius:50%;opacity:0}
42
+ #k2dm-selo{position:absolute;overflow:hidden;opacity:0}
43
+ #k2dm-selo-cor{position:absolute;inset:0;opacity:0}
44
+ #k2dm-selo-ck{position:absolute;border-radius:50%;opacity:0}
45
+ #k2dm-selo-ck svg{position:absolute;left:22%;top:22%;width:56%;height:56%;fill:none;stroke:currentColor;stroke-width:6;stroke-linecap:round;stroke-linejoin:round}
46
+ #k2dm-selo-txt{position:absolute;white-space:nowrap;font-weight:900;opacity:0}
47
+ </style>
48
+ <div id="root" data-composition-id="k2d_comando" data-width="1080" data-height="1920" data-duration="7">
49
+ <div id="k2dm-fundo"></div>
50
+ <div id="k2dm-bloco">
51
+ <div id="k2dm-rotulo"><div id="k2dm-rotulo-pil"><i id="k2dm-rotulo-ponto"></i><span id="k2dm-rotulo-txt">PEDIDO</span></div></div>
52
+ <div id="k2dm-cartao">
53
+ <div id="k2dm-borda"></div>
54
+ <div id="k2dm-corpo"></div>
55
+ <div id="k2dm-conteudo">
56
+ <div id="k2dm-icone">IA</div>
57
+ <div id="k2dm-texto"></div>
58
+ <div id="k2dm-cursor"></div>
59
+ <div id="k2dm-enviar"><svg viewBox="0 0 44 44"><path d="M22 35V11M11 22L22 11L33 22"/></svg><div id="k2dm-enviar-cor"><svg viewBox="0 0 44 44"><path d="M22 35V11M11 22L22 11L33 22"/></svg></div></div>
60
+ </div>
61
+ </div>
62
+ <div id="k2dm-selo"><div id="k2dm-selo-cor"></div><div id="k2dm-selo-ck"><svg viewBox="0 0 44 44"><path d="M11 23L19 31L33 14"/></svg></div><span id="k2dm-selo-txt">pronto em minutos</span></div>
63
+ </div>
64
+ </div>
65
+ <script>
66
+ (function () {
67
+ const ID = "k2d_comando";
68
+ // ------------------------------------------------------------------ PARÂMETROS (troque aqui)
69
+ // Tempo: número (segundos da cena), "palavra" ou {palavra, depois, padrao}. UMA palavra, procurada na fala do
70
+ // trecho sem acento e pelo começo; "depois" pula as repetições anteriores; "padrao" vale quando a palavra não
71
+ // aparece (cena aberta sozinha no estúdio, transcrição que mudou).
72
+ const PARAM = {
73
+ texto: "edita esse vídeo pra mim", // o pedido que a barra digita (aqui só o exemplo: troque pelo que a pessoa pede)
74
+ inicio: { palavra: "edita", padrao: 0.9 }, // a palavra falada em que a 1ª letra aparece
75
+ fim: null, // opcional: a palavra falada em que a digitação termina, ex.: { palavra: "mim", padrao: 2.7 }.
76
+ // Sem "fim", a digitação vai até a palavra do envio (no máximo uma palavra falada por palavra do texto)
77
+ envio: { palavra: "pronto", padrao: 3.8 }, // a palavra em que o botão envia e o selo entra; null = logo depois da última letra
78
+ selo: "pronto em minutos", // o selo de resultado; null = sem selo
79
+ rotulo_topo: "pedido", // o rótulo pequeno em cima da barra; null = sem rótulo
80
+ icone: "IA", // uma letra ou símbolo simples no círculo da esquerda; "" = círculo liso; null = sem círculo
81
+ entra: 0, // quando a barra aparece (fica parada uns "pausa" segundos antes da 1ª letra)
82
+ pausa: 0.6, // tudo parado pelo menos isso depois de cada ação
83
+ legenda: true, // tela cheia: deixa livre a faixa da legenda (a cena usa os 70% de cima da zona segura)
84
+ // Com false a cena usa a zona segura inteira: esconda a legenda no trecho ("esconde_legenda": true)
85
+ fundo: "auto", // "auto" (papel em tela cheia, transparente na moldura), "papel", "#rrggbb" ou null
86
+ 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);
87
+ // null = área livre da moldura ou a zona segura
88
+ entrada: 0.2,
89
+ saida: 0.25,
90
+ };
91
+ // ------------------------------------------------------------------ dados do build.py (padrões para abrir sozinha)
92
+ const C = Object.assign({ palavras: [], duracao: 7, largura: 1080, altura: 1920, seguro: [140, 230, 940, 1530], rosto: null, moldura: null,
93
+ cores: { papel: "#fffdfd", linha: "#e6ddf8", base: "#c7b7ef", destaque: "#665080", tinta: "#352c40", verde: "#2e9e5b", vermelho: "#e5484d" } },
94
+ (window.__cenas || {})[ID] || {});
95
+ const DUR = C.duracao, U = Math.min(C.largura, C.altura) / 1080;
96
+ // 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
97
+ const raiz = document.getElementById("k2dm-fundo").parentElement;
98
+ if (raiz.offsetWidth && (Math.abs(raiz.offsetWidth - C.largura) > 2 || Math.abs(raiz.offsetHeight - C.altura) > 2))
99
+ 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`);
100
+ // ------------------------------------------------------------------ "acima_do_rosto": o cartão acima da cabeça, sem cobrir o rosto
101
+ // area: "acima_do_rosto" vira [seguro.x0, seguro.y0 + 20, seguro.x1, rosto.topo + 40] (sem rosto, a faixa de cima da zona segura,
102
+ // 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
103
+ // 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.
104
+ // Na moldura o lugar já é a área livre acima do quadro.
105
+ const ZS = C.seguro, ROSTO = C.rosto, ACIMA = PARAM.area === "acima_do_rosto" && !C.moldura;
106
+ if (PARAM.area === "acima_do_rosto") {
107
+ if (!ACIMA) PARAM.area = null;
108
+ else {
109
+ const ya = ZS[1] + 20, yb = ROSTO ? ROSTO.topo + 40 : Math.round(ZS[1] + (ZS[3] - ZS[1]) * 0.25);
110
+ 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`);
111
+ PARAM.area = [ZS[0], ya, ZS[2], Math.max(yb, ya + 160)];
112
+ if (PARAM.fundo === "auto") PARAM.fundo = null;
113
+ }
114
+ }
115
+ // 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
116
+ // (a pessoa aparece por baixo) e quando a área não é a "acima_do_rosto", que já fica acima da cabeça
117
+ 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)
118
+ console.warn(`[${ID}] o cartão cobre o rosto: use area: "acima_do_rosto" ou desça/suba a area`); };
119
+ // ------------------------------------------------------------------ ajudantes do kit 2D (iguais em todas as cenas do kit; não precisa mexer)
120
+ const sem = (s) => String(s).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
121
+ 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; };
122
+ const momento = (m, padrao) => (typeof m === "number" ? m : typeof m === "string" ? palavra(m, 0, padrao)
123
+ : m && typeof m === "object" ? palavra(m.palavra || "", m.depois == null ? 0 : momento(m.depois, 0), m.padrao == null ? padrao : m.padrao) : padrao);
124
+ // fila: uma ação de cada vez; a próxima espera a anterior terminar e ficar parada PARAM.pausa segundos
125
+ let fimUltima = C.moldura ? Math.max(0, C.moldura.fechada_em || 0) : 0, livre = fimUltima + PARAM.entrada;
126
+ function agenda(quer, dur, colada) {
127
+ const t = Math.max(quer, colada ? fimUltima : livre);
128
+ 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"`);
129
+ fimUltima = t + dur; livre = fimUltima + PARAM.pausa; return t;
130
+ }
131
+ const cor = (n) => C.cores[n] || n;
132
+ 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)); };
133
+ const mistura = (a, b, k) => "#" + rgb(a).map((v, i) => Math.round(v + (rgb(b)[i] - v) * k).toString(16).padStart(2, "0")).join("");
134
+ // 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
135
+ 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; };
136
+ const larg = (t, px) => [...String(t)].reduce((s, ch) => s + peso(ch), 0) * px * 1.1;
137
+ 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; }, []);
138
+ 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); };
139
+ // onde a cena fica: PARAM.area, a área livre da moldura ou a zona segura (sem a faixa da legenda, com PARAM.legenda)
140
+ const A = PARAM.area ? PARAM.area.slice() : C.moldura && C.moldura.area_livre ? C.moldura.area_livre.slice()
141
+ : [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]];
142
+ const AW = A[2] - A[0], AH = A[3] - A[1];
143
+ const fundo = PARAM.fundo === "auto" ? (C.moldura ? null : "papel") : PARAM.fundo;
144
+ function pintaFundo(el) {
145
+ if (fundo === "papel" && C.fundo_css) el.style.cssText += ";" + C.fundo_css; // estilo escuro: o fundo do próprio estilo (grade e brilho)
146
+ 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)` }); }
147
+ else if (fundo) el.style.backgroundColor = fundo;
148
+ }
149
+ 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; };
150
+ const px = (v) => `${v}px`;
151
+ 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
152
+ // letra sobre uma cor: escura em fundo claro, branca em fundo escuro. "escuro" é a tinta do estilo (a letra escura de
153
+ // papelaria e noite) ou, num estilo escuro (tinta clara), o papel
154
+ const escuro = claro(C.cores.tinta) ? C.cores.papel : C.cores.tinta;
155
+ const sobre = (fundo) => (claro(fundo) ? escuro : "#ffffff");
156
+
157
+ // ------------------------------------------------------------------ o texto: palavras e letras, cada letra invisível até a vez dela
158
+ const $ = (id) => document.getElementById(id);
159
+ pintaFundo($("k2dm-fundo"));
160
+ const T = String(PARAM.texto || "").normalize("NFC").split(/\s+/).filter(Boolean);
161
+ if (!T.length) { console.error(`[${ID}] o parâmetro "texto" está vazio: escreva o pedido`); T.push("?"); }
162
+ // a escala: a maior em que o bloco inteiro (rótulo, barra com as linhas que o texto faz, selo) cabe na área. Em área baixa e larga
163
+ // (o cartão acima da cabeça) a barra usa a largura toda e a letra cresce até a altura que sobra
164
+ const baixa = AH / AW < 0.45, largura = (u) => Math.min(AW - 40 * u, baixa ? Math.max(880 * u, 4.6 * AH) : 880 * u);
165
+ const alturaDe = (k) => { const u = U * k, P = 28 * u, bt = 84 * u, tw = largura(u) - P - (PARAM.icone != null ? bt + 22 * u : 0) - P - bt - 22 * u;
166
+ const fs = cabe(PARAM.texto, tw, AH, 58 * u, 28 * u, 4), lh = Math.round(fs * 1.25);
167
+ return (PARAM.rotulo_topo ? 61 * u : 0) + 2 * (P + Math.max(0, (bt - lh) / 2)) + linhas(PARAM.texto, fs, tw).length * lh + 12 * u + (PARAM.selo ? 118 * u : 0); };
168
+ let k = Math.min(1.35, AW / (800 * U));
169
+ while (k > 0.3 && alturaDe(k) > AH) k -= 0.02;
170
+ const u = U * k;
171
+ const W = largura(u), P = 28 * u, bt = 84 * u, bw = Math.max(3, 4 * u), S = 12 * u, R = 46 * u;
172
+ const temIcone = PARAM.icone != null, xt = P + (temIcone ? bt + 22 * u : 0), tw = W - xt - P - bt - 22 * u;
173
+ const fs = cabe(PARAM.texto, tw, AH, 58 * u, 28 * u, 4), lh = Math.round(fs * 1.25);
174
+ const pv = P + Math.max(0, (bt - lh) / 2), yc = pv + lh / 2; // 1ª linha centrada no botão
175
+ const tx = $("k2dm-texto"), letras = [], falaPalavra = [];
176
+ Object.assign(tx.style, { left: px(xt), top: px(pv), width: px(tw), fontSize: px(fs), lineHeight: px(lh), color: C.cores.tinta });
177
+ T.forEach((w, k) => {
178
+ const pe = novo("span", `k2dm-palavra-${k}`, "k2dm-palavra", tx, { marginRight: px(0.27 * fs) });
179
+ falaPalavra.push(letras.length);
180
+ [...w].forEach((ch) => letras.push(novo("span", `k2dm-letra-${letras.length}`, "k2dm-letra", pe, null, ch)));
181
+ });
182
+ // o círculo, o botão (neutro até a 1ª letra, depois colorido) e os anéis do envio
183
+ const ic = $("k2dm-icone");
184
+ if (!temIcone) ic.style.display = "none";
185
+ else { ic.textContent = PARAM.icone; Object.assign(ic.style, { left: px(P), top: px(yc - bt / 2), width: px(bt), height: px(bt), fontSize: px(bt * 0.42), background: C.cores.base, color: sobre(C.cores.base) }); }
186
+ const bx = W - P - bt, by = yc - bt / 2;
187
+ Object.assign($("k2dm-enviar").style, { left: px(bx), top: px(by), width: px(bt), height: px(bt), background: mistura(C.cores.linha, C.cores.papel, 0.35), color: mistura(C.cores.tinta, C.cores.papel, 0.62) });
188
+ Object.assign($("k2dm-enviar-cor").style, { background: C.cores.destaque, color: sobre(C.cores.destaque) });
189
+ const aneis = [0, 1, 2].map((i) => { const r = bt / 2 + (8 + 11 * i) * u;
190
+ return novo("div", `k2dm-anel-${i}`, "k2dm-anel", $("k2dm-conteudo"), { left: px(bx + bt / 2 - r), top: px(by + bt / 2 - r), width: px(2 * r), height: px(2 * r), border: `${bw}px solid ${C.cores.destaque}` }); });
191
+ const cw = Math.max(3, 4 * u), chh = fs * 1.05, cur = $("k2dm-cursor");
192
+ Object.assign(cur.style, { left: px(-cw / 2), width: px(cw), height: px(chh), borderRadius: px(cw / 2), background: C.cores.destaque });
193
+ // o rótulo de cima e o selo de resultado
194
+ const fsR = 26 * u, hR = PARAM.rotulo_topo ? fsR * 1.5 : 0, gR = PARAM.rotulo_topo ? 22 * u : 0, hSel = PARAM.selo ? 84 * u : 0, gSel = PARAM.selo ? 34 * u : 0;
195
+ const rot = $("k2dm-rotulo"), sel = $("k2dm-selo");
196
+ if (!PARAM.rotulo_topo) rot.style.display = "none";
197
+ else { $("k2dm-rotulo-txt").textContent = String(PARAM.rotulo_topo).toUpperCase(); Object.assign(rot.style, { height: px(hR), fontSize: px(fsR), color: C.cores.destaque });
198
+ Object.assign($("k2dm-rotulo-ponto").style, { width: px(fsR * 0.5), height: px(fsR * 0.5), marginRight: px(fsR * 0.6), background: C.cores.base });
199
+ // com o vídeo da pessoa por baixo (sem fundo), o rótulo leva uma pílula escura: a letra solta sobre o vídeo não se lê
200
+ if (!fundo && !C.moldura) Object.assign($("k2dm-rotulo-pil").style, { height: px(hR), padding: `0 ${22 * u}px`, boxSizing: "border-box", borderRadius: px(hR / 2), background: C.cores.papel, border: `${bw}px solid ${C.cores.base}` }); }
201
+ const cSel = mistura(cor("verde"), C.cores.tinta, 0.18), fsS = 40 * u, ckS = 52 * u;
202
+ if (!PARAM.selo) sel.style.display = "none";
203
+ else { $("k2dm-selo-txt").textContent = PARAM.selo; Object.assign($("k2dm-selo-txt").style, { fontSize: px(fsS), lineHeight: px(hSel), color: sobre(cSel) });
204
+ $("k2dm-selo-cor").style.background = cSel; Object.assign($("k2dm-selo-ck").style, { left: px(15 * u), top: px((hSel - ckS) / 2), width: px(ckS), height: px(ckS), background: sobre(cSel), color: cSel }); }
205
+
206
+ // ------------------------------------------------------------------ quando cada letra aparece (por palavra falada)
207
+ const P0 = C.palavras || [], L0 = T.map((w) => [...w].length), nL = letras.length;
208
+ const tEnv = PARAM.envio == null ? null : momento(PARAM.envio, 3.8);
209
+ const indice = (m, de) => { const a = sem(typeof m === "string" ? m : (m && m.palavra) || ""), dep = m && typeof m === "object" && m.depois != null ? momento(m.depois, 0) : 0;
210
+ return a ? P0.findIndex((w, i) => i >= de && w.t >= dep - 1e-3 && sem(w.texto).startsWith(a)) : -1; };
211
+ let falados = [];
212
+ const i0 = PARAM.inicio == null ? -1 : indice(PARAM.inicio, 0);
213
+ if (i0 >= 0) {
214
+ let i1 = PARAM.fim == null ? -1 : indice(PARAM.fim, i0);
215
+ if (i1 < 0) { i1 = i0; while (i1 + 1 < P0.length && i1 - i0 + 1 < T.length && (tEnv == null || P0[i1 + 1].t < tEnv - 0.2)) i1++; }
216
+ falados = P0.slice(i0, i1 + 1).map((w) => ({ t: w.t, fim: Math.max(w.fim == null ? w.t + 0.3 : w.fim, w.t + 0.12) }));
217
+ }
218
+ const iv = []; // [começo, fim] de cada palavra do texto, em segundos da cena
219
+ if (falados.length) {
220
+ // a palavra k do texto cai na palavra falada que a chama; se várias caem na mesma, dividem o intervalo dela
221
+ const m = falados.length, alvo = T.map((_, k) => (T.length === 1 ? 0 : Math.round(k * (m - 1) / (T.length - 1))));
222
+ alvo.forEach((j, k) => { const s = falados[j], g = alvo.filter((v) => v === j).length, r = alvo.slice(0, k).filter((v) => v === j).length;
223
+ iv.push([s.t + (s.fim - s.t) * r / g, s.t + (s.fim - s.t) * (r + 1) / g]); });
224
+ } else {
225
+ // sem fala (cena aberta sozinha ou palavra que não apareceu): reparte pela duração, entre o início e o fim padrão
226
+ const a0 = momento(PARAM.inicio, 0.9), a1 = momento(PARAM.fim, tEnv == null ? a0 + Math.max(0.8, nL * 0.07) : Math.max(a0 + 0.8, tEnv - 1.0));
227
+ const tot = L0.reduce((s, v) => s + v + 0.7, 0); let acc = 0;
228
+ L0.forEach((v) => { iv.push([a0 + (a1 - a0) * acc / tot, a0 + (a1 - a0) * (acc + v) / tot]); acc += v + 0.7; });
229
+ }
230
+ const tLet = [];
231
+ T.forEach((_, k) => { for (let c = 0; c < L0[k]; c++) tLet.push(iv[k][0] + (iv[k][1] - iv[k][0]) * c / L0[k]); });
232
+
233
+ // ------------------------------------------------------------------ a linha do tempo (montada com a fonte carregada, porque a posição de cada letra é medida no navegador)
234
+ const montar = () => {
235
+ const r0 = tx.getBoundingClientRect(), esc = tx.offsetWidth && r0.width ? r0.width / tx.offsetWidth : 1;
236
+ const med = letras.map((el) => { const r = el.getBoundingClientRect();
237
+ return { x0: (r.left - r0.left) / esc, x1: (r.right - r0.left) / esc, ln: Math.max(0, Math.floor(((r.top + r.bottom) / 2 - r0.top) / esc / lh)) }; });
238
+ const N = Math.max(...med.map((m) => m.ln)) + 1, H = 2 * pv + N * lh, oculto = (n) => H - (2 * pv + n * lh);
239
+ // as medidas dependem das linhas que a fonte de verdade quebrou
240
+ const cy0 = hR + gR, Hb = cy0 + H + S + gSel + hSel, Xb = A[0] + (AW - W) / 2, Yb = A[1] + (AH - Hb) / 2;
241
+ Object.assign($("k2dm-bloco").style, { left: px(Xb), top: px(Yb), width: px(W), height: px(Hb) });
242
+ Object.assign($("k2dm-cartao").style, { left: 0, top: px(cy0), width: px(W), height: px(H + S) });
243
+ Object.assign($("k2dm-borda").style, { left: 0, top: 0, width: px(W), height: px(H + S), borderRadius: px(R), background: C.cores.base }); // a borda e a sombra dura são a mesma peça
244
+ Object.assign($("k2dm-corpo").style, { left: px(bw), top: px(bw), width: px(W - 2 * bw), height: px(H - 2 * bw), borderRadius: px(R - bw), background: C.cores.papel });
245
+ if (PARAM.selo) { const ws = 15 * u + ckS + 16 * u + $("k2dm-selo-txt").offsetWidth + 30 * u;
246
+ Object.assign(sel.style, { left: px((W - ws) / 2), top: px(cy0 + H + S + gSel), width: px(ws), height: px(hSel), borderRadius: px(hSel / 2) });
247
+ Object.assign($("k2dm-selo-txt").style, { left: px(15 * u + ckS + 16 * u), top: 0 }); }
248
+ avisaRosto(Xb, Yb, Xb + W, Yb + Hb);
249
+ const tl = gsap.timeline({ paused: true });
250
+ tl.fromTo("#k2dm-fundo", { opacity: 0 }, { opacity: 1, duration: Math.max(0.01, PARAM.entrada), ease: "power1.out" }, 0);
251
+ tl.fromTo("#k2dm-bloco", { opacity: 0 }, { opacity: 1, duration: 0.01 }, 0);
252
+ const tE = agenda(momento(PARAM.entra, 0), 0.45);
253
+ // nada anima escala no kit: o Chrome guarda o desenho da escala antiga por um tempo e o render deixa de sair igual
254
+ tl.fromTo("#k2dm-cartao", { opacity: 0, y: 60 * u }, { opacity: 1, y: 0, duration: 0.45, ease: "back.out(1.5)" }, tE);
255
+ if (PARAM.rotulo_topo) tl.fromTo(rot, { opacity: 0, y: 14 * u }, { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }, tE + 0.15);
256
+ // a digitação: uma ação só, da 1ª à última letra
257
+ const q = tLet[0], d = agenda(q, tLet[nL - 1] - q + 0.08) - q, ts = tLet.map((t) => t + d), tU = ts[nL - 1];
258
+ const pos = (i, depois) => ({ x: xt + (depois ? med[i].x1 + 3 * u : med[i].x0 - 3 * u), y: pv + med[i].ln * lh + (lh - chh) / 2 });
259
+ const pisca = (de, ate) => { for (let t = de; t < ate - 0.1; t += 0.9) { tl.set(cur, { opacity: 1 }, t); tl.set(cur, { opacity: 0 }, Math.min(ate - 0.05, t + 0.45)); } };
260
+ tl.set(cur, pos(0, false), 0);
261
+ pisca(tE + 0.3, ts[0] - 0.05);
262
+ letras.forEach((el, i) => {
263
+ tl.fromTo(el, { opacity: 0 }, { opacity: 1, duration: 0.06, ease: "none" }, ts[i]);
264
+ tl.set(cur, { ...pos(i, true), opacity: 1 }, ts[i]);
265
+ const k = falaPalavra.indexOf(i + 1); // a próxima letra é o começo de uma palavra: o cursor já vai para lá (a linha quebra aqui)
266
+ if (k > 0) tl.set(cur, pos(i + 1, false), Math.min(ts[i + 1] - 0.005, Math.max(ts[i] + 0.02, ts[i + 1] - 0.07)));
267
+ });
268
+ tl.fromTo("#k2dm-enviar-cor", { opacity: 0 }, { opacity: 1, duration: 0.15, ease: "power1.out" }, ts[0]);
269
+ // a barra cresce para baixo: o recorte abre uma linha antes da 1ª letra dela (a borda e o corpo abrem juntos)
270
+ const rec = (n, r) => `inset(0px 0px ${oculto(n)}px 0px round ${r}px)`;
271
+ let livreCresce = 0;
272
+ for (let n = 1; n < N; n++) {
273
+ const tg = Math.max(tE + 0.2, ts[med.findIndex((m) => m.ln >= n)] - 0.26, livreCresce);
274
+ [["#k2dm-borda", R], ["#k2dm-corpo", R - bw]].forEach(([id, r]) => tl.fromTo(id, { clipPath: rec(n, r) }, { clipPath: rec(n + 1, r), duration: 0.24, ease: "power2.out", immediateRender: n === 1 }, tg));
275
+ livreCresce = tg + 0.24;
276
+ }
277
+ // o envio: o cursor pisca esperando, o botão dá um toque e três anéis acendem em sequência (sem escala)
278
+ const tq = tEnv == null ? tU + 0.9 : tEnv, tp = agenda(Math.max(tq - 0.6, tU + 0.25), 0.55, true);
279
+ pisca(tU + 0.15, tp);
280
+ tl.set(cur, { opacity: 0 }, tp + 0.05);
281
+ tl.fromTo("#k2dm-enviar", { y: 0 }, { y: 4 * u, duration: 0.09, ease: "power1.out", yoyo: true, repeat: 1 }, tp);
282
+ aneis.forEach((a, i) => {
283
+ tl.fromTo(a, { opacity: 0 }, { opacity: 0.9, duration: 0.1, ease: "power1.out" }, tp + 0.1 * i);
284
+ tl.fromTo(a, { opacity: 0.9 }, { opacity: 0, duration: 0.22, ease: "power1.in", immediateRender: false }, tp + 0.1 * i + 0.1);
285
+ });
286
+ // o selo de resultado entra logo depois do toque, na palavra dita (a cor passa como uma cortina)
287
+ if (PARAM.selo) {
288
+ const t = agenda(tq - 0.1, 0.7, true);
289
+ tl.fromTo(sel, { opacity: 0, y: 26 * u }, { opacity: 1, y: 0, duration: 0.4, ease: "back.out(1.7)" }, t);
290
+ tl.fromTo("#k2dm-selo-cor", { opacity: 1, clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.3, ease: "power2.inOut" }, t + 0.05);
291
+ tl.fromTo("#k2dm-selo-ck", { opacity: 0 }, { opacity: 1, duration: 0.15, ease: "power1.out" }, t + 0.2);
292
+ tl.fromTo("#k2dm-selo-ck svg", { clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.22, ease: "power2.out" }, t + 0.28);
293
+ tl.fromTo("#k2dm-selo-txt", { opacity: 0, x: -12 * u }, { opacity: 1, x: 0, duration: 0.25, ease: "power2.out" }, t + 0.22);
294
+ }
295
+ 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)`);
296
+ if (PARAM.saida > 0) tl.fromTo(["#k2dm-fundo", "#k2dm-bloco"], { opacity: 1 }, { opacity: 0, duration: PARAM.saida, ease: "power1.in", immediateRender: false }, Math.max(0, DUR - PARAM.saida));
297
+ window.__timelines["k2d_comando"] = tl;
298
+ };
299
+ // a letra de verdade precisa estar carregada antes de medir; a timeline só é registrada no fim
300
+ (document.fonts && document.fonts.load ? Promise.all([document.fonts.load(`800 ${Math.round(fs)}px Montserrat`), document.fonts.load(`900 ${Math.round(fsS)}px Montserrat`)]) : Promise.resolve()).then(montar, montar);
301
+ })();
302
+ </script>
303
+ </template>
304
+ </body></html>
@@ -0,0 +1,217 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR"><head><meta charset="UTF-8"/><title>k2d · conversa que muda de estado (celular)</title></head>
3
+ <body>
4
+ <!--
5
+ Kit 2D (a): um celular com uma conversa que muda de estado sem mudar de desenho nem de lugar. O celular aparece
6
+ parado com as mensagens do começo; a cada ação dita, só uma coisa muda: um balão novo entra no espaço que já
7
+ estava reservado para ele (com os três pontinhos antes, se quiser), ou o selo do topo troca de texto e de cor
8
+ ("AGUARDANDO" vira "PEDIDO CONFIRMADO"). Uma ação de cada vez, com tudo parado depois de cada uma. Nenhuma
9
+ mensagem sobe nem muda de lugar: o espaço de todas é calculado desde o começo.
10
+ Serve para atendimento, venda pelo WhatsApp, pedido que se confirma. Só texto que existe (print, fala, o que
11
+ a pessoa mandou); nome e foto de cliente só com autorização. Guia: references/animacao.md ("Kit 2D").
12
+ Para usar: copie para compositions/cenas/<nome>.html, troque "k2d_conversa" (template, raiz e ID) e o prefixo
13
+ "k2dc-", mude os PARÂMETROS e ligue no projeto.json em "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_conversa-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
+ #k2dc-fundo{position:absolute;inset:0;opacity:0}
22
+ #k2dc-celular{position:absolute;box-sizing:border-box;opacity:0}
23
+ #k2dc-tela{position:absolute;overflow:hidden}
24
+ #k2dc-topo{position:absolute;left:0;right:0;top:0}
25
+ #k2dc-avatar{position:absolute;display:flex;align-items:center;justify-content:center;border-radius:50%;font-weight:900;color:#fff}
26
+ #k2dc-contato{position:absolute;display:block;font-weight:800;white-space:nowrap}
27
+ #k2dc-status{position:absolute;overflow:hidden}
28
+ .k2dc-status-cor{position:absolute;inset:0;opacity:0}
29
+ .k2dc-status-txt{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-weight:900;color:#fff;white-space:nowrap;opacity:0}
30
+ .k2dc-balao{position:absolute;box-sizing:border-box;opacity:0}
31
+ .k2dc-msg{display:block;font-weight:600;line-height:1.22}
32
+ .k2dc-digitando{position:absolute;box-sizing:border-box;opacity:0}
33
+ .k2dc-ponto{position:absolute;border-radius:50%}
34
+ #k2dc-entalhe{position:absolute}
35
+ </style>
36
+ <div id="root" data-composition-id="k2d_conversa" data-width="1080" data-height="1920" data-duration="7">
37
+ <div id="k2dc-fundo"></div>
38
+ <div id="k2dc-celular">
39
+ <div id="k2dc-tela">
40
+ <div id="k2dc-topo"><div id="k2dc-avatar">C</div><span id="k2dc-contato">Cliente</span><div id="k2dc-status"></div></div>
41
+ </div>
42
+ <div id="k2dc-entalhe"></div>
43
+ </div>
44
+ </div>
45
+ <script>
46
+ (function () {
47
+ const ID = "k2d_conversa";
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
+ contato: "Cliente", // nome no topo (nome real só com autorização; senão um neutro)
54
+ status: { texto: "AGUARDANDO", cor: "base" }, // o selo do topo no começo (cor: nome do estilo ou "#rrggbb")
55
+ // mensagens que já estão na tela quando o celular aparece. de: "ela" (a outra pessoa, à esquerda) ou "voce"
56
+ mensagens: [
57
+ { de: "ela", texto: "Oi! Ainda tem o kit com 3 peças?" },
58
+ ],
59
+ // as ações, uma de cada vez, na ordem da fala:
60
+ // "mensagem": um balão novo (de, texto); digitando: true mostra os três pontinhos antes, no mesmo lugar
61
+ // "status": o selo do topo troca de texto e de cor
62
+ acoes: [
63
+ { tipo: "mensagem", de: "voce", texto: "Tem sim! Separei um pra você.", digitando: true, em: { palavra: "respondi", padrao: 1.0 } },
64
+ { tipo: "mensagem", de: "ela", texto: "Fechado, paguei no PIX.", em: { palavra: "pagou", padrao: 3.0 } },
65
+ { tipo: "status", texto: "PEDIDO CONFIRMADO", cor: "verde", em: { palavra: "confirmado", padrao: 4.3 } },
66
+ ],
67
+ entra: 0, // quando o celular aparece (fica parado uns "pausa" segundos antes da 1ª ação)
68
+ pausa: 0.6, // tudo parado pelo menos isso depois de cada ação
69
+ legenda: true, // tela cheia: deixa livre a faixa da legenda (a cena usa os 70% de cima da zona segura).
70
+ // Com false o celular fica maior: esconda a legenda no trecho ("esconde_legenda": true)
71
+ fundo: "auto", // "auto" (papel em tela cheia, transparente na moldura), "papel", "#rrggbb" ou null
72
+ area: null, // [x0, y0, x1, y1] em px; null = área livre da moldura ou a zona segura
73
+ entrada: 0.2,
74
+ saida: 0.25,
75
+ };
76
+ // ------------------------------------------------------------------ dados do build.py (padrões para abrir sozinha)
77
+ const C = Object.assign({ palavras: [], duracao: 7, largura: 1080, altura: 1920, seguro: [140, 230, 940, 1530], rosto: null, moldura: null,
78
+ cores: { papel: "#fffdfd", linha: "#e6ddf8", base: "#c7b7ef", destaque: "#665080", tinta: "#352c40", verde: "#2e9e5b", vermelho: "#e5484d" } },
79
+ (window.__cenas || {})[ID] || {});
80
+ const DUR = C.duracao, U = Math.min(C.largura, C.altura) / 1080;
81
+ // 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
82
+ const raiz = document.getElementById("k2dc-fundo").parentElement;
83
+ if (raiz.offsetWidth && (Math.abs(raiz.offsetWidth - C.largura) > 2 || Math.abs(raiz.offsetHeight - C.altura) > 2))
84
+ 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`);
85
+ // ------------------------------------------------------------------ ajudantes do kit 2D (iguais em todas as cenas do kit; não precisa mexer)
86
+ const sem = (s) => String(s).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
87
+ 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; };
88
+ const momento = (m, padrao) => (typeof m === "number" ? m : typeof m === "string" ? palavra(m, 0, padrao)
89
+ : m && typeof m === "object" ? palavra(m.palavra || "", m.depois == null ? 0 : momento(m.depois, 0), m.padrao == null ? padrao : m.padrao) : padrao);
90
+ // fila: uma ação de cada vez; a próxima espera a anterior terminar e ficar parada PARAM.pausa segundos
91
+ let fimUltima = C.moldura ? Math.max(0, C.moldura.fechada_em || 0) : 0, livre = fimUltima + PARAM.entrada;
92
+ function agenda(quer, dur, colada) {
93
+ const t = Math.max(quer, colada ? fimUltima : livre);
94
+ 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"`);
95
+ fimUltima = t + dur; livre = fimUltima + PARAM.pausa; return t;
96
+ }
97
+ const cor = (n) => C.cores[n] || n;
98
+ 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)); };
99
+ const mistura = (a, b, k) => "#" + rgb(a).map((v, i) => Math.round(v + (rgb(b)[i] - v) * k).toString(16).padStart(2, "0")).join("");
100
+ // 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
101
+ 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; };
102
+ const larg = (t, px) => [...String(t)].reduce((s, ch) => s + peso(ch), 0) * px * 1.1;
103
+ 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; }, []);
104
+ 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); };
105
+ // onde a cena fica: PARAM.area, a área livre da moldura ou a zona segura (sem a faixa da legenda, com PARAM.legenda)
106
+ const A = PARAM.area ? PARAM.area.slice() : C.moldura && C.moldura.area_livre ? C.moldura.area_livre.slice()
107
+ : [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]];
108
+ const AW = A[2] - A[0], AH = A[3] - A[1];
109
+ const fundo = PARAM.fundo === "auto" ? (C.moldura ? null : "papel") : PARAM.fundo;
110
+ function pintaFundo(el) {
111
+ if (fundo === "papel" && C.fundo_css) el.style.cssText += ";" + C.fundo_css; // estilo escuro: o fundo do próprio estilo (grade e brilho)
112
+ 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)` }); }
113
+ else if (fundo) el.style.backgroundColor = fundo;
114
+ }
115
+ 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; };
116
+ const px = (v) => `${v}px`;
117
+ 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
118
+ // letra sobre uma cor: escura em fundo claro, branca em fundo escuro. "escuro" é a tinta do estilo (a letra escura de
119
+ // papelaria e noite) ou, num estilo escuro (tinta clara), o papel
120
+ const escuro = claro(C.cores.tinta) ? C.cores.papel : C.cores.tinta;
121
+ const sobre = (fundo) => (claro(fundo) ? escuro : "#ffffff");
122
+
123
+ // ------------------------------------------------------------------ o celular: tamanho pela área, telas e topo
124
+ const $ = (id) => document.getElementById(id);
125
+ pintaFundo($("k2dc-fundo"));
126
+ const corpo = claro(C.cores.tinta) ? C.cores.linha : C.cores.tinta; // o corpo do celular: tinta (escura) em papelaria e noite, cinza em estilo escuro
127
+ const ch = Math.min(AH, AW / 0.54), cw = ch * 0.54;
128
+ const X = A[0] + (AW - cw) / 2, Y = A[1] + (AH - ch) / 2, b = cw * 0.035, sw = cw - 2 * b, sh = ch - 2 * b;
129
+ Object.assign($("k2dc-celular").style, { left: px(X), top: px(Y), width: px(cw), height: px(ch), borderRadius: px(cw * 0.13), background: corpo,
130
+ boxShadow: `0 ${16 * U}px 0 -4px ${C.cores.base}` }); // sombra dura: a desfocada varia uns tons de um render para outro na placa de vídeo
131
+ Object.assign($("k2dc-tela").style, { left: px(b), top: px(b), width: px(sw), height: px(sh), borderRadius: px(cw * 0.1), background: mistura(C.cores.linha, C.cores.papel, 0.35) });
132
+ Object.assign($("k2dc-entalhe").style, { left: px((cw - cw * 0.3) / 2), top: px(b + cw * 0.025), width: px(cw * 0.3), height: px(cw * 0.055), borderRadius: px(cw * 0.03), background: corpo });
133
+ const th = sw * 0.34, av = sw * 0.16, pad = sw * 0.05;
134
+ Object.assign($("k2dc-topo").style, { height: px(th), background: C.cores.papel, borderBottom: `${Math.max(2, 3 * U)}px solid ${C.cores.linha}` });
135
+ Object.assign($("k2dc-avatar").style, { left: px(pad), top: px(th * 0.32), width: px(av), height: px(av), background: C.cores.destaque, color: sobre(C.cores.destaque), fontSize: px(av * 0.46) });
136
+ $("k2dc-avatar").textContent = (PARAM.contato || "?").trim().charAt(0).toUpperCase();
137
+ const cx = pad + av + pad * 0.6, cwTxt = sw - cx - pad;
138
+ Object.assign($("k2dc-contato").style, { left: px(cx), top: px(th * 0.28), width: px(cwTxt), color: C.cores.tinta, fontSize: px(cabe(PARAM.contato, cwTxt, av * 0.6, sw * 0.075, sw * 0.04, 1)) });
139
+ $("k2dc-contato").textContent = PARAM.contato;
140
+ // o selo do topo: todos os estados empilhados no mesmo lugar, com a largura do texto mais comprido
141
+ const estados = [PARAM.status].concat(PARAM.acoes.filter((a) => a.tipo === "status"));
142
+ const fsSt = sw * 0.05, stW = Math.min(cwTxt, Math.max(...estados.map((e) => larg(e.texto, fsSt))) + sw * 0.08), stH = fsSt * 1.9;
143
+ const $st = $("k2dc-status");
144
+ Object.assign($st.style, { left: px(cx), top: px(th * 0.3 + av * 0.62), width: px(stW), height: px(stH), borderRadius: px(stH / 2) });
145
+ const stCor = [], stTxt = [];
146
+ estados.forEach((e, k) => {
147
+ const fundoSt = mistura(cor(e.cor || "destaque"), C.cores.tinta, 0.2);
148
+ stCor.push(novo("div", `k2dc-status-cor-${k}`, "k2dc-status-cor", $st, { background: fundoSt }));
149
+ stTxt.push(novo("span", `k2dc-status-txt-${k}`, "k2dc-status-txt", $st, { fontSize: px(fsSt), color: sobre(fundoSt) }, e.texto));
150
+ });
151
+ // as mensagens: o lugar de todas (as do começo e as que vão chegar) calculado agora; nada sobe depois
152
+ const todas = PARAM.mensagens.map((m) => ({ ...m, inicial: true })).concat(PARAM.acoes.filter((a) => a.tipo === "mensagem"));
153
+ const chatY = th + pad, chatH = sh - chatY - pad, bw = sw * 0.8;
154
+ let fs = sw * 0.08, lugares = [];
155
+ for (let tenta = 0; tenta < 30; tenta++) {
156
+ lugares = []; let y = chatY;
157
+ const pb = fs * 0.55, ph = fs * 0.75;
158
+ todas.forEach((m) => {
159
+ const ls = linhas(m.texto, fs, bw - 2 * ph), w = Math.min(bw, Math.max(...ls.map((l) => larg(l, fs))) + 2 * ph), h = ls.length * fs * 1.22 + 2 * pb;
160
+ lugares.push({ x: m.de === "voce" ? sw - pad - w : pad, y, w, h, ph, pb });
161
+ y += h + fs * 0.55;
162
+ });
163
+ if (y - fs * 0.55 <= chatY + chatH) break;
164
+ fs *= 0.93;
165
+ }
166
+ const baloes = [], digitando = [];
167
+ todas.forEach((m, k) => {
168
+ const r = lugares[k], voce = m.de === "voce";
169
+ const bal = novo("div", `k2dc-balao-${k}`, "k2dc-balao", $("k2dc-tela"), { left: px(r.x), top: px(r.y), width: px(r.w), height: px(r.h), padding: `${r.pb}px ${r.ph}px`,
170
+ borderRadius: px(fs * 0.8), background: voce ? C.cores.destaque : C.cores.papel, border: voce ? "none" : `${Math.max(2, 3 * U)}px solid ${C.cores.linha}` });
171
+ novo("span", `k2dc-msg-${k}`, "k2dc-msg", bal, { fontSize: px(fs), color: voce ? sobre(C.cores.destaque) : C.cores.tinta }, m.texto);
172
+ baloes.push(bal);
173
+ if (m.digitando) {
174
+ const dw = fs * 3.4, dh = fs * 1.9;
175
+ const dig = novo("div", `k2dc-digitando-${k}`, "k2dc-digitando", $("k2dc-tela"), { left: px(voce ? sw - pad - dw : pad), top: px(r.y), width: px(dw), height: px(dh),
176
+ borderRadius: px(fs * 0.8), background: voce ? mistura(C.cores.destaque, C.cores.papel, 0.25) : C.cores.papel });
177
+ digitando[k] = { el: dig, pontos: [0, 1, 2].map((j) => novo("div", `k2dc-ponto-${k}-${j}`, "k2dc-ponto", dig, { left: px(dw * (0.26 + 0.24 * j) - fs * 0.16), top: px(dh / 2 - fs * 0.16),
178
+ width: px(fs * 0.32), height: px(fs * 0.32), background: voce ? C.cores.papel : C.cores.base })) };
179
+ }
180
+ });
181
+
182
+ // ------------------------------------------------------------------ a linha do tempo
183
+ const tl = gsap.timeline({ paused: true });
184
+ tl.fromTo("#k2dc-fundo", { opacity: 0 }, { opacity: 1, duration: Math.max(0.01, PARAM.entrada), ease: "power1.out" }, 0);
185
+ const tE = agenda(momento(PARAM.entra, 0), 0.45);
186
+ tl.fromTo("#k2dc-celular", { opacity: 0, y: 70 * U }, { opacity: 1, y: 0, duration: 0.45, ease: "back.out(1.4)" }, tE);
187
+ tl.fromTo([stCor[0], stTxt[0]].concat(baloes.filter((_, k) => todas[k].inicial)), { opacity: 0 }, { opacity: 1, duration: 0.01 }, tE);
188
+ let nMsg = PARAM.mensagens.length, nSt = 0;
189
+ PARAM.acoes.forEach((a, n) => {
190
+ const quer = momento(a.em, 1 + n * 1.5) - 0.1;
191
+ if (a.tipo === "mensagem") {
192
+ const k = nMsg++, d = digitando[k], dur = d ? 1.45 : 0.35, t = agenda(quer, dur);
193
+ let tb = t;
194
+ if (d) {
195
+ // nada anima escala no kit: o Chrome guarda o desenho da escala antiga por um tempo e o render deixa de sair igual
196
+ tl.fromTo(d.el, { opacity: 0, y: 12 * U }, { opacity: 1, y: 0, duration: 0.2, ease: "back.out(2)" }, t);
197
+ d.pontos.forEach((p, j) => tl.fromTo(p, { y: 0 }, { y: -fs * 0.22, duration: 0.18, ease: "sine.inOut", yoyo: true, repeat: 3 }, t + 0.2 + j * 0.1));
198
+ tl.to(d.el, { opacity: 0, duration: 0.12, ease: "power1.in", immediateRender: false }, t + 1.0);
199
+ tb = t + 1.1;
200
+ }
201
+ tl.fromTo(baloes[k], { opacity: 0, y: 22 * U }, { opacity: 1, y: 0, duration: 0.35, ease: "back.out(1.7)" }, tb);
202
+ } else if (a.tipo === "status") {
203
+ const k = ++nSt, t = agenda(quer, 0.55);
204
+ tl.to(stTxt[k - 1], { opacity: 0, duration: 0.15, ease: "power1.in", immediateRender: false }, t);
205
+ // a cor nova passa como uma cortina (misturar as duas por transparência dá um tom sujo no meio)
206
+ tl.fromTo(stCor[k], { opacity: 1, clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.3, ease: "power2.inOut" }, t + 0.1);
207
+ tl.fromTo(stTxt[k], { opacity: 0, y: fsSt * 0.4 }, { opacity: 1, y: 0, duration: 0.2, ease: "power2.out" }, t + 0.35);
208
+ tl.fromTo($st, { y: 0 }, { y: -6 * U, duration: 0.12, ease: "power1.out", yoyo: true, repeat: 1 }, t + 0.35);
209
+ }
210
+ });
211
+ 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)`);
212
+ if (PARAM.saida > 0) tl.fromTo(["#k2dc-fundo", "#k2dc-celular"], { opacity: 1 }, { opacity: 0, duration: PARAM.saida, ease: "power1.in", immediateRender: false }, Math.max(0, DUR - PARAM.saida));
213
+ window.__timelines["k2d_conversa"] = tl;
214
+ })();
215
+ </script>
216
+ </template>
217
+ </body></html>