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,410 @@
1
+ """palavra_impacto: one to four big words that slam onto the screen one after the other (kinetic typography).
2
+
3
+ Each word starts big and faint, falls onto the screen accelerating, lands with a squash, and on the
4
+ landing the screen flashes, the block shakes and short impact lines shoot out. Two looks: "contorno"
5
+ (white letters with a thick black outline, like the `impacto` captions) and "tarja" (each word on a solid,
6
+ slightly tilted chip, white or in the accent color, like the `tarja` hook). In "tarja" the chip and its word
7
+ slam in as one piece.
8
+
9
+ Full-screen flash: `flash: "tela"` draws on `E.Canvas(box, full=True)` on EVERY frame (Contract 12). The
10
+ flash, the glow and the impact lines are effects, not content: `ink()` gives the area of the resting words
11
+ (plus the entrance scale), so face-aware placing and the caption rules only see the words.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import math
18
+ from functools import lru_cache
19
+
20
+ from PIL import Image
21
+
22
+ from motion import engine as E
23
+ from motion.engine import MotionError
24
+
25
+ TEMPLATE = {
26
+ "id": "palavra_impacto",
27
+ "label_pt": "Palavra de impacto",
28
+ "description_pt": "Uma ou duas palavras gigantes que batem na tela uma de cada vez: entram grandes, "
29
+ "encolhem com um tranco, a tela pisca e o texto treme um pouco.",
30
+ "use_when_pt": "Na palavra forte da fala: o gancho, uma virada ou uma ordem curta (PARE, ZERO ESTOQUE, "
31
+ "SEM CNPJ). Use pouco, uma ou duas vezes por vídeo. Marque a palavra principal com "
32
+ "*asteriscos* para ela sair na cor de destaque.",
33
+ "notes_pt": "Até 4 palavras em até 2 linhas. O clarão da tela inteira (flash: tela) pisca por 1 quadro "
34
+ "em cada palavra; em vídeo com muita coisa piscando, use flash: local ou nenhum.",
35
+ "category": "texto",
36
+ "version": 1,
37
+ "params": {
38
+ "text": {"type": "text", "default": "*ATENÇÃO*", "max_len": 32, "required_text": True,
39
+ "label_pt": "Até 4 palavras. *palavra* sai na cor de destaque e | quebra a linha"},
40
+ "style": {"type": "choice", "choices": ["contorno", "tarja"], "default": "contorno",
41
+ "label_pt": "Visual: contorno (letra branca com borda preta) ou tarja (cada palavra numa "
42
+ "plaquinha inclinada)"},
43
+ "font": {"type": "choice", "choices": ["condensada", "redonda"], "default": "condensada",
44
+ "label_pt": "Letra: condensada (alta e estreita) ou redonda (larga e grossa)"},
45
+ "size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
46
+ "label_pt": "Tamanho das palavras"},
47
+ "flash": {"type": "choice", "choices": ["tela", "local", "nenhum"], "default": "tela",
48
+ "label_pt": "Clarão na batida: tela inteira, só em volta das palavras, ou nenhum"},
49
+ "flash_color": {"type": "choice", "choices": ["branco", "destaque"], "default": "branco",
50
+ "label_pt": "Cor do clarão"},
51
+ "shake": {"type": "bool", "default": True, "label_pt": "Tremida curta quando cada palavra bate"},
52
+ "burst": {"type": "bool", "default": True, "label_pt": "Risquinhos de impacto saindo em volta"},
53
+ "stagger": {"type": "number", "default": 0.24, "min": 0, "max": 1.5,
54
+ "label_pt": "Segundos entre uma palavra e a próxima (0: todas juntas)"},
55
+ "upper": {"type": "bool", "default": True, "label_pt": "Tudo em letra maiúscula"},
56
+ "sound": {"type": "choice", "choices": ["metal", "grave", "batida"], "default": "metal",
57
+ "label_pt": "Som de cada batida: metal (impacto metálico), grave ou batida sintética"},
58
+ },
59
+ "default_duration": 2.0,
60
+ "default_region": "baixo",
61
+ "sfx": [{"at": 0.13, "name": "metal_hit", "why": "palavra batendo"}],
62
+ "bleed": 300,
63
+ "example": {"text": "ZERO *ESTOQUE*"},
64
+ }
65
+
66
+ FONTS = {"condensada": "display_condensed", "redonda": "sans_black"}
67
+ MAX_SIZE = {("condensada", "normal"): 250.0, ("condensada", "grande"): 330.0,
68
+ ("redonda", "normal"): 165.0, ("redonda", "grande"): 220.0}
69
+ SOUNDS = {"metal": "metal_hit", "grave": "bass_drop", "batida": "synth_hit"}
70
+ MAX_WORDS = 4
71
+
72
+ T_HIT = 0.15 # seconds from a word's start to its landing
73
+ T_FADE = 0.12 # the word fades in (in_quad) while it falls: frame 0 is faint, not yet "ink"
74
+ START_SCALE = 1.8 # the word starts this big (the bleed holds it)
75
+ SQUASH = 0.075 # scale dip on the landing
76
+ T_SETTLE = 0.40 # the landing wobble ends exactly here
77
+ FLASH_T = 0.15
78
+ FLASH_TAU = 0.0185 # flash decay at 30 fps: 0.60, 0.08, 0.01 (only the landing frame passes alpha 24)
79
+ SHAKE_T = 0.30
80
+ BURST_T = 0.30
81
+ N_RAYS = 12
82
+ RAY_SPREAD = math.radians(30) # the lines fan out this much above and below the horizontal
83
+ SIDE = 24.0 # free space kept at the box sides
84
+ # "tarja" chips, as fractions of the font size
85
+ CHIP_X, CHIP_Y, CHIP_GAP = 0.20, 0.17, 0.12
86
+ TILTS = (-3.0, 2.2, -1.6, 2.8)
87
+
88
+
89
+ # ------------------------------------------------------------------ text and layout
90
+
91
+ def _lines_of(text: str, upper: bool, font_key: str) -> list[list[tuple[str, bool]]]:
92
+ """'ZERO | *ESTOQUE*' -> [[('ZERO', False)], [('ESTOQUE', True)]] (words the font cannot draw vanish)."""
93
+ lines: list[list[tuple[str, bool]]] = []
94
+ cur: list[tuple[str, bool]] = []
95
+ for word, acc in E.parse_accent(str(text).replace("|", " | ")):
96
+ if word == "|":
97
+ if cur:
98
+ lines.append(cur)
99
+ cur = []
100
+ continue
101
+ w = E.clean_text(word, font_key).strip()
102
+ if not w:
103
+ continue
104
+ cur.append((w.upper() if upper else w, acc))
105
+ if cur:
106
+ lines.append(cur)
107
+ if not lines:
108
+ raise MotionError("palavra_impacto.params.text precisa de pelo menos uma palavra com letras ou números")
109
+ count = sum(len(ln) for ln in lines)
110
+ if count > MAX_WORDS:
111
+ raise MotionError(f"palavra_impacto.params.text tem {count} palavras; o máximo é {MAX_WORDS}. "
112
+ "Deixe só as palavras fortes")
113
+ if len(lines) > 2:
114
+ raise MotionError(f"palavra_impacto.params.text tem {len(lines)} linhas; o máximo é 2 (use só um |)")
115
+ return lines
116
+
117
+
118
+ def _word_count(params: dict) -> int:
119
+ return sum(len(ln) for ln in _lines_of(params["text"], params["upper"], FONTS[params["font"]]))
120
+
121
+
122
+ def validate(params: dict) -> None:
123
+ """At most 4 words in 2 lines, with letters the font draws (refused when the item is planned)."""
124
+ _lines_of(params["text"], params["upper"], FONTS[params["font"]])
125
+
126
+
127
+ def _hit_times(count: int, stagger: float, fps: int) -> list[float]:
128
+ """Landing of each word in `t_in` time ((frame + 1) / fps), snapped to a frame so the flash peaks on it."""
129
+ return [E.frame_of(k * stagger + T_HIT, fps) / float(fps) for k in range(count)]
130
+
131
+
132
+ @lru_cache(maxsize=64)
133
+ def _layout(params_json: str, box: E.Box) -> dict:
134
+ p = json.loads(params_json)
135
+ fk = FONTS[p["font"]]
136
+ tarja = p["style"] == "tarja"
137
+ lines = _lines_of(p["text"], p["upper"], fk)
138
+ max_size = MAX_SIZE[(p["font"], p["size"])]
139
+ avail = box.w - 2 * SIDE
140
+ space_unit = E.font(fk, 100.0).getlength(" ") / 100.0
141
+ # gap between two words and the extra width around a line, as fractions of the size
142
+ gap_unit = (2 * CHIP_X + CHIP_GAP) if tarja else space_unit
143
+ ends_unit = (2 * CHIP_X) if tarja else 0.16 # chips, or the outline and the shake
144
+
145
+ def unit_width(line) -> float:
146
+ return (sum(E.measure(w, fk, 100.0).width for w, _ in line) / 100.0
147
+ + gap_unit * (len(line) - 1) + ends_unit)
148
+
149
+ def fit(line) -> float:
150
+ return min(max_size, avail / unit_width(line))
151
+
152
+ if len(lines) == 1 and len(lines[0]) >= 2:
153
+ words = lines[0]
154
+ one = fit(words)
155
+ best = None
156
+ if one < 0.80 * max_size:
157
+ for cut in range(1, len(words)):
158
+ s = min(fit(words[:cut]), fit(words[cut:]))
159
+ if best is None or s > best[0]:
160
+ best = (s, [words[:cut], words[cut:]])
161
+ if best is not None and best[0] > one * 1.12:
162
+ lines = best[1]
163
+ n = len(lines)
164
+ cap_unit = E.measure("H", fk, 100.0).cap / 100.0
165
+ line_gap_unit = (2 * CHIP_Y + 0.14) if tarja else 0.20
166
+ pad_y_unit = CHIP_Y if tarja else 0.10
167
+ block_unit = n * cap_unit + (n - 1) * line_gap_unit + 2 * pad_y_unit
168
+ size = min(min(fit(ln) for ln in lines), (box.h - 40.0) / block_unit)
169
+ size = max(40.0, size)
170
+
171
+ def width_at(line, s: float) -> float:
172
+ return (sum(E.measure(w, fk, s).width for w, _ in line) + gap_unit * s * (len(line) - 1)
173
+ + ends_unit * s)
174
+
175
+ # the widths are checked at the final size (FreeType hinting is not perfectly linear)
176
+ while size > 40.0 and any(width_at(ln, size) > avail + 0.5 for ln in lines):
177
+ size *= 0.985
178
+ cap = E.measure("H", fk, size).cap
179
+ line_gap = line_gap_unit * size
180
+ block_h = n * cap + (n - 1) * line_gap
181
+ top = (box.h - block_h) / 2.0
182
+ gap = gap_unit * size
183
+ escuro = box.theme.dark
184
+ has_accent = any(a for ln in lines for _, a in ln)
185
+ count = sum(len(ln) for ln in lines)
186
+ px, py = CHIP_X * size, CHIP_Y * size
187
+ words = []
188
+ k = 0
189
+ for i, line in enumerate(lines):
190
+ widths = [E.measure(w, fk, size).width for w, _ in line]
191
+ total = sum(widths) + gap * (len(line) - 1)
192
+ x = (box.w - total) / 2.0
193
+ base = top + cap + i * (cap + line_gap)
194
+ for (text, acc), wd in zip(line, widths):
195
+ w = {"text": text, "x": x, "w": wd, "k": k, "base": base, "cx": x + wd / 2.0, "cy": base - cap / 2.0}
196
+ if tarja:
197
+ on_accent = acc if has_accent else (count == 1 or k % 2 == 1)
198
+ w.update(chip=(x - px, base - cap - py, wd + 2 * px, cap + 2 * py),
199
+ chip_fill="accent" if on_accent else "white",
200
+ color="on_accent" if on_accent else "ink", stroke_color=None,
201
+ tilt=TILTS[k % len(TILTS)], ink=(x - px, base - cap - py, x + wd + px, base + py))
202
+ else:
203
+ w.update(color="accent" if acc else ("white" if escuro else "ink"),
204
+ stroke_color="black" if escuro else "white",
205
+ ink=(x, base - cap, x + wd, base))
206
+ words.append(w)
207
+ x += wd + gap
208
+ k += 1
209
+ hits = _hit_times(k, p["stagger"], box.fps)
210
+ # per landing: the area of the words already on screen (impact lines and the local flash go around it)
211
+ landings = []
212
+ for j, h in enumerate(sorted(set(hits))):
213
+ landed = [w for w in words if hits[w["k"]] <= h + 1e-9]
214
+ x0 = min(w["ink"][0] for w in landed)
215
+ y0 = min(w["ink"][1] for w in landed)
216
+ x1 = max(w["ink"][2] for w in landed)
217
+ y1 = max(w["ink"][3] for w in landed)
218
+ r = E.rng("palavra_impacto", "rays", j)
219
+ ccx, ccy = (x0 + x1) / 2.0, (y0 + y1) / 2.0
220
+ rx, ry = (x1 - x0) / 2.0 + 36.0, (y1 - y0) / 2.0 + 28.0
221
+ rays = []
222
+ for m in range(N_RAYS):
223
+ # emphasis lines on the two sides (the placing only moves the item up or down, so lines above and
224
+ # below the words would make it look taller than it is)
225
+ side = 0.0 if m % 2 == 0 else math.pi
226
+ ang = side + RAY_SPREAD * ((m // 2) / (N_RAYS // 2 - 1) * 2.0 - 1.0) + r.uniform(-0.08, 0.08)
227
+ if m % 2:
228
+ ang = side - (ang - side)
229
+ ca, sa = math.cos(ang), math.sin(ang)
230
+ sx, sy = ccx + rx * ca, ccy + ry * sa # on the ellipse, pointing away from the center
231
+ length = r.uniform(0.55, 1.0) * min(95.0, 0.36 * size + 20.0)
232
+ rays.append({"p0": (sx + ca * 6.0, sy + sa * 6.0),
233
+ "p1": (sx + ca * (length + 24.0), sy + sa * (length + 24.0)),
234
+ "width": r.uniform(7.0, 11.0), "delay": r.uniform(0.0, 0.04)})
235
+ landings.append({"hit": h, "first": j == 0, "area": (x0, y0, x1, y1), "rays": rays})
236
+ return {"font": fk, "size": size, "cap": cap, "words": words, "hits": hits, "landings": landings,
237
+ "stroke": size * 0.08, "tarja": tarja,
238
+ "shadow": E.Shadow(blur=max(6.0, size * 0.05), dy=size * 0.045, opacity=0.55),
239
+ "chip_shadow": E.Shadow(blur=22.0, dy=12.0, opacity=0.35), "chip_radius": size * 0.07}
240
+
241
+
242
+ # ------------------------------------------------------------------ motion curves
243
+
244
+ def _scale(tk: float) -> float:
245
+ """tk = seconds since the landing (negative before it)."""
246
+ if tk < 0:
247
+ u = E.clamp01(1.0 + tk / T_HIT)
248
+ return E.lerp(START_SCALE, 1.0, E.in_quad(u))
249
+ if tk >= T_SETTLE:
250
+ return 1.0
251
+ env = math.exp(-tk * 12.0) * (1.0 - tk / T_SETTLE)
252
+ return 1.0 - SQUASH * env * math.cos(2 * math.pi * 5.0 * tk)
253
+
254
+
255
+ def _opacity(tk: float) -> float:
256
+ return E.eased(tk, -T_HIT, T_FADE, "in_quad")
257
+
258
+
259
+ def ink(params: dict, box: E.Box) -> tuple[float, float, float, float]:
260
+ """What counts as drawn (box-local design px): the words at rest, their outline and shadow, and a little of
261
+ the landing squash. The flash, the glow and the impact lines are effects and stay out."""
262
+ L = _layout(json.dumps(params, sort_keys=True), box)
263
+ pad = L["stroke"] + L["shadow"].dy + 6.0
264
+ x0 = min(w["ink"][0] for w in L["words"]) - pad
265
+ y0 = min(w["ink"][1] for w in L["words"]) - pad
266
+ x1 = max(w["ink"][2] for w in L["words"]) + pad
267
+ y1 = max(w["ink"][3] for w in L["words"]) + pad
268
+ return x0, y0, x1, y1
269
+
270
+
271
+ def _flash(landing: dict, t: float) -> float:
272
+ tk = t - landing["hit"]
273
+ if not 0.0 <= tk < FLASH_T:
274
+ return 0.0
275
+ peak = 0.60 if landing["first"] else 0.38
276
+ return peak * math.exp(-tk / FLASH_TAU) * (1.0 - tk / FLASH_T)
277
+
278
+
279
+ # ------------------------------------------------------------------ drawing
280
+
281
+ def _word_outline(cv: E.Canvas, L: dict, w: dict, t: float) -> None:
282
+ tk = t - L["hits"][w["k"]]
283
+ op = _opacity(tk)
284
+ if not cv.visible(op):
285
+ return
286
+ fk, size = L["font"], L["size"]
287
+ pivot = (w["cx"], w["cy"])
288
+ if tk < 0:
289
+ # motion trail: where the word was a moment ago, faint
290
+ sg = _scale(tk - 0.04)
291
+ og = _opacity(tk - 0.04) * 0.16 * op
292
+ if cv.visible(og) and sg > _scale(tk) + 0.01:
293
+ with cv.group(scale=sg, pivot=pivot):
294
+ cv.text(w["x"], w["base"], w["text"], fk, size, color=w["color"], opacity=og)
295
+ with cv.group(scale=_scale(tk), pivot=pivot, opacity=op):
296
+ cv.text(w["x"], w["base"], w["text"], fk, size, color=w["color"], stroke=L["stroke"],
297
+ stroke_color=w["stroke_color"], shadow=L["shadow"])
298
+
299
+
300
+ SPRITE_PAD = 110.0 # design px around a chip sprite: the shadow and the tilt
301
+
302
+
303
+ @lru_cache(maxsize=32)
304
+ def _chip_sprite(params_json: str, box: E.Box, k: int) -> tuple:
305
+ """Word k on its chip, with the shadow, tilted, drawn ONCE at the render scale.
306
+
307
+ A tilted group costs a full-layer rotation per frame (about 20 ms at 1080x1920); the chip never changes
308
+ shape, so every frame only scales and pastes this sprite. Returns (premultiplied image, absolute render px
309
+ of its top-left, chip center inside it)."""
310
+ L = _layout(params_json, box)
311
+ w = L["words"][k]
312
+ x, y, cw, ch = w["chip"]
313
+ sb = E.with_box(box, x=box.x + x, y=box.y + y, w=cw, h=ch, bleed=SPRITE_PAD)
314
+ sc = E.Canvas(sb)
315
+ with sc.group(rotate=w["tilt"], pivot=(cw / 2.0, ch / 2.0)):
316
+ sc.rect(0.0, 0.0, cw, ch, radius=L["chip_radius"], fill=w["chip_fill"], shadow=L["chip_shadow"])
317
+ sc.text(w["x"] - x, w["base"] - y, w["text"], L["font"], L["size"], color=w["color"])
318
+ cx, cy = sc.px(cw / 2.0, ch / 2.0)
319
+ return sc.image.convert("RGBa"), sc.origin_x, sc.origin_y, cx, cy
320
+
321
+
322
+ def _word_chip(cv: E.Canvas, L: dict, w: dict, t: float, params_json: str) -> None:
323
+ tk = t - L["hits"][w["k"]]
324
+ op = _opacity(tk)
325
+ if not cv.visible(op):
326
+ return
327
+ img, ox, oy, cx, cy = _chip_sprite(params_json, cv.box, w["k"])
328
+ s = _scale(tk)
329
+ left, top = float(ox), float(oy)
330
+ if abs(s - 1.0) > 1e-4:
331
+ size = (max(1, int(round(img.width * s))), max(1, int(round(img.height * s))))
332
+ sx, sy = size[0] / img.width, size[1] / img.height
333
+ img = img.resize(size, Image.BICUBIC)
334
+ left, top = ox + cx - cx * sx, oy + cy - cy * sy # the chip center stays in place
335
+ sprite = img.convert("RGBA")
336
+ k = cv.box.scale
337
+ cv.paste_image(sprite, left / k - cv.box.x, top / k - cv.box.y, w=sprite.width / k, h=sprite.height / k,
338
+ opacity=op)
339
+
340
+
341
+ def _rays(cv: E.Canvas, landing: dict, t: float) -> None:
342
+ for ray in landing["rays"]:
343
+ u = E.progress(t, landing["hit"] + ray["delay"], BURST_T)
344
+ if u <= 0 or u >= 1:
345
+ continue
346
+ head = E.out_cubic(min(1.0, u * 1.5))
347
+ tail = E.in_cubic(u)
348
+ if (head - tail) * math.dist(ray["p0"], ray["p1"]) < 10.0: # no leftover dots
349
+ continue
350
+ cv.polyline([ray["p0"], ray["p1"]], ray["width"], color="accent", start=tail, progress=head)
351
+
352
+
353
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
354
+ key = json.dumps(params, sort_keys=True)
355
+ L = _layout(key, box)
356
+ cv = E.Canvas(box, full=params["flash"] == "tela")
357
+ t = (frame_index + 1) / fps # entrances run one frame early: frame 0 already shows the first word
358
+
359
+ if params["flash"] != "nenhum":
360
+ color = "white" if params["flash_color"] == "branco" else "accent"
361
+ for landing in L["landings"]:
362
+ fl = _flash(landing, t)
363
+ if fl <= 0.002:
364
+ continue
365
+ if params["flash"] == "tela":
366
+ cv.fill(color, fl)
367
+ else:
368
+ # a flat glow behind the words
369
+ x0, y0, x1, y1 = landing["area"]
370
+ m = cv.rect_mask(x0 - 60, y0 - 12, x1 - x0 + 120, y1 - y0 + 24, radius=(y1 - y0 + 24) / 2.0)
371
+ if m is not None:
372
+ cv.fill_mask(E.blur_mask(m, cv.len(55), cv.W, cv.H), color, min(1.0, fl * 1.2))
373
+
374
+ dx = dy = 0.0
375
+ if params["shake"]:
376
+ amp = 16.0 if L["size"] < 200 else 20.0
377
+ for j, landing in enumerate(L["landings"]):
378
+ sx, sy = E.shake(t, landing["hit"], SHAKE_T, amplitude=amp * (1.0 if j == 0 else 0.7),
379
+ frequency=20.0, seed=("palavra_impacto", j))
380
+ dx, dy = dx + sx, dy + sy
381
+
382
+ with cv.group(dx=dx, dy=dy):
383
+ if params["burst"]:
384
+ for landing in L["landings"]:
385
+ _rays(cv, landing, t)
386
+ for w in L["words"]:
387
+ if L["tarja"]:
388
+ _word_chip(cv, L, w, t, key)
389
+ else:
390
+ _word_outline(cv, L, w, t)
391
+ return cv
392
+
393
+
394
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
395
+ name = SOUNDS[params["sound"]]
396
+ hits = sorted(set(_hit_times(_word_count(params), params["stagger"], fps)))
397
+ # the landing is drawn on the frame where (frame + 1) / fps == hit
398
+ return [{"frame": max(0, int(round(h * fps)) - 1), "name": name, "volume": 1.0 if j == 0 else 0.8,
399
+ "why": "palavra batendo"} for j, h in enumerate(hits)]
400
+
401
+
402
+ def suggested_duration(params: dict) -> float:
403
+ """The last landing and about 1.4 s to read."""
404
+ last = (_word_count(params) - 1) * params["stagger"] + T_HIT
405
+ return round(max(TEMPLATE["default_duration"], last + 1.4), 2)
406
+
407
+
408
+ def settle(params: dict, fps: int) -> float:
409
+ last = max(_hit_times(_word_count(params), params["stagger"], fps))
410
+ return last - 1.0 / fps + max(T_SETTLE, SHAKE_T, BURST_T + 0.04, FLASH_T) + 0.02