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,370 @@
1
+ """barra_progresso: a progress bar (or ring) filling up to a percentage, with the label and the percentage
2
+ counting, moving stripes and a bright tip while it fills, and a short glow when it lands (SPEC Contract 12).
3
+ Optional segments turn it into a step bar ("etapa 3 de 5").
4
+
5
+ `_stripes` draws moving diagonal stripes clipped to a mask. The card, label and track are drawn once
6
+ (`E.static_sprite`) and pasted after the entrance.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ from functools import lru_cache
14
+
15
+ import numpy as np
16
+
17
+ from motion import engine as E
18
+
19
+ TEMPLATE = {
20
+ "id": "barra_progresso",
21
+ "label_pt": "Barra de progresso",
22
+ "description_pt": "Uma barra que enche até a porcentagem, com o nome em cima e a porcentagem contando. "
23
+ "Pode virar um anel ou uma barra em etapas.",
24
+ "use_when_pt": "Quando a fala cita quanto de algo já foi feito ou falta: 80% do curso, 3 de 5 etapas, "
25
+ "vagas quase acabando, parte da meta batida.",
26
+ "category": "números",
27
+ "version": 1,
28
+ "params": {
29
+ "value": {"type": "number", "default": 75, "min": 0, "max": 100, "label_pt": "Porcentagem final, de 0 a 100"},
30
+ "start_value": {"type": "number", "default": 0, "min": 0, "max": 100,
31
+ "label_pt": "Porcentagem onde começa"},
32
+ "label": {"type": "text", "default": "Concluído", "max_len": 30, "label_pt": "Nome da barra"},
33
+ "note": {"type": "text", "default": "", "max_len": 36,
34
+ "label_pt": "Texto pequeno embaixo, ex.: 3 de 4 módulos (opcional)"},
35
+ "decimals": {"type": "integer", "default": None, "nullable": True, "min": 0, "max": 1,
36
+ "label_pt": "Casas decimais (vazio: automático)"},
37
+ "mode": {"type": "choice", "choices": ["barra", "anel"], "default": "barra",
38
+ "label_pt": "barra (deitada) ou anel (círculo com o número no meio)"},
39
+ "segments": {"type": "integer", "default": None, "nullable": True, "min": 2, "max": 10,
40
+ "label_pt": "Dividir em etapas (vazio: barra contínua)"},
41
+ "show_value": {"type": "bool", "default": True, "label_pt": "Mostrar a porcentagem"},
42
+ "duration": {"type": "number", "default": 1.4, "min": 0.3, "max": 10,
43
+ "label_pt": "Segundos para encher"},
44
+ "delay": {"type": "number", "default": 0.15, "min": 0, "max": 5,
45
+ "label_pt": "Segundos antes de começar a encher"},
46
+ "ease": {"type": "choice", "choices": ["suave", "linear"], "default": "suave",
47
+ "label_pt": "Ritmo: suave (rápido e freia no fim) ou linear (constante)"},
48
+ "card": {"type": "bool", "default": True, "label_pt": "Cartão atrás da barra"},
49
+ },
50
+ "default_duration": 3.0,
51
+ "default_region": "baixo",
52
+ "sfx": [{"at": 0.0, "name": "pop"}, {"at": 0.15, "name": "count", "volume": 1.3}],
53
+ "example": {"value": 87, "label": "Vagas preenchidas", "note": "restam poucas vagas nesta turma"},
54
+ }
55
+
56
+ LAND = 0.4
57
+ FIT = 0.97 # text fitted to a full width keeps 3% spare: the half-size preview rounds glyphs wider
58
+
59
+
60
+ def _decimals(p: dict) -> int:
61
+ if p["decimals"] is not None:
62
+ return int(p["decimals"])
63
+ return max(E.auto_decimals(p["value"], 1), E.auto_decimals(p["start_value"], 1))
64
+
65
+
66
+ def _stripes(cv: E.Canvas, m, t: float, period: float, opacity: float) -> None:
67
+ """Diagonal light stripes moving to the right, clipped to the mask."""
68
+ if m is None or opacity <= 0.003:
69
+ return
70
+ h, w = m.a.shape
71
+ per = cv.len(period)
72
+ xs = np.arange(w, dtype=np.float32)[None, :] + m.x + 0.5
73
+ ys = np.arange(h, dtype=np.float32)[:, None] + m.y + 0.5
74
+ u = (xs + ys - cv.len(t * period * 1.6)) / per
75
+ tri = np.abs((u % 1.0) - 0.5) * 2.0 # 0..1 triangle wave
76
+ edge = 1.0 / max(1.0, per) # about one pixel of soft edge
77
+ band = np.clip((tri - 0.5) / (2 * edge) + 0.5, 0.0, 1.0)
78
+ cv.fill_mask(E.Mask(m.a * band, m.x, m.y), "white", opacity)
79
+
80
+
81
+ # ------------------------------------------------------------------ layout (once per item)
82
+
83
+ @lru_cache(maxsize=64)
84
+ def _layout(params_json: str, box: E.Box) -> dict:
85
+ p = json.loads(params_json)
86
+ theme = box.theme
87
+ card = p["card"]
88
+ dec = _decimals(p)
89
+ label = p["label"].strip()
90
+ note = p["note"].strip()
91
+ show = p["show_value"]
92
+ widest = E.format_number(100 if p["value"] >= 99.5 or p["start_value"] >= 99.5 else 88.8, dec)
93
+ accent_ok = not card or E.contrast(theme.accent, theme.card) >= 2.4
94
+ out = {"dec": dec, "label": label, "note": note, "card": card, "show": show,
95
+ "value_color": "accent" if accent_ok else "text", "mode": p["mode"], "segments": p["segments"]}
96
+ note_size = 32.0
97
+ if p["mode"] == "barra":
98
+ pad = 44.0 if card else 8.0
99
+ card_w = box.w
100
+ inner = card_w - 2 * pad
101
+ v_size = 66.0
102
+ pct = 0.56
103
+ num_w = E.measure(widest, "sans_black", v_size, tabular=True).width
104
+ val_w = (num_w + v_size * 0.03 + E.measure("%", "sans_black", v_size * pct).width) if show else 0.0
105
+ l_size = E.fit_text(label, "text_bold", inner - val_w - 30.0, 44.0, 28.0) if label else 0.0
106
+ l_cap = E.measure("H", "text_bold", l_size).cap if label else 0.0
107
+ v_cap = E.measure("0", "sans_black", v_size).cap if show else 0.0
108
+ head = max(l_cap, v_cap)
109
+ bar_h = 38.0
110
+ gap = 28.0 if head else 0.0
111
+ if note:
112
+ note_size = E.fit_text(note, "text_semibold", inner * FIT, 32.0, 24.0)
113
+ n_cap = E.measure("H", "text_semibold", note_size).cap if note else 0.0
114
+ n_gap = 26.0 if note else 0.0
115
+ card_h = 2 * pad + head + gap + bar_h + n_gap + n_cap + (4.0 if note else 0.0)
116
+ top = (box.h - card_h) / 2.0
117
+ x0 = (box.w - card_w) / 2.0
118
+ head_base = top + pad + head
119
+ out.update(card_box=(x0, top, card_w, card_h), radius=min(40.0, card_h * 0.2), l_size=l_size,
120
+ v_size=v_size, pct=pct, num_w=num_w, head_base=head_base, left=x0 + pad, right=x0 + pad + inner,
121
+ bar=(x0 + pad, head_base + gap, inner, bar_h), note_size=note_size,
122
+ note_base=head_base + gap + bar_h + n_gap + n_cap)
123
+ else:
124
+ pad = 44.0 if card else 8.0
125
+ R = 150.0
126
+ ring_w = 30.0
127
+ v_size = 92.0
128
+ pct = 0.5
129
+ num_w = E.measure(widest, "sans_black", v_size, tabular=True).width
130
+ full_w = num_w + v_size * 0.03 + E.measure("%", "sans_black", v_size * pct).width
131
+ inner_room = (R - ring_w / 2.0) * 2 * 0.8
132
+ if full_w > inner_room:
133
+ v_size *= inner_room / full_w
134
+ num_w = E.measure(widest, "sans_black", v_size, tabular=True).width
135
+ l_size = E.fit_text(label, "text_bold", (box.w - 2 * pad) * FIT, 42.0, 28.0) if label else 0.0
136
+ l_cap = E.measure("H", "text_bold", l_size).cap if label else 0.0
137
+ if note:
138
+ note_size = E.fit_text(note, "text_semibold", (box.w - 2 * pad) * FIT, 32.0, 24.0)
139
+ n_cap = E.measure("H", "text_semibold", note_size).cap if note else 0.0
140
+ l_gap = 34.0 if label else 0.0
141
+ n_gap = 18.0 if note else 0.0
142
+ content_w = max(2 * R + ring_w, E.measure(label, "text_bold", l_size).width if label else 0.0,
143
+ E.measure(note, "text_semibold", note_size).width if note else 0.0)
144
+ card_w = min(box.w, content_w + 2 * pad + 20.0)
145
+ card_h = 2 * pad + 2 * R + ring_w + l_gap + l_cap + n_gap + n_cap + (4.0 if note else 0.0)
146
+ if card_h > box.h: # a short region: shrink the ring
147
+ k = max(0.5, 1.0 - (card_h - box.h) / (2 * R + ring_w))
148
+ R, ring_w, v_size = R * k, ring_w * k, v_size * k
149
+ num_w = E.measure(widest, "sans_black", v_size, tabular=True).width
150
+ card_h = 2 * pad + 2 * R + ring_w + l_gap + l_cap + n_gap + n_cap + (4.0 if note else 0.0)
151
+ top = (box.h - card_h) / 2.0
152
+ x0 = (box.w - card_w) / 2.0
153
+ cy = top + pad + ring_w / 2.0 + R
154
+ v_cap = E.measure("0", "sans_black", v_size).cap
155
+ label_base = cy + R + ring_w / 2.0 + l_gap + l_cap
156
+ out.update(card_box=(x0, top, card_w, card_h), radius=min(44.0, card_w * 0.1), l_size=l_size,
157
+ v_size=v_size, pct=pct, num_w=num_w, ring=(box.w / 2.0, cy, R, ring_w),
158
+ value_base=cy + v_cap / 2.0, label_base=label_base, note_size=note_size,
159
+ note_base=label_base + n_gap + n_cap, cx=box.w / 2.0)
160
+ return out
161
+
162
+
163
+ # ------------------------------------------------------------------ drawing
164
+
165
+ def _fill(params: dict, t: float) -> tuple[float, float]:
166
+ """(percentage shown now, eased progress 0..1)."""
167
+ fn = "linear" if params["ease"] == "linear" else "out_quart"
168
+ k = E.eased(t, params["delay"], params["duration"], fn)
169
+ return params["start_value"] + (params["value"] - params["start_value"]) * k, k
170
+
171
+
172
+ def _t_end(params: dict) -> float:
173
+ return params["delay"] + params["duration"]
174
+
175
+
176
+ def _value(cv: E.Canvas, L: dict, x: float, base: float, v: float, anchor: str, scale: float, outline: dict) -> None:
177
+ size = L["v_size"]
178
+ num = E.format_number(v, L["dec"])
179
+ pct_s = size * L["pct"]
180
+ num_w = E.measure(num, "sans_black", size, tabular=True).width
181
+ pct_w = E.measure("%", "sans_black", pct_s).width
182
+ total = num_w + size * 0.03 + pct_w
183
+ x0 = x - total if anchor == "r" else x - total / 2.0
184
+ with cv.group(scale=scale, pivot=(x0 + total / 2.0, base - size * 0.36)):
185
+ cv.text(x0, base, num, "sans_black", size, color=L["value_color"], tabular=True, **outline)
186
+ cv.text(x0 + num_w + size * 0.03, base, "%", "sans_black", pct_s, color="text", opacity=0.9, **outline)
187
+
188
+
189
+ STATIC_AFTER = 0.5 # seconds: from here on the card, label, note and tracks never change
190
+
191
+
192
+ def _segments(L: dict) -> list[tuple[float, float]]:
193
+ """(x, width) of each piece of the bar."""
194
+ bx, by, bw, bh = L["bar"]
195
+ n = L["segments"] or 1
196
+ gap = 10.0 if L["segments"] else 0.0
197
+ sw = (bw - gap * (n - 1)) / n
198
+ return [(bx + i * (sw + gap), sw) for i in range(n)]
199
+
200
+
201
+ def _arcs(L: dict) -> list[tuple[float, float]]:
202
+ """(start degrees, sweep degrees) of each piece of the ring."""
203
+ rcx, rcy, R, rw = L["ring"]
204
+ segs = L["segments"]
205
+ n = segs or 1
206
+ # round caps stick out rw/2 at each end: leave room for them so the gaps stay open
207
+ cap_deg = math.degrees(rw / 2.0 / R) if segs else 0.0
208
+ gap_deg = 8.0 if segs else 0.0
209
+ span = 360.0 / n
210
+ return [(-90.0 + i * span + gap_deg / 2.0 + cap_deg, span - gap_deg - 2 * cap_deg) for i in range(n)]
211
+
212
+
213
+ def _draw_static(cv: E.Canvas, L: dict, note_in: float) -> None:
214
+ """Everything that does not move once the card is in: card, label, note and the empty track."""
215
+ card = L["card"]
216
+ outline = {} if card else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
217
+ soft = {} if card else {"stroke": 4.0, "stroke_color": "black"}
218
+ track = "track" if card else (0, 0, 0, 120)
219
+ if card:
220
+ cx0, cy0, cw, ch = L["card_box"]
221
+ cv.card(cx0, cy0, cw, ch, radius=L["radius"])
222
+ if L["mode"] == "barra":
223
+ if L["label"]:
224
+ cv.text(L["left"], L["head_base"], L["label"], "text_bold", L["l_size"], color="text", **outline)
225
+ bx, by, bw, bh = L["bar"]
226
+ for sx, sw in _segments(L):
227
+ cv.pill(sx, by, sw, bh, fill=track)
228
+ if L["note"]:
229
+ cv.text(L["left"], L["note_base"], L["note"], "text_semibold", L["note_size"],
230
+ color="muted" if card else "text", opacity=note_in, **soft)
231
+ return
232
+ rcx, rcy, R, rw = L["ring"]
233
+ if L["segments"]:
234
+ for a0, sweep in _arcs(L):
235
+ cv.polyline(E.ellipse_points(rcx, rcy, R, R, a0, sweep), rw, color=track)
236
+ else:
237
+ cv.ring(rcx, rcy, R, rw, color=track)
238
+ if L["label"]:
239
+ cv.text(L["cx"], L["label_base"], L["label"], "text_bold", L["l_size"], color="text", anchor="ms", **outline)
240
+ if L["note"]:
241
+ cv.text(L["cx"], L["note_base"], L["note"], "text_semibold", L["note_size"],
242
+ color="muted" if card else "text", opacity=note_in, anchor="ms", **soft)
243
+
244
+
245
+ def _static_sprite(params_json: str, box: E.Box):
246
+ """The static part drawn once (card, label, note and the empty track)."""
247
+ return E.static_sprite(("barra_progresso", params_json), box,
248
+ lambda c: _draw_static(c, _layout(params_json, box), 1.0))
249
+
250
+
251
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
252
+ cv = E.Canvas(box)
253
+ t = frame_index / fps
254
+ key = json.dumps(params, sort_keys=True)
255
+ L = _layout(key, box)
256
+ cx0, cy0, cw, ch = L["card_box"]
257
+ card = L["card"]
258
+ t_in = t + 1.0 / fps
259
+ alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
260
+ scale = 0.92 + 0.08 * E.out_back(E.progress(t_in, 0.0, 0.5), 1.4)
261
+ rise = 30.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
262
+ outline = {} if card else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
263
+ v, k = _fill(params, t)
264
+ frac = v / 100.0
265
+ t_end = _t_end(params)
266
+ moving = params["value"] != params["start_value"] and 0.0 < k < 1.0
267
+ land = E.pulse(t, t_end - 0.02, LAND) if params["value"] != params["start_value"] else 0.0
268
+ stripe_op = 0.16 * (1.0 - E.eased(t, t_end - 0.25, 0.3))
269
+ segs = L["segments"]
270
+
271
+ with cv.group(opacity=alpha, scale=scale, dy=rise, pivot=(cx0 + cw / 2.0, cy0 + ch / 2.0)):
272
+ if t_in >= STATIC_AFTER: # the entrance is over: the group is the identity
273
+ cv.paste_sprite(_static_sprite(key, box))
274
+ else:
275
+ _draw_static(cv, L, E.eased(t, 0.1, 0.35))
276
+
277
+ if L["mode"] == "barra":
278
+ if L["show"]:
279
+ _value(cv, L, L["right"], L["head_base"], v, "r", 1.0 + 0.07 * land, outline)
280
+ bx, by, bw, bh = L["bar"]
281
+ fills = []
282
+ pieces = _segments(L)
283
+ for i, (sx, sw) in enumerate(pieces):
284
+ f = E.clamp01(frac * len(pieces) - i)
285
+ if f <= 0:
286
+ break
287
+ fw = sw * f
288
+ m = cv.rect_mask(sx, by, max(fw, bh), bh, radius=bh / 2.0)
289
+ if fw < bh:
290
+ m = E.clip_mask(m, x1=cv.px(sx + fw, 0.0)[0])
291
+ fills.append(m)
292
+ tip_x = bx + bw * frac if not segs else None
293
+
294
+ def draw_fill() -> None:
295
+ grad = E.Gradient(box.theme.accent, E.mix_color(box.theme.accent, (255, 255, 255), 0.28),
296
+ start=(bx, 0.0), end=(bx + bw, 0.0))
297
+ for m in fills:
298
+ if m is None:
299
+ continue
300
+ cv.fill_mask(m, grad)
301
+ cv.fill_mask(_gloss(m), "white", 0.16)
302
+ if moving:
303
+ _stripes(cv, m, t, 26.0, stripe_op)
304
+
305
+ if fills:
306
+ if land > 0.02:
307
+ cv.glow(draw_fill, radius=20.0, opacity=0.65 * land)
308
+ else:
309
+ draw_fill()
310
+ if moving and tip_x is not None and frac > 0:
311
+ tx = max(bx + bh / 2.0, tip_x - bh / 2.0)
312
+ cv.glow(lambda: cv.circle(tx, by + bh / 2.0, bh * 0.2, fill="white"), radius=14.0, opacity=0.9,
313
+ color="white")
314
+ else:
315
+ rcx, rcy, R, rw = L["ring"]
316
+ arcs = []
317
+ n = len(_arcs(L))
318
+ for i, (a0, sweep) in enumerate(_arcs(L)):
319
+ f = E.clamp01(frac * n - i)
320
+ if f > 0:
321
+ arcs.append((a0, sweep * f))
322
+
323
+ def draw_arcs() -> None:
324
+ for a0, sw in arcs:
325
+ if sw >= 359.99:
326
+ cv.ring(rcx, rcy, R, rw, color="accent")
327
+ else:
328
+ cv.polyline(E.ellipse_points(rcx, rcy, R, R, a0, sw), rw, color="accent")
329
+
330
+ if arcs:
331
+ if land > 0.02:
332
+ cv.glow(draw_arcs, radius=22.0, opacity=0.65 * land)
333
+ else:
334
+ draw_arcs()
335
+ if moving and not segs and frac > 0:
336
+ ang = math.radians(-90.0 + 360.0 * frac)
337
+ tx, ty = rcx + R * math.cos(ang), rcy + R * math.sin(ang)
338
+ cv.glow(lambda: cv.circle(tx, ty, rw * 0.2, fill="white"), radius=14.0, opacity=0.9, color="white")
339
+ if L["show"]:
340
+ _value(cv, L, rcx, L["value_base"], v, "m", 1.0 + 0.07 * land, outline)
341
+ return cv
342
+
343
+
344
+ def _gloss(m):
345
+ """The upper 45% of a bar mask (a soft highlight on top of the fill)."""
346
+ h = m.a.shape[0]
347
+ rows = np.arange(h, dtype=np.float32)
348
+ k = np.clip((h * 0.45 - rows) / max(1.0, h * 0.25), 0.0, 1.0)
349
+ return E.Mask(m.a * k[:, None], m.x, m.y)
350
+
351
+
352
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
353
+ cues = [{"at": 0.0, "name": "pop", "why": "entrada"}]
354
+ start, dur = params["delay"], params["duration"]
355
+ if params["value"] != params["start_value"]:
356
+ steps = 1 if params["ease"] != "linear" else max(1, int(dur / 0.6 + 0.5))
357
+ for k in range(steps):
358
+ cues.append({"at": start + k * 0.6, "name": "count", "volume": 1.3, "why": "barra enchendo"})
359
+ if params["value"] >= 100:
360
+ cues.append({"at": start + dur, "name": "success", "why": "100%"})
361
+ return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
362
+
363
+
364
+ def suggested_duration(params: dict) -> float:
365
+ """Fill, landing glow and about 1.5 s to read."""
366
+ return round(max(TEMPLATE["default_duration"], _t_end(params) + LAND + 1.5), 2)
367
+
368
+
369
+ def settle(params: dict, fps: int) -> float:
370
+ return _t_end(params) + LAND + 0.05
@@ -0,0 +1,132 @@
1
+ """carimbo: uma palavra que cai carimbada na tela, torta, com a moldura batendo junto.
2
+
3
+ O carimbo vem de cima grande e desfocado, bate (a tela treme de leve), e fica. Serve para fechar uma
4
+ frase com uma palavra só: APROVADO, ESGOTADO, TEM UM CUSTO. Pode ter uma linha menor embaixo.
5
+
6
+ Tempo: `delay` até a batida; a partir daí fica parado até o fim do item.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from functools import lru_cache
13
+
14
+ from motion import engine as E
15
+
16
+ TEMPLATE = {
17
+ "id": "carimbo",
18
+ "label_pt": "Carimbo",
19
+ "description_pt": "Uma palavra cai carimbada na tela, torta e com moldura, e a tela treme junto. "
20
+ "Pode ter uma linha menor embaixo.",
21
+ "use_when_pt": "Para fechar uma frase com uma palavra só, no instante em que ela é falada: "
22
+ "APROVADO, ESGOTADO, TEM UM CUSTO, VAGAS ENCERRADAS.",
23
+ "notes_pt": "Palavra curta lê melhor (até 22 letras). Não use como legenda: é um golpe só, na palavra "
24
+ "que importa.",
25
+ "category": "enfase",
26
+ "version": 1,
27
+ "params": {
28
+ "texto": {"type": "text", "default": "APROVADO", "max_len": 22, "required_text": True,
29
+ "label_pt": "A palavra do carimbo (até 22 letras)"},
30
+ "subtexto": {"type": "text", "default": "", "max_len": 28,
31
+ "label_pt": "Linha menor embaixo, dentro da moldura (opcional)"},
32
+ "delay": {"type": "number", "default": 0.12, "min": 0, "max": 30,
33
+ "label_pt": "Segundos até a batida do carimbo"},
34
+ "angulo": {"type": "number", "default": -7.0, "min": -20, "max": 20,
35
+ "label_pt": "Inclinação do carimbo em graus (negativo inclina para a esquerda)"},
36
+ "estilo": {"type": "choice", "choices": ["moldura", "cheio"], "default": "moldura",
37
+ "label_pt": "Moldura vazada (padrão) ou retângulo pintado na cor de destaque"},
38
+ "tremer": {"type": "bool", "default": True,
39
+ "label_pt": "A moldura treme quando o carimbo bate"},
40
+ },
41
+ "default_duration": 2.0,
42
+ "default_region": "centro",
43
+ "sfx": [{"at": 0.12, "name": "bass_drop", "volume": 0.9}],
44
+ "example": {"texto": "TEM UM CUSTO", "subtexto": "servidor, IA, suporte", "angulo": -5},
45
+ }
46
+
47
+ FONT = "sans_black"
48
+ SUB_FONT = "text_bold"
49
+ MAX_SIZE = 150.0
50
+ MIN_SIZE = 44.0
51
+ QUEDA = 0.20 # segundos da queda até bater
52
+ TREMOR = 0.22 # segundos de tremida depois da batida
53
+ ESCALA_INICIAL = 2.6
54
+
55
+
56
+ @lru_cache(maxsize=64)
57
+ def _layout(params_json: str, box: E.Box) -> dict:
58
+ p = json.loads(params_json)
59
+ texto = p["texto"].strip()
60
+ sub = p["subtexto"].strip()
61
+ pad_x, pad_y = 46.0, 30.0
62
+ disp = max(220.0, box.w * 0.86) - 2 * pad_x
63
+ size = min(MAX_SIZE, E.fit_text(texto.upper(), FONT, disp, MAX_SIZE, MIN_SIZE))
64
+ cap = E.measure("H", FONT, size).cap
65
+ largura = E.measure(texto.upper(), FONT, size).width
66
+ sub_size = sub_cap = 0.0
67
+ sub_w = 0.0
68
+ if sub:
69
+ sub_size = min(size * 0.34, E.fit_text(sub, SUB_FONT, disp, size * 0.34, 20.0))
70
+ sub_cap = E.measure("H", SUB_FONT, sub_size).cap
71
+ sub_w = E.measure(sub, SUB_FONT, sub_size).width
72
+ conteudo_h = cap + (sub_cap + size * 0.28 if sub else 0.0)
73
+ w = min(box.w - 8.0, max(largura, sub_w) + 2 * pad_x)
74
+ h = conteudo_h + 2 * pad_y
75
+ return {"size": size, "cap": cap, "w": w, "h": h, "pad_y": pad_y,
76
+ "sub_size": sub_size, "sub_cap": sub_cap, "texto": texto.upper(), "sub": sub,
77
+ "borda": max(7.0, size * 0.085), "raio": max(14.0, size * 0.16)}
78
+
79
+
80
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
81
+ cv = E.Canvas(box)
82
+ t = frame_index / fps + 1.0 / fps
83
+ L = _layout(json.dumps(params, sort_keys=True), box)
84
+ bate = float(params["delay"])
85
+ if t < bate - QUEDA:
86
+ return cv
87
+ queda = E.clamp01((t - (bate - QUEDA)) / QUEDA) # 0 no alto, 1 na batida
88
+ escala = ESCALA_INICIAL + (1.0 - ESCALA_INICIAL) * (queda ** 2.2) # desaba no fim (in_quart)
89
+ opac = min(1.0, queda * 1.8)
90
+ tremida = 0.0
91
+ if params["tremer"] and 0 <= t - bate < TREMOR:
92
+ f = E.clamp01((t - bate) / TREMOR)
93
+ tremida = (1.0 - f) * 5.0 * (1 if int((t - bate) * fps) % 2 == 0 else -1)
94
+ x = box.cx - L["w"] / 2.0
95
+ y = box.cy - L["h"] / 2.0
96
+ cheio = params["estilo"] == "cheio"
97
+ with cv.group(opacity=opac, scale=escala, pivot=(box.cx, box.cy),
98
+ rotate=float(params["angulo"]) + tremida * 0.25, dx=tremida):
99
+ if cheio:
100
+ cv.rect(x, y, L["w"], L["h"], radius=L["raio"], fill="accent",
101
+ shadow=E.Shadow(blur=18.0, dy=8.0, opacity=0.45))
102
+ else:
103
+ if box.theme.style != "vidro": # papel, pop, neon: the stamp lands on a card, not on the video
104
+ cv.card(x, y, L["w"], L["h"], radius=L["raio"], border=False)
105
+ cv.rect(x, y, L["w"], L["h"], radius=L["raio"], fill=None, stroke="accent",
106
+ stroke_width=L["borda"], shadow=E.Shadow(blur=14.0, dy=6.0, opacity=0.4))
107
+ cor = "on_accent" if cheio else "accent"
108
+ base = y + L["pad_y"] + L["cap"]
109
+ cv.text(box.cx, base, L["texto"], FONT, L["size"], color=cor, anchor="ms",
110
+ stroke=0.0, shadow=E.Shadow(blur=10.0, dy=4.0, opacity=0.35))
111
+ if L["sub"]:
112
+ cv.text(box.cx, base + L["size"] * 0.28 + L["sub_cap"], L["sub"], SUB_FONT, L["sub_size"],
113
+ color=cor, opacity=0.9, anchor="ms")
114
+ return cv
115
+
116
+
117
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
118
+ return [{"frame": E.frame_of(float(params["delay"]), fps), "name": "bass_drop", "volume": 0.9,
119
+ "why": "batida do carimbo"}]
120
+
121
+
122
+ def settle(params: dict, fps: int) -> float:
123
+ return float(params["delay"]) + TREMOR + 0.05
124
+
125
+
126
+ def suggested_duration(params: dict) -> float:
127
+ return round(max(TEMPLATE["default_duration"], float(params["delay"]) + 1.4), 2)
128
+
129
+
130
+ def ink(params: dict, box: E.Box) -> tuple[float, float, float, float]:
131
+ L = _layout(json.dumps(params, sort_keys=True), box)
132
+ return (box.cx - L["w"] / 2.0, box.cy - L["h"] / 2.0, box.cx + L["w"] / 2.0, box.cy + L["h"] / 2.0)