primocode 10.16.0 → 10.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) 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 +28 -8
  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 +36 -8
  198. package/lib/catalogo.js +4 -0
  199. package/lib/chamada-torta.js +81 -0
  200. package/lib/editor-pro.js +786 -0
  201. package/lib/entrega.js +58 -0
  202. package/lib/estudio/conferencia-visual.js +76 -0
  203. package/lib/estudio/diretor.js +6 -1
  204. package/lib/estudio/exportar.js +3 -1
  205. package/lib/estudio/gosto.js +45 -4
  206. package/lib/estudio/index.js +8 -5
  207. package/lib/estudio/kit-ae/base.jsx +119 -13
  208. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  209. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  210. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  211. package/lib/estudio/maquina-diretor.js +61 -7
  212. package/lib/estudio/maquina-foto.js +177 -0
  213. package/lib/estudio/maquina-guarda.js +207 -0
  214. package/lib/estudio/maquina-layout.js +22 -1
  215. package/lib/estudio/maquina.js +60 -11
  216. package/lib/estudio/referencias.js +1 -1
  217. package/lib/estudio/visao/visao.swift +97 -0
  218. package/lib/estudio/visao.js +60 -0
  219. package/lib/local/cerebro.json +192 -6
  220. package/lib/local/motor.js +5 -2
  221. package/lib/mcp.js +105 -7
  222. package/lib/nuvem.js +11 -1
  223. package/lib/outra-pasta.js +1 -1
  224. package/lib/passagem.js +50 -0
  225. package/lib/perigo.js +29 -12
  226. package/lib/protecao.js +134 -0
  227. package/lib/sandbox.js +11 -4
  228. package/lib/titulo-conversa.js +46 -0
  229. package/lib/tools.js +3 -0
  230. package/lib/ui.js +1 -1
  231. package/package.json +3 -2
  232. package/skills/editar-meu-video/SKILL.md +6 -2
  233. package/skills/editar-video/SKILL.md +113 -0
  234. package/studio/pessoa.py +52 -0
  235. package/voz/primo_voz/app.py +43 -4
  236. package/voz/primo_voz/chat.html +17 -6
  237. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,233 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR"><head><meta charset="UTF-8"/><title>k2d · bloco de notas com a lista que se conclui</title></head>
3
+ <body>
4
+ <!--
5
+ Kit 2D (a): um bloco de notas, como o app de notas do celular. O cartão aparece parado, com o título e as linhas
6
+ pautadas. Cada item da lista é escrito na palavra dita, da esquerda para a direita (um recorte que abre, nunca a
7
+ letra mudando de tamanho), com uma bolinha vazia ao lado. Quando a pessoa conclui o item, na palavra do check, a
8
+ bolinha vira um check verde e o texto escurece um pouco. Um item (em geral o último) pode ficar em destaque, com a
9
+ linha pintada na cor de destaque do estilo. O texto longo quebra em mais linhas e a linha cresce, sem estourar o
10
+ cartão: o lugar de cada linha é calculado antes da primeira ação, e o cartão inteiro cabe na área.
11
+ Serve para "primeiro isso, depois aquilo", "o que eu já fiz", lista de pontos que vão sendo concluídos. Só os
12
+ itens que a pessoa de fato diz; a cena não inventa item nem marca como feito o que a fala não diz que foi feito.
13
+ Guia: references/animacao.md ("Kit 2D"). Para usar: copie para compositions/cenas/<nome>.html, troque "k2d_notas"
14
+ (template, raiz e ID) e o prefixo "k2dn-", 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) o bloco vira um cartão pequeno acima da
16
+ cabeça, sem fundo, e se ajusta à altura que sobra (com poucos itens e sem título cabe maior). 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_notas-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
+ #k2dn-fundo{position:absolute;inset:0;opacity:0}
26
+ #k2dn-cartao{position:absolute;opacity:0}
27
+ #k2dn-borda,#k2dn-corpo,#k2dn-conteudo{position:absolute}
28
+ #k2dn-titulo{position:absolute;display:block;white-space:nowrap;font-weight:900}
29
+ #k2dn-divisor{position:absolute}
30
+ .k2dn-regua{position:absolute}
31
+ .k2dn-item{position:absolute;opacity:0}
32
+ .k2dn-faixa{position:absolute;opacity:0}
33
+ .k2dn-bola{position:absolute;box-sizing:border-box;border-radius:50%}
34
+ .k2dn-ok{position:absolute;border-radius:50%;opacity:0}
35
+ .k2dn-ok svg{position:absolute;left:20%;top:20%;width:60%;height:60%;fill:none;stroke:currentColor;stroke-width:6;stroke-linecap:round;stroke-linejoin:round}
36
+ .k2dn-texto{position:absolute;display:block;font-weight:800;overflow-wrap:anywhere;opacity:0}
37
+ </style>
38
+ <div id="root" data-composition-id="k2d_notas" data-width="1080" data-height="1920" data-duration="7">
39
+ <div id="k2dn-fundo"></div>
40
+ <div id="k2dn-cartao">
41
+ <div id="k2dn-borda"></div>
42
+ <div id="k2dn-corpo"></div>
43
+ <div id="k2dn-conteudo"><span id="k2dn-titulo">Hoje</span><div id="k2dn-divisor"></div></div>
44
+ </div>
45
+ </div>
46
+ <script>
47
+ (function () {
48
+ const ID = "k2d_notas";
49
+ // ------------------------------------------------------------------ PARÂMETROS (troque aqui)
50
+ // Tempo: número (segundos da cena), "palavra" ou {palavra, depois, padrao}. UMA palavra, procurada na fala do
51
+ // trecho sem acento e pelo começo; "depois" pula as repetições anteriores; "padrao" vale quando a palavra não
52
+ // aparece (cena aberta sozinha no estúdio, transcrição que mudou).
53
+ const PARAM = {
54
+ titulo: "Hoje", // o título do bloco; null = sem título (o cartão fica menor e a letra maior)
55
+ // os itens, um por linha, na ordem da fala. texto: o que é escrito (quebra em mais linhas se for longo); palavra: a palavra falada em que
56
+ // ele é escrito; check: { palavra, padrao } = a palavra em que a bolinha vira check (sem "check" a bolinha fica aberta; true = logo depois de
57
+ // escrito); destaque: true pinta a linha na cor de destaque do estilo (em geral só o último item)
58
+ itens: [
59
+ { texto: "revisar o texto", palavra: { palavra: "revisar", padrao: 1.2 }, check: { palavra: "pronto", padrao: 2.6 } },
60
+ { texto: "ajustar as cores", palavra: { palavra: "ajustar", padrao: 3.4 }, check: { palavra: "feito", padrao: 4.6 } },
61
+ { texto: "publicar o vídeo", palavra: { palavra: "publicar", padrao: 5.2 }, destaque: true },
62
+ ],
63
+ entra: 0, // quando o cartão aparece (fica parado uns "pausa" segundos antes do 1º item)
64
+ pausa: 0.4, // tudo parado pelo menos isso depois de cada ação (um item leva 0,5 s para ser escrito)
65
+ legenda: true, // tela cheia: deixa livre a faixa da legenda (a cena usa os 70% de cima da zona segura)
66
+ // Com false a cena usa a zona segura inteira: esconda a legenda no trecho ("esconde_legenda": true)
67
+ fundo: "auto", // "auto" (papel em tela cheia, transparente na moldura), "papel", "#rrggbb" ou null
68
+ 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);
69
+ // null = área livre da moldura ou a zona segura
70
+ entrada: 0.2,
71
+ saida: 0.25,
72
+ };
73
+ // ------------------------------------------------------------------ dados do build.py (padrões para abrir sozinha)
74
+ const C = Object.assign({ palavras: [], duracao: 7, largura: 1080, altura: 1920, seguro: [140, 230, 940, 1530], rosto: null, moldura: null,
75
+ cores: { papel: "#fffdfd", linha: "#e6ddf8", base: "#c7b7ef", destaque: "#665080", tinta: "#352c40", verde: "#2e9e5b", vermelho: "#e5484d" } },
76
+ (window.__cenas || {})[ID] || {});
77
+ const DUR = C.duracao, U = Math.min(C.largura, C.altura) / 1080;
78
+ // 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
79
+ const raiz = document.getElementById("k2dn-fundo").parentElement;
80
+ if (raiz.offsetWidth && (Math.abs(raiz.offsetWidth - C.largura) > 2 || Math.abs(raiz.offsetHeight - C.altura) > 2))
81
+ 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`);
82
+ // ------------------------------------------------------------------ "acima_do_rosto": o cartão acima da cabeça, sem cobrir o rosto
83
+ // area: "acima_do_rosto" vira [seguro.x0, seguro.y0 + 20, seguro.x1, rosto.topo + 40] (sem rosto, a faixa de cima da zona segura,
84
+ // 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
85
+ // 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.
86
+ // Na moldura o lugar já é a área livre acima do quadro.
87
+ const ZS = C.seguro, ROSTO = C.rosto, ACIMA = PARAM.area === "acima_do_rosto" && !C.moldura;
88
+ if (PARAM.area === "acima_do_rosto") {
89
+ if (!ACIMA) PARAM.area = null;
90
+ else {
91
+ const ya = ZS[1] + 20, yb = ROSTO ? ROSTO.topo + 40 : Math.round(ZS[1] + (ZS[3] - ZS[1]) * 0.25);
92
+ 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`);
93
+ PARAM.area = [ZS[0], ya, ZS[2], Math.max(yb, ya + 160)];
94
+ if (PARAM.fundo === "auto") PARAM.fundo = null;
95
+ }
96
+ }
97
+ // 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
98
+ // (a pessoa aparece por baixo) e quando a área não é a "acima_do_rosto", que já fica acima da cabeça
99
+ 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)
100
+ console.warn(`[${ID}] o cartão cobre o rosto: use area: "acima_do_rosto" ou desça/suba a area`); };
101
+ // ------------------------------------------------------------------ ajudantes do kit 2D (iguais em todas as cenas do kit; não precisa mexer)
102
+ const sem = (s) => String(s).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
103
+ 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; };
104
+ const momento = (m, padrao) => (typeof m === "number" ? m : typeof m === "string" ? palavra(m, 0, padrao)
105
+ : m && typeof m === "object" ? palavra(m.palavra || "", m.depois == null ? 0 : momento(m.depois, 0), m.padrao == null ? padrao : m.padrao) : padrao);
106
+ // fila: uma ação de cada vez; a próxima espera a anterior terminar e ficar parada PARAM.pausa segundos
107
+ let fimUltima = C.moldura ? Math.max(0, C.moldura.fechada_em || 0) : 0, livre = fimUltima + PARAM.entrada;
108
+ function agenda(quer, dur, colada) {
109
+ const t = Math.max(quer, colada ? fimUltima : livre);
110
+ 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"`);
111
+ fimUltima = t + dur; livre = fimUltima + PARAM.pausa; return t;
112
+ }
113
+ const cor = (n) => C.cores[n] || n;
114
+ 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)); };
115
+ const mistura = (a, b, k) => "#" + rgb(a).map((v, i) => Math.round(v + (rgb(b)[i] - v) * k).toString(16).padStart(2, "0")).join("");
116
+ // 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
117
+ 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; };
118
+ const larg = (t, px) => [...String(t)].reduce((s, ch) => s + peso(ch), 0) * px * 1.1;
119
+ 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; }, []);
120
+ 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); };
121
+ // onde a cena fica: PARAM.area, a área livre da moldura ou a zona segura (sem a faixa da legenda, com PARAM.legenda)
122
+ const A = PARAM.area ? PARAM.area.slice() : C.moldura && C.moldura.area_livre ? C.moldura.area_livre.slice()
123
+ : [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]];
124
+ const AW = A[2] - A[0], AH = A[3] - A[1];
125
+ const fundo = PARAM.fundo === "auto" ? (C.moldura ? null : "papel") : PARAM.fundo;
126
+ function pintaFundo(el) {
127
+ if (fundo === "papel" && C.fundo_css) el.style.cssText += ";" + C.fundo_css; // estilo escuro: o fundo do próprio estilo (grade e brilho)
128
+ 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)` }); }
129
+ else if (fundo) el.style.backgroundColor = fundo;
130
+ }
131
+ 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; };
132
+ const px = (v) => `${v}px`;
133
+ 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
134
+ // letra sobre uma cor: escura em fundo claro, branca em fundo escuro. "escuro" é a tinta do estilo (a letra escura de
135
+ // papelaria e noite) ou, num estilo escuro (tinta clara), o papel
136
+ const escuro = claro(C.cores.tinta) ? C.cores.papel : C.cores.tinta;
137
+ const sobre = (fundo) => (claro(fundo) ? escuro : "#ffffff");
138
+
139
+ // ------------------------------------------------------------------ o bloco: título, linhas pautadas e uma linha por item (cada uma nasce invisível até a vez dela)
140
+ const $ = (id) => document.getElementById(id);
141
+ pintaFundo($("k2dn-fundo"));
142
+ const TIT = PARAM.titulo == null ? "" : String(PARAM.titulo).trim();
143
+ const IT = (Array.isArray(PARAM.itens) ? PARAM.itens : []).filter((it) => it && String(it.texto || "").trim());
144
+ if (!IT.length) { console.error(`[${ID}] o parâmetro "itens" está vazio: escreva as linhas da lista`); IT.push({ texto: "?", palavra: 1 }); }
145
+ const cV = cor("verde"), cD = C.cores.destaque, cont = $("k2dn-conteudo"), tit = $("k2dn-titulo"), div = $("k2dn-divisor");
146
+ const linhasEl = IT.map((it, i) => {
147
+ const destaque = !!it.destaque, letra = destaque ? sobre(cD) : C.cores.tinta;
148
+ const el = novo("div", `k2dn-item-${i}`, "k2dn-item", cont);
149
+ const faixa = destaque ? novo("div", `k2dn-faixa-${i}`, "k2dn-faixa", el, { background: cD }) : null;
150
+ const bola = novo("div", `k2dn-bola-${i}`, "k2dn-bola", el);
151
+ const ok = novo("div", `k2dn-ok-${i}`, "k2dn-ok", el, { background: cV, color: sobre(cV) });
152
+ ok.innerHTML = `<svg viewBox="0 0 44 44"><path d="M11 23L19 31L33 14"/></svg>`;
153
+ const txt = novo("div", `k2dn-texto-${i}`, "k2dn-texto", el, { color: letra }, String(it.texto).trim());
154
+ const regua = i < IT.length - 1 ? novo("div", `k2dn-regua-${i}`, "k2dn-regua", cont, { background: C.cores.linha }) : null;
155
+ return { it, destaque, el, faixa, bola, ok, txt, regua, aro: destaque ? sobre(cD) : mistura(C.cores.tinta, C.cores.papel, 0.55) };
156
+ });
157
+ if (!TIT) { tit.style.display = "none"; div.style.display = "none"; } else tit.textContent = TIT;
158
+
159
+ // ------------------------------------------------------------------ as medidas numa escala k (o tamanho de cada linha vem do navegador, com a fonte carregada)
160
+ // em área baixa e larga (o cartão acima da cabeça) o bloco usa a largura toda: a letra cresce até a altura que sobra, em vez de a largura encolher junto com ela
161
+ const baixa = AH / AW < 0.45;
162
+ const poe = (k) => {
163
+ const u = U * k, bw = Math.max(3, 4 * u), W = Math.min(AW - 40 * u, baixa ? Math.max(820 * u, 4.6 * AH) : Math.max(820 * u, 600 * U)), Wc = W - 2 * bw, P = 30 * u, fs = Math.round(46 * u), lh = Math.round(fs * 1.28);
164
+ const d = Math.round(fs * 0.88), xt = P + d + 22 * u, tw = Wc - xt - P, pv = (baixa ? 12 : 18) * u, dv = Math.max(2, 3 * u);
165
+ let y = 8 * u;
166
+ if (TIT) {
167
+ const fsT = cabe(TIT, Wc - 2 * P, AH, (baixa ? 46 : 54) * u, 24 * u, 1), hT = Math.round(fsT * 1.25), ti = (baixa ? 14 : 24) * u, gp = (baixa ? 8 : 14) * u;
168
+ Object.assign(tit.style, { left: px(P), top: px(ti), fontSize: px(fsT), lineHeight: px(hT), color: C.cores.tinta });
169
+ Object.assign(div.style, { left: px(P), top: px(ti + hT + gp), width: px(Wc - 2 * P), height: px(dv), background: C.cores.linha });
170
+ y = ti + hT + gp + dv + 4 * u;
171
+ }
172
+ linhasEl.forEach((r) => {
173
+ Object.assign(r.txt.style, { left: px(xt), top: px(pv), width: px(tw), fontSize: px(fs), lineHeight: px(lh) });
174
+ const h = 2 * pv + r.txt.offsetHeight, y0 = pv + (lh - d) / 2;
175
+ Object.assign(r.el.style, { left: 0, top: px(y), width: px(Wc), height: px(h) });
176
+ Object.assign(r.bola.style, { left: px(P), top: px(y0), width: px(d), height: px(d), border: `${Math.max(3, 4 * u)}px solid ${r.aro}` });
177
+ Object.assign(r.ok.style, { left: px(P), top: px(y0), width: px(d), height: px(d) });
178
+ if (r.faixa) Object.assign(r.faixa.style, { left: px(10 * u), top: px(5 * u), width: px(Wc - 20 * u), height: px(h - 10 * u), borderRadius: px(Math.min(26 * u, (h - 10 * u) / 2)) });
179
+ if (r.regua) Object.assign(r.regua.style, { left: px(P), top: px(y + h - dv / 2), width: px(Wc - 2 * P), height: px(dv) });
180
+ y += h;
181
+ });
182
+ return { u, bw, W, Wc, H: y + 8 * u + 2 * bw, S: 12 * u };
183
+ };
184
+
185
+ // ------------------------------------------------------------------ a linha do tempo (montada com a fonte carregada, porque a altura de cada linha é medida no navegador)
186
+ const montar = () => {
187
+ // a maior escala em que o cartão inteiro (com a sombra dura) cabe na área
188
+ let k = Math.min(1.35, AW / (800 * U)), g = poe(k);
189
+ while (k > 0.3 && g.H + g.S > AH) { k -= 0.02; g = poe(k); }
190
+ const { u, bw, W, H, S } = g, R = 40 * u, X = A[0] + (AW - W) / 2, Y = A[1] + (AH - H - S) / 2;
191
+ Object.assign($("k2dn-cartao").style, { left: px(X), top: px(Y), width: px(W), height: px(H + S) });
192
+ Object.assign($("k2dn-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
193
+ Object.assign($("k2dn-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 });
194
+ Object.assign(cont.style, { left: px(bw), top: px(bw), width: px(W - 2 * bw), height: px(H - 2 * bw) });
195
+ avisaRosto(X, Y, X + W, Y + H + S);
196
+ const tl = gsap.timeline({ paused: true });
197
+ tl.fromTo("#k2dn-fundo", { opacity: 0 }, { opacity: 1, duration: Math.max(0.01, PARAM.entrada), ease: "power1.out" }, 0);
198
+ const tE = agenda(momento(PARAM.entra, 0), 0.45);
199
+ // nada anima escala no kit: o Chrome guarda o desenho da escala antiga por um tempo e o render deixa de sair igual
200
+ tl.fromTo("#k2dn-cartao", { opacity: 0, y: 60 * u }, { opacity: 1, y: 0, duration: 0.45, ease: "back.out(1.5)" }, tE);
201
+ // as ações na ordem do tempo: escrever o item (começa um pouco antes da palavra) e marcar o check (nunca antes de o item estar escrito)
202
+ const acoes = [];
203
+ IT.forEach((it, i) => {
204
+ const q = momento(it.palavra, 1 + i * 1.3) - 0.1;
205
+ acoes.push({ i, q, check: false });
206
+ if (it.check) acoes.push({ i, q: Math.max(momento(it.check, q + 1.1), q + 0.5), check: true });
207
+ });
208
+ acoes.sort((a, b) => a.q - b.q || (a.check ? 1 : -1));
209
+ acoes.forEach((a) => {
210
+ const r = linhasEl[a.i], t = agenda(a.q, a.check ? 0.4 : 0.5), atraso = r.destaque ? 0.1 : 0;
211
+ if (!a.check) {
212
+ tl.fromTo(r.el, { opacity: 0 }, { opacity: 1, duration: 0.05, ease: "none" }, t);
213
+ if (r.faixa) tl.fromTo(r.faixa, { opacity: 1, clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.4, ease: "power2.out" }, t);
214
+ // o texto abre da esquerda para a direita (a linha de destaque já vai na frente, para a letra nunca cair sobre o fundo)
215
+ tl.fromTo(r.txt, { clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.45, ease: "power2.out" }, t + atraso);
216
+ tl.fromTo(r.txt, { opacity: 0 }, { opacity: 1, duration: 0.04, ease: "none", immediateRender: false }, t + atraso);
217
+ } else {
218
+ // a bolinha vira check verde: o disco entra e o traço do check se desenha; o texto escurece um pouco (menos na linha de destaque)
219
+ tl.fromTo(r.ok, { opacity: 0 }, { opacity: 1, duration: 0.14, ease: "power1.out" }, t);
220
+ tl.fromTo(r.ok.firstChild, { clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.22, ease: "power2.out" }, t + 0.1);
221
+ if (!r.destaque) tl.to(r.txt, { opacity: 0.55, duration: 0.3, ease: "power1.out", immediateRender: false }, t + 0.1);
222
+ }
223
+ });
224
+ 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 a lista parada pelo menos 0,6 s (estenda o "ate" da cena)`);
225
+ if (PARAM.saida > 0) tl.fromTo(["#k2dn-fundo", "#k2dn-cartao"], { opacity: 1 }, { opacity: 0, duration: PARAM.saida, ease: "power1.in", immediateRender: false }, Math.max(0, DUR - PARAM.saida));
226
+ window.__timelines["k2d_notas"] = tl;
227
+ };
228
+ // a letra de verdade precisa estar carregada antes de medir; a timeline só é registrada no fim
229
+ (document.fonts && document.fonts.load ? document.fonts.load(`800 ${Math.round(46 * U)}px Montserrat`).then(() => document.fonts.load(`900 ${Math.round(54 * U)}px Montserrat`)) : Promise.resolve()).then(montar, montar);
230
+ })();
231
+ </script>
232
+ </template>
233
+ </body></html>
@@ -0,0 +1,197 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR"><head><meta charset="UTF-8"/><title>k3d · caminho (câmera andando por pontos rotulados)</title></head>
3
+ <body>
4
+ <!--
5
+ Kit 3D (b): um ambiente simples (chão e blocos) com um caminho que contorna os obstáculos. A câmera abre parada
6
+ no primeiro ponto, anda junto do caminho, que vai sendo desenhado, chega em cada ponto rotulado na palavra
7
+ combinada e termina parada numa visão geral. Serve para percurso, circulação, disposição de um lugar, etapas
8
+ que ficam "em lugares" diferentes. Sem planta ou medida conferida, é esquemático. Guia: references/3d.md.
9
+ A câmera nunca entra num bloco: a cena confere isso em todos os quadros e acusa erro no check se entrar.
10
+ Para usar: copie para compositions/cenas/<nome>.html, troque "k3d_caminho" (template, raiz e ID) e o prefixo
11
+ "k3dc-", mude os PARÂMETROS e ligue no projeto.json em "cenas_proprias".
12
+ -->
13
+ <template id="k3d_caminho-template">
14
+ <style>
15
+ @font-face{font-family:Montserrat;src:url("assets/fontes/Montserrat.ttf") format("truetype");font-weight:100 900;font-display:block}
16
+ #root{position:absolute;inset:0;overflow:hidden;font-family:Montserrat}
17
+ #k3dc-palco{position:absolute;inset:0;opacity:0}
18
+ #k3dc-tela{position:absolute;left:0;top:0;width:100%;height:100%;display:block}
19
+ #k3dc-rotulos{position:absolute;inset:0}
20
+ </style>
21
+ <div id="root" data-composition-id="k3d_caminho" data-width="1080" data-height="1920" data-duration="5">
22
+ <div id="k3dc-palco"><canvas id="k3dc-tela" width="1080" height="1920"></canvas><div id="k3dc-rotulos"></div></div>
23
+ </div>
24
+ <script>
25
+ (function () {
26
+ const ID = "k3d_caminho";
27
+ // ------------------------------------------------------------------ PARÂMETROS (troque aqui)
28
+ const PARAM = {
29
+ // o caminho, em ordem, no chão (x para a direita, z para perto da câmera; unidades livres, sem escala real).
30
+ // Ponto com nome = parada rotulada, que a câmera alcança na palavra de "chega". Ponto sem nome = curva do
31
+ // caminho (passa por ali sem rótulo). O primeiro e o último precisam de "chega".
32
+ pontos: [
33
+ { nome: "Erro", x: -1.6, z: 2.9, chega: { palavra: "erro", padrao: 0.8 } },
34
+ { x: 1.5, z: 2.9 },
35
+ { nome: "Pausa", x: 1.6, z: 0.9, chega: { palavra: "pausa", padrao: 1.6 } },
36
+ { x: -1.5, z: 0.9 },
37
+ { nome: "Ééé", x: -1.6, z: -0.9, chega: { palavra: "ééé", padrao: 2.5 } },
38
+ { x: 1.5, z: -0.9 },
39
+ { nome: "Corte", x: 1.6, z: -2.9, chega: { palavra: "cortou", padrao: 3.4 } },
40
+ ],
41
+ // blocos que o caminho contorna: centro (x, z), largura (x), profundidade (z), altura
42
+ obstaculos: [
43
+ { x: -0.8, z: 1.9, largura: 3.2, profundidade: 0.45, altura: 0.8 },
44
+ { x: 0.8, z: 0.0, largura: 3.2, profundidade: 0.45, altura: 0.8 },
45
+ { x: -0.8, z: -1.9, largura: 3.2, profundidade: 0.45, altura: 0.8 },
46
+ ],
47
+ base: true, // uma base (maquete) embaixo de tudo: dá chão mesmo sem fundo
48
+ seguir: { azim: 0, elev: 48, janela: null }, // câmera acompanhando (graus); janela = largura do chão que ela
49
+ // mostra em volta do ponto da vez (null = a largura do caminho todo)
50
+ acompanha: 0.3, // quanto a câmera acompanha o caminho para os lados (0 = só avança, 1 = tudo)
51
+ final: { azim: 12, elev: 60, duracao: 0.5 }, // visão geral onde termina parada (null = fica no último ponto)
52
+ fundo: "papel", // "papel", "#rrggbb" ou null (transparente: a pessoa aparece numa moldura por baixo)
53
+ area: null, // [x0, y0, x1, y1] em px onde fica o 3D (com moldura, a área livre acima dela)
54
+ recorte: true, // com area: o 3D só aparece dentro dela (janela de cantos redondos)
55
+ sombras: true, // sombra das luzes (false deixa o render mais leve; a sombra de contato fica)
56
+ entrada: 0.2,
57
+ saida: 0.2,
58
+ };
59
+ // ------------------------------------------------------------------ dados do build.py (padrões para abrir sozinha)
60
+ const C = Object.assign({ palavras: [], duracao: 5, largura: 1080, altura: 1920, seguro: [140, 230, 940, 1530], rosto: null,
61
+ cores: { papel: "#fffdfd", linha: "#e6ddf8", base: "#c7b7ef", destaque: "#665080", tinta: "#352c40", verde: "#2e9e5b", vermelho: "#e5484d" } },
62
+ (window.__cenas || {})[ID] || {});
63
+ const DUR = C.duracao;
64
+
65
+ // curva monótona (sem voltar) que passa pelos tempos de chegada e começa e termina com velocidade zero
66
+ function curvaDeTempo(ts, us) {
67
+ const n = ts.length, m = new Array(n).fill(0), d = [];
68
+ for (let i = 0; i < n - 1; i++) d.push((us[i + 1] - us[i]) / Math.max(1e-6, ts[i + 1] - ts[i]));
69
+ for (let i = 1; i < n - 1; i++) m[i] = d[i - 1] * d[i] <= 0 ? 0 : (2 * d[i - 1] * d[i]) / (d[i - 1] + d[i]);
70
+ return (t) => {
71
+ if (t <= ts[0]) return us[0];
72
+ if (t >= ts[n - 1]) return us[n - 1];
73
+ let i = 0;
74
+ while (i < n - 2 && t > ts[i + 1]) i++;
75
+ const h = ts[i + 1] - ts[i], s = (t - ts[i]) / h, s2 = s * s, s3 = s2 * s;
76
+ return (2 * s3 - 3 * s2 + 1) * us[i] + (s3 - 2 * s2 + s) * h * m[i] + (-2 * s3 + 3 * s2) * us[i + 1] + (s3 - s2) * h * m[i + 1];
77
+ };
78
+ }
79
+
80
+ async function construir(K, P) {
81
+ const THREE = K.THREE;
82
+ P.luzes({ chave: [3, 10, 6], alvo: [0, 0, 0], area: 7 });
83
+ P.chao({ tamanho: 26 });
84
+ const larguraTudo = Math.max(...PARAM.pontos.map((p) => Math.abs(p.x)), ...PARAM.obstaculos.map((o) => Math.abs(o.x) + o.largura / 2)) * 2 + 0.9;
85
+ const fundoTudo = Math.max(...PARAM.pontos.map((p) => Math.abs(p.z)), ...PARAM.obstaculos.map((o) => Math.abs(o.z) + o.profundidade / 2)) * 2 + 0.9;
86
+ if (PARAM.base) {
87
+ const b = P.placa({ largura: larguraTudo, altura: 0.1, profundidade: fundoTudo, raio: 0.25, chanfro: 0.03, cor: C.cores.papel, lados: C.cores.base });
88
+ b.position.y = -0.1;
89
+ P.cena.add(b);
90
+ P.solido(b, "a base");
91
+ P.sombraDeContato({ largura: larguraTudo, profundidade: fundoTudo, forca: 0.35, y: -0.1 }).colocar(0, 0, 0);
92
+ }
93
+ // blocos
94
+ PARAM.obstaculos.forEach((o, i) => {
95
+ const b = P.placa({ largura: o.largura, altura: o.altura, profundidade: o.profundidade, raio: 0.12, chanfro: 0.04,
96
+ cor: o.cor || C.cores.base, lados: o.lados || "#" + new THREE.Color(o.cor || C.cores.base).multiplyScalar(0.8).getHexString() });
97
+ b.position.set(o.x, 0, o.z);
98
+ P.cena.add(b);
99
+ P.solido(b, o.nome || `bloco ${i + 1}`);
100
+ P.sombraDeContato({ largura: o.largura, profundidade: o.profundidade, forca: 0.4 }).colocar(o.x, o.z, 0);
101
+ });
102
+ // o caminho
103
+ const pts = PARAM.pontos.map((p) => new THREE.Vector3(p.x, 0.03, p.z));
104
+ const curva = new THREE.CatmullRomCurve3(pts, false, "centripetal");
105
+ const N = 400;
106
+ const comp = curva.getLengths(N), total = comp[N];
107
+ const uDoPonto = PARAM.pontos.map((_, i) => comp[Math.round((i / (PARAM.pontos.length - 1)) * N)] / total);
108
+ // o caminho não pode passar por dentro de um bloco
109
+ for (let k = 0; k <= 300; k++) {
110
+ const q = curva.getPointAt(k / 300);
111
+ PARAM.obstaculos.forEach((o, i) => {
112
+ if (Math.abs(q.x - o.x) < o.largura / 2 + 0.06 && Math.abs(q.z - o.z) < o.profundidade / 2 + 0.06) {
113
+ console.error(`[cena 3D ${ID}] o caminho passa por dentro do bloco ${i + 1} (perto de x ${q.x.toFixed(2)}, z ${q.z.toFixed(2)}): ponha um ponto de curva antes`);
114
+ k = 301;
115
+ }
116
+ });
117
+ }
118
+ // tempos: as paradas nas palavras; as curvas no meio, pela distância andada
119
+ const marcados = PARAM.pontos.map((p, i) => (p.chega != null ? { i, t: K.momento(C, p.chega, 0) } : null)).filter(Boolean);
120
+ const tempos = PARAM.pontos.map((p, i) => {
121
+ const m = marcados.find((x) => x.i === i);
122
+ if (m) return m.t;
123
+ const a = [...marcados].reverse().find((x) => x.i < i) || marcados[0], b = marcados.find((x) => x.i > i) || marcados[marcados.length - 1];
124
+ return a.i === b.i ? a.t : K.mistura(a.t, b.t, (uDoPonto[i] - uDoPonto[a.i]) / Math.max(1e-6, uDoPonto[b.i] - uDoPonto[a.i]));
125
+ });
126
+ for (let i = 1; i < tempos.length; i++) tempos[i] = Math.max(tempos[i], tempos[i - 1] + 0.05);
127
+ const uEm = curvaDeTempo(tempos, uDoPonto);
128
+ const raioTubo = 0.07, lados = 10;
129
+ const tubo = new THREE.Mesh(new THREE.TubeGeometry(curva, 600, raioTubo, lados, false),
130
+ new THREE.MeshStandardMaterial({ color: C.cores.destaque, roughness: 0.4 }));
131
+ tubo.castShadow = true;
132
+ P.cena.add(tubo);
133
+ const porAnel = lados * 6, totalIdx = tubo.geometry.index.count;
134
+ const cabeca = new THREE.Mesh(new THREE.SphereGeometry(0.16, 32, 16), P.material(C.cores.destaque, { verniz: 0.6 }));
135
+ cabeca.castShadow = true;
136
+ P.cena.add(cabeca);
137
+ // pinos nas paradas
138
+ const paradas = PARAM.pontos.map((p, i) => ({ p, i })).filter((x) => x.p.nome);
139
+ const pinos = paradas.map(({ p }) => {
140
+ const g = new THREE.Group();
141
+ const haste = P.cilindro({ raio: 0.035, altura: 0.7, cor: C.cores.tinta });
142
+ const bola = new THREE.Mesh(new THREE.SphereGeometry(0.15, 32, 16), P.material(C.cores.vermelho, { verniz: 0.6 }));
143
+ bola.position.y = 0.72;
144
+ bola.castShadow = true;
145
+ g.add(haste, bola);
146
+ g.position.set(p.x, 0, p.z);
147
+ P.cena.add(g);
148
+ return g;
149
+ });
150
+ const rot = P.rotulos(paradas.map(({ p }) => ({ texto: p.nome, lado: "cima" })));
151
+ // câmera: acompanhando, mostra um pedaço do chão de "janela" de largura em volta do ponto da vez; no fim, tudo
152
+ const j = PARAM.seguir.janela || larguraTudo;
153
+ const distSeguir = P.distanciaQueCabe(new THREE.Box3(new THREE.Vector3(-j / 2, 0, -j * 0.35), new THREE.Vector3(j / 2, 1, j * 0.35)),
154
+ { alvo: [0, 0.2, 0], azim: PARAM.seguir.azim, elev: PARAM.seguir.elev }, 1.0);
155
+ const caixaTudo = new THREE.Box3(new THREE.Vector3(-larguraTudo / 2, -0.1, -fundoTudo / 2), new THREE.Vector3(larguraTudo / 2, 1.2, fundoTudo / 2));
156
+ const final = PARAM.final ? { alvo: [0, 0, 0], azim: PARAM.final.azim, elev: PARAM.final.elev } : null;
157
+ if (final) final.dist = P.distanciaQueCabe(caixaTudo, final, 1.05);
158
+ const tFim = tempos[tempos.length - 1];
159
+ const foco = new THREE.Vector3();
160
+ return (t) => {
161
+ const u = K.lim(uEm(t));
162
+ curva.getPointAt(u, foco);
163
+ tubo.geometry.setDrawRange(0, Math.floor((u * totalIdx) / porAnel) * porAnel);
164
+ cabeca.position.set(foco.x, 0.16, foco.z);
165
+ // pinos: sobem quando o caminho chega neles
166
+ paradas.forEach(({ i }, k) => { const s = Math.max(0.001, K.trecho(t, tempos[i] - 0.18, 0.28, K.freia)); pinos[k].scale.set(s, s, s); });
167
+ const seguir = { alvo: [foco.x * PARAM.acompanha, 0.2, foco.z], dist: distSeguir, azim: PARAM.seguir.azim, elev: PARAM.seguir.elev };
168
+ if (final) P.orbita(K.misturaPose(seguir, final, K.trecho(t, tFim, PARAM.final.duracao || 0.6)));
169
+ else P.orbita(seguir);
170
+ rot.atualizar(paradas.map(({ i }) => ({ ponto: [PARAM.pontos[i].x, 0.95, PARAM.pontos[i].z], opacidade: K.trecho(t, tempos[i] - 0.05, 0.22) })));
171
+ };
172
+ }
173
+
174
+ // ------------------------------------------------------------------ ligação com o HyperFrames (não precisa mexer)
175
+ const palco = document.getElementById("k3dc-palco");
176
+ const tl = gsap.timeline({ paused: true });
177
+ const relogio = { t: 0 };
178
+ let desenhar = null;
179
+ tl.fromTo(relogio, { t: 0 }, { t: DUR, duration: DUR, ease: "none", onUpdate: () => desenhar && desenhar(relogio.t) }, 0);
180
+ if (PARAM.entrada > 0) tl.fromTo(palco, { opacity: 0 }, { opacity: 1, duration: PARAM.entrada, ease: "power1.out" }, 0);
181
+ else tl.set(palco, { opacity: 1 }, 0);
182
+ if (PARAM.saida > 0) tl.fromTo(palco, { opacity: 1 }, { opacity: 0, duration: PARAM.saida, ease: "power1.in", immediateRender: false }, Math.max(0, DUR - PARAM.saida));
183
+ window.__timelines["k3d_caminho"] = tl;
184
+ window.addEventListener("hf-seek", () => { if (desenhar) desenhar(relogio.t); });
185
+ const src = typeof __hfCompositionSrc === "string" ? __hfCompositionSrc : "";
186
+ const RAIZ = src.includes("/compositions/") ? src.slice(0, src.lastIndexOf("/compositions/") + 1) : document.baseURI;
187
+ const fundo = PARAM.fundo === "papel" ? C.cores.papel : PARAM.fundo;
188
+ (window.__hf = window.__hf || {}).buildReady = window.__hf.buildReady || {};
189
+ window.__hf.buildReady[ID] = import(new URL("compositions/cenas/3d/motor-three.js", RAIZ).href)
190
+ .then((K) => K.iniciar({ id: ID, C, raiz: RAIZ, canvas: document.getElementById("k3dc-tela"), camada: document.getElementById("k3dc-rotulos"),
191
+ fundo, area: PARAM.area, recorte: PARAM.area && PARAM.recorte ? PARAM.area : null, fov: 34, sombras: PARAM.sombras, construir: (P) => construir(K, P) }))
192
+ .then((d) => { desenhar = d; desenhar(relogio.t); })
193
+ .catch((e) => console.error(`[cena 3D ${ID}] não carreguei o motor 3D: ${e && e.message ? e.message : e}`));
194
+ })();
195
+ </script>
196
+ </template>
197
+ </body></html>
@@ -0,0 +1,152 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR"><head><meta charset="UTF-8"/><title>k3d · vista explodida (peças que se separam e voltam)</title></head>
3
+ <body>
4
+ <!--
5
+ Kit 3D (a): peças empilhadas que abrem parado, se separam na palavra combinada (com um giro curto da câmera
6
+ para aparecerem as laterais e a espessura), ganham rótulos presos a cada uma e voltam ao encaixe na outra
7
+ palavra, sem trocar de lugar entre si. Termina parada no ângulo que mais explica.
8
+ Serve para "o que tem dentro", camadas de um produto, as partes de um método que se juntam.
9
+ Sem medidas conferidas a cena é esquemática: não escreva tamanho nem escala nos rótulos. Guia: references/3d.md.
10
+ Para usar: copie para compositions/cenas/<nome>.html, troque "k3d_explodida" (template, raiz e ID) e o prefixo
11
+ "k3de-", mude os PARÂMETROS e ligue no projeto.json em "cenas_proprias".
12
+ -->
13
+ <template id="k3d_explodida-template">
14
+ <style>
15
+ @font-face{font-family:Montserrat;src:url("assets/fontes/Montserrat.ttf") format("truetype");font-weight:100 900;font-display:block}
16
+ #root{position:absolute;inset:0;overflow:hidden;font-family:Montserrat}
17
+ #k3de-palco{position:absolute;inset:0;opacity:0}
18
+ #k3de-tela{position:absolute;left:0;top:0;width:100%;height:100%;display:block}
19
+ #k3de-rotulos{position:absolute;inset:0}
20
+ </style>
21
+ <div id="root" data-composition-id="k3d_explodida" data-width="1080" data-height="1920" data-duration="5">
22
+ <div id="k3de-palco"><canvas id="k3de-tela" width="1080" height="1920"></canvas><div id="k3de-rotulos"></div></div>
23
+ </div>
24
+ <script>
25
+ (function () {
26
+ const ID = "k3d_explodida";
27
+ // ------------------------------------------------------------------ PARÂMETROS (troque aqui)
28
+ const PARAM = {
29
+ // as peças, de baixo para cima. nome = rótulo; forma: "placa" (padrão), "caixa", "cilindro" ou "modelo"
30
+ // (com arquivo: "assets/peca.glb", liberado para uso); cor null = tons do estilo; altura = espessura da peça
31
+ pecas: [
32
+ { nome: "Vídeo bruto", altura: 0.26 },
33
+ { nome: "Áudio da fala", altura: 0.2 },
34
+ { nome: "Cada palavra", altura: 0.2 },
35
+ { nome: "Os cortes", altura: 0.2 },
36
+ ],
37
+ largura: 2.6, // tamanho das peças (esquemático, sem escala real)
38
+ profundidade: 1.8,
39
+ encolhe: 0.08, // cada peça de cima fica um pouco menor (0 = todas iguais)
40
+ afastamento: 0.8, // quanto sobra entre uma peça e outra quando estão separadas
41
+ separa: { palavra: "ouviu", padrao: 0.9 }, // quando começam a se separar
42
+ duracaoSepara: 1.0,
43
+ junta: { palavra: "palavra", depois: 2.6, padrao: 3.5 }, // quando voltam ao encaixe (null = ficam separadas)
44
+ duracaoJunta: 0.7,
45
+ cameraInicio: { azim: 26, elev: 20 }, // graus: a vista parada do começo
46
+ cameraFim: { azim: 44, elev: 28 }, // graus: onde a câmera para (o giro curto acontece junto da separação)
47
+ rotulos: "dir", // lado dos rótulos: "dir", "esq" ou null (sem rótulos)
48
+ fundo: "papel", // "papel", "#rrggbb" ou null (transparente)
49
+ area: null, // [x0, y0, x1, y1] em px onde fica o 3D; null = zona segura
50
+ recorte: true, // com area: o 3D só aparece dentro dela (janela de cantos redondos)
51
+ sombras: true, // sombra das luzes (false deixa o render mais leve; a sombra de contato fica)
52
+ entrada: 0.2,
53
+ saida: 0.2,
54
+ };
55
+ // ------------------------------------------------------------------ dados do build.py (padrões para abrir sozinha)
56
+ const C = Object.assign({ palavras: [], duracao: 5, largura: 1080, altura: 1920, seguro: [140, 230, 940, 1530], rosto: null,
57
+ cores: { papel: "#fffdfd", linha: "#e6ddf8", base: "#c7b7ef", destaque: "#665080", tinta: "#352c40", verde: "#2e9e5b", vermelho: "#e5484d" } },
58
+ (window.__cenas || {})[ID] || {});
59
+ const DUR = C.duracao;
60
+
61
+ async function construir(K, P) {
62
+ const THREE = K.THREE;
63
+ const n = PARAM.pecas.length;
64
+ const tom = (i) => {
65
+ const a = new THREE.Color(C.cores.destaque), b = new THREE.Color(C.cores.base);
66
+ return "#" + a.lerp(b, n > 1 ? i / (n - 1) : 0).getHexString();
67
+ };
68
+ const escuro = (hex) => "#" + new THREE.Color(hex).multiplyScalar(0.72).getHexString();
69
+ P.luzes({ chave: [4, 9, 5], alvo: [0, 1, 0], area: 5 });
70
+ P.chao({ tamanho: 22 });
71
+ const pecas = [];
72
+ let yJunto = 0, ySeparado = 0;
73
+ for (let i = 0; i < n; i++) {
74
+ const p = PARAM.pecas[i];
75
+ const k = 1 - PARAM.encolhe * i;
76
+ const w = PARAM.largura * k, d = PARAM.profundidade * k, h = p.altura || 0.2;
77
+ const cor = p.cor || tom(i), lados = p.lados || escuro(cor);
78
+ let obj;
79
+ if (p.forma === "modelo") obj = (await P.carregarModelo(p.arquivo, { largura: w })).objeto;
80
+ else if (p.forma === "cilindro") obj = P.cilindro({ raio: Math.min(w, d) / 2, altura: h, cor });
81
+ else if (p.forma === "caixa") obj = P.caixa({ largura: w, altura: h, profundidade: d, cor, lados });
82
+ else obj = P.placa({ largura: w, altura: h, profundidade: d, raio: 0.16 * k, chanfro: Math.min(0.035, h / 4), cor, lados });
83
+ const hh = p.forma === "modelo" ? new THREE.Box3().setFromObject(obj).getSize(new THREE.Vector3()).y : h;
84
+ P.cena.add(obj);
85
+ P.solido(obj, p.nome);
86
+ pecas.push({ obj, w, d, h: hh, yJunto, ySeparado });
87
+ yJunto += hh;
88
+ ySeparado += hh + PARAM.afastamento;
89
+ }
90
+ const sombra = P.sombraDeContato({ largura: PARAM.largura, profundidade: PARAM.profundidade, forca: 0.55 });
91
+ sombra.colocar(0, 0, 0);
92
+ const tSep = K.momento(C, PARAM.separa, 0.9);
93
+ const tJun = PARAM.junta ? Math.max(tSep + PARAM.duracaoSepara + 0.3, K.momento(C, PARAM.junta, 3.5)) : Infinity;
94
+ // enquadramento medido: o conjunto encaixado visto da pose do começo e o conjunto aberto visto da pose final
95
+ const caixaEm = (abre) => {
96
+ const b = new THREE.Box3();
97
+ pecas.forEach((q) => { q.obj.position.y = K.mistura(q.yJunto, q.ySeparado, abre); q.obj.updateMatrixWorld(true); b.union(new THREE.Box3().setFromObject(q.obj)); });
98
+ return b;
99
+ };
100
+ const cJ = caixaEm(0), cS = caixaEm(1);
101
+ const alvoJ = cJ.getCenter(new THREE.Vector3()).toArray(), alvoS = cS.getCenter(new THREE.Vector3()).toArray();
102
+ const distJunto = P.distanciaQueCabe(cJ, { alvo: alvoJ, ...PARAM.cameraInicio }, 1.15);
103
+ const distSeparado = P.distanciaQueCabe(cS, { alvo: alvoS, ...PARAM.cameraFim }, 1.03);
104
+ const rot = PARAM.rotulos ? P.rotulos(PARAM.pecas.map((p) => ({ texto: p.nome, lado: PARAM.rotulos }))) : null;
105
+ const lado = PARAM.rotulos === "esq" ? -1 : 1;
106
+ return (t) => {
107
+ // quanto as peças estão abertas: 0 = encaixadas, 1 = separadas (sobe na separação, desce na junção)
108
+ const abre = K.trecho(t, tSep, PARAM.duracaoSepara) * (1 - K.trecho(t, tJun, PARAM.duracaoJunta));
109
+ pecas.forEach((q) => { q.obj.position.y = K.mistura(q.yJunto, q.ySeparado, abre); });
110
+ // câmera: um giro curto junto da separação e fica lá; a distância e a altura do alvo acompanham o tamanho
111
+ const g = K.trecho(t, tSep, PARAM.duracaoSepara + 0.2);
112
+ const pose = K.misturaPose(PARAM.cameraInicio, PARAM.cameraFim, g);
113
+ const alvo = [0, 1, 2].map((j) => K.mistura(alvoJ[j], alvoS[j], abre));
114
+ P.orbita({ alvo, dist: K.mistura(distJunto, distSeparado, abre), azim: pose.azim, elev: pose.elev });
115
+ if (rot) {
116
+ // rótulo aparece depois que a peça se afastou e some antes de ela voltar
117
+ const tIn = tSep + PARAM.duracaoSepara * 0.7;
118
+ rot.atualizar(pecas.map((q, i) => ({
119
+ ponto: [lado * q.w / 2, q.obj.position.y + q.h / 2, 0],
120
+ opacidade: K.trecho(t, tIn + i * 0.12, 0.25) * (1 - K.trecho(t, tJun - 0.25, 0.2)),
121
+ })), { coluna: true });
122
+ }
123
+ };
124
+ }
125
+
126
+ // ------------------------------------------------------------------ ligação com o HyperFrames (não precisa mexer)
127
+ const palco = document.getElementById("k3de-palco");
128
+ const tl = gsap.timeline({ paused: true });
129
+ const relogio = { t: 0 };
130
+ let desenhar = null;
131
+ tl.fromTo(relogio, { t: 0 }, { t: DUR, duration: DUR, ease: "none", onUpdate: () => desenhar && desenhar(relogio.t) }, 0);
132
+ if (PARAM.entrada > 0) tl.fromTo(palco, { opacity: 0 }, { opacity: 1, duration: PARAM.entrada, ease: "power1.out" }, 0);
133
+ else tl.set(palco, { opacity: 1 }, 0);
134
+ if (PARAM.saida > 0) tl.fromTo(palco, { opacity: 1 }, { opacity: 0, duration: PARAM.saida, ease: "power1.in", immediateRender: false }, Math.max(0, DUR - PARAM.saida));
135
+ window.__timelines["k3d_explodida"] = tl;
136
+ window.addEventListener("hf-seek", () => { if (desenhar) desenhar(relogio.t); });
137
+ const src = typeof __hfCompositionSrc === "string" ? __hfCompositionSrc : "";
138
+ const RAIZ = src.includes("/compositions/") ? src.slice(0, src.lastIndexOf("/compositions/") + 1) : document.baseURI;
139
+ const fundo = PARAM.fundo === "papel" ? C.cores.papel : PARAM.fundo;
140
+ // o 3D fica de um lado e os rótulos ganham uma coluna do outro, dentro da mesma área
141
+ const lim3 = PARAM.area || C.seguro, reserva = PARAM.rotulos ? 260 * Math.min(C.largura, C.altura) / 1080 : 0;
142
+ const area3d = PARAM.rotulos === "esq" ? [lim3[0] + reserva, lim3[1], lim3[2], lim3[3]] : [lim3[0], lim3[1], lim3[2] - reserva, lim3[3]];
143
+ (window.__hf = window.__hf || {}).buildReady = window.__hf.buildReady || {};
144
+ window.__hf.buildReady[ID] = import(new URL("compositions/cenas/3d/motor-three.js", RAIZ).href)
145
+ .then((K) => K.iniciar({ id: ID, C, raiz: RAIZ, canvas: document.getElementById("k3de-tela"), camada: document.getElementById("k3de-rotulos"),
146
+ fundo, area: area3d, limites: lim3, recorte: PARAM.area && PARAM.recorte ? PARAM.area : null, fov: 30, sombras: PARAM.sombras, construir: (P) => construir(K, P) }))
147
+ .then((d) => { desenhar = d; desenhar(relogio.t); })
148
+ .catch((e) => console.error(`[cena 3D ${ID}] não carreguei o motor 3D: ${e && e.message ? e.message : e}`));
149
+ })();
150
+ </script>
151
+ </template>
152
+ </body></html>