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,510 @@
1
+ """grafico_barras: 2 to 6 bars growing one after the other, the value counting over each bar and its name
2
+ under it (SPEC Contract 12, catalog). Two looks: "colunas" (standing bars) and "barras" (lying bars, better
3
+ for longer names). The highlighted bar gets the accent color and a short glow when it lands.
4
+
5
+ A bar is a rounded rect cut at the axis (`E.clip_mask`), so it is rounded on the free end and flat on the
6
+ axis. The card, title, axis and names are drawn once (`E.static_sprite`) and pasted after the entrance.
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": "grafico_barras",
18
+ "label_pt": "Gráfico de barras",
19
+ "description_pt": "De 2 a 6 barras que crescem uma depois da outra num cartão, com o valor contando em cada "
20
+ "barra e o nome embaixo. A barra de destaque ganha a cor do vídeo e um brilho quando chega.",
21
+ "use_when_pt": "Para comparar poucos números: antes e depois, mês a mês, um jeito contra o outro. Só com "
22
+ "números que existem no material da pessoa, sem inventar.",
23
+ "category": "números",
24
+ "version": 1,
25
+ "params": {
26
+ "bars": {
27
+ "type": "list", "min_items": 2, "max_items": 6,
28
+ "item": {"type": "object", "fields": {
29
+ "label": {"type": "text", "default": "", "max_len": 22, "label_pt": "Nome da barra (curto; em colunas ele quebra em 2 linhas)"},
30
+ "value": {"type": "number", "min": 0, "max": 1e13, "label_pt": "Valor da barra (0 ou mais)"},
31
+ "highlight": {"type": "bool", "default": False, "label_pt": "Esta barra fica em destaque"},
32
+ }},
33
+ "default": [{"label": "Jan", "value": 120}, {"label": "Fev", "value": 260},
34
+ {"label": "Mar", "value": 410}, {"label": "Abr", "value": 680}],
35
+ "label_pt": "Barras, de 2 a 6: [{\"label\": \"Jan\", \"value\": 120}, ...] (use ponto para decimais)",
36
+ },
37
+ "title": {"type": "text", "default": "", "max_len": 34, "label_pt": "Título em cima do gráfico (opcional)"},
38
+ "prefix": {"type": "text", "default": "", "max_len": 4, "label_pt": "Antes de cada valor, ex.: R$ ou +"},
39
+ "suffix": {"type": "text", "default": "", "max_len": 8, "label_pt": "Depois de cada valor, ex.: %, h ou mil"},
40
+ "decimals": {"type": "integer", "default": None, "nullable": True, "min": 0, "max": 2,
41
+ "label_pt": "Casas decimais (vazio: automático)"},
42
+ "mode": {"type": "choice", "choices": ["colunas", "barras"], "default": "colunas",
43
+ "label_pt": "colunas (barras em pé) ou barras (deitadas, boas para nomes longos). Colunas que não "
44
+ "cabem viram barras sozinhas"},
45
+ "highlight": {"type": "choice", "choices": ["maior", "ultima", "todas", "nenhuma"], "default": "maior",
46
+ "label_pt": "Qual barra fica na cor de destaque (se nenhuma barra tiver highlight: true)"},
47
+ "show_values": {"type": "bool", "default": True, "label_pt": "Mostrar o valor em cada barra"},
48
+ "duration": {"type": "number", "default": 0.9, "min": 0.2, "max": 5,
49
+ "label_pt": "Segundos que cada barra leva para crescer"},
50
+ "stagger": {"type": "number", "default": 0.18, "min": 0, "max": 2,
51
+ "label_pt": "Segundos entre uma barra e a próxima"},
52
+ "delay": {"type": "number", "default": 0.15, "min": 0, "max": 5,
53
+ "label_pt": "Segundos antes da primeira barra crescer"},
54
+ "card": {"type": "bool", "default": True, "label_pt": "Cartão atrás do gráfico"},
55
+ },
56
+ "default_duration": 3.5,
57
+ "default_region": "baixo",
58
+ "sfx": [{"at": 0.0, "name": "pop"}, {"at": 0.15, "name": "tick", "volume": 0.8}],
59
+ "example": {"title": "Horas por semana na loja", "suffix": "h", "highlight": "ultima",
60
+ "bars": [{"label": "Antes", "value": 20}, {"label": "Depois", "value": 4}]},
61
+ }
62
+
63
+ LAND = 0.4 # seconds of the glow on a highlighted bar after it lands
64
+ FIT = 0.97 # text fitted to a full width keeps 3% spare: the half-size preview rounds glyphs wider
65
+ ROOM = 40.0 # design px of the region left free (the entrance rise and the card shadow)
66
+ OVERSHOOT = 1.0 # out_back overshoot of the growth (about 5% of the height)
67
+
68
+
69
+ # ------------------------------------------------------------------ helpers
70
+
71
+ def _decimals(p: dict) -> int:
72
+ if p["decimals"] is not None:
73
+ return int(p["decimals"])
74
+ d = max(E.auto_decimals(b["value"]) for b in p["bars"])
75
+ money = E.looks_like_money(p["prefix"], p["suffix"])
76
+ return 2 if money and d else d
77
+
78
+
79
+ def _join(num: str, prefix: str, suffix: str) -> str:
80
+ """'R$' + '12' -> 'R$ 12'; '+' + '12' -> '+12'; '12' + '%' -> '12%'; '12' + 'mil' -> '12 mil'."""
81
+ pre = (prefix + (" " if len(prefix) > 1 else "")) if prefix else ""
82
+ suf = ((" " if len(suffix) > 1 else "") + suffix) if suffix else ""
83
+ return pre + num + suf
84
+
85
+
86
+ def _value_text(v: float, dec: int, prefix: str, suffix: str, compact: bool, final: float | None = None) -> str:
87
+ """The value as shown; `final` (the bar's end value) keeps the compact form and its unit while it counts."""
88
+ if compact and abs(final if final is not None else v) >= 1000:
89
+ num = E.format_compact(v, 1, like=final)
90
+ elif compact:
91
+ num = E.format_number(v, min(dec, E.auto_decimals(final if final is not None else v, 1)))
92
+ else:
93
+ num = E.format_number(v, dec)
94
+ return _join(num, prefix, suffix)
95
+
96
+
97
+ def _rest_color(theme: E.Theme, card: bool):
98
+ if not card: # straight on the video: a solid light bar with a shadow
99
+ return (236, 236, 236, 225)
100
+ return (255, 255, 255, 72) if theme.dark else (17, 17, 17, 52)
101
+
102
+
103
+ def _fill_bar(cv: E.Canvas, m, color, card: bool) -> None:
104
+ """Fill a bar mask; without a card the bar gets a soft drop shadow to read on any video."""
105
+ if m is None:
106
+ return
107
+ if not card:
108
+ sh = E.blur_mask(m, cv.len(9.0), cv.W, cv.H)
109
+ sh.y += int(round(cv.len(5.0)))
110
+ cv.fill_mask(sh, (0, 0, 0), 0.45)
111
+ cv.fill_mask(m, color)
112
+
113
+
114
+ def _highlighted(p: dict) -> list[bool]:
115
+ bars = p["bars"]
116
+ chosen = [bool(b.get("highlight")) for b in bars]
117
+ if any(chosen):
118
+ return chosen
119
+ mode = p["highlight"]
120
+ if mode == "todas":
121
+ return [True] * len(bars)
122
+ if mode == "nenhuma":
123
+ return [False] * len(bars)
124
+ if mode == "ultima":
125
+ return [i == len(bars) - 1 for i in range(len(bars))]
126
+ top = max(b["value"] for b in bars)
127
+ first = next(i for i, b in enumerate(bars) if b["value"] == top)
128
+ return [i == first for i in range(len(bars))]
129
+
130
+
131
+ COUNT_SAMPLES = 33
132
+
133
+
134
+ def _count_steps() -> list[tuple[float, float]]:
135
+ """(bar length, count) pairs over the growth of a bar (the length overshoots a little, the count does not)."""
136
+ return [(E.out_back(g, OVERSHOOT), E.out_cubic(g))
137
+ for g in (k / (COUNT_SAMPLES - 1) for k in range(COUNT_SAMPLES))]
138
+
139
+
140
+ def _widest(texts: list[str]) -> str:
141
+ return max(texts, key=lambda s: E.measure(s, "sans_black", 100, tabular=True).width)
142
+
143
+
144
+ def _fit_values(texts_of, width: float, max_size: float, min_size: float) -> tuple[float, bool]:
145
+ """Size for the widest value text; switch to the compact form ('12,5 mil') when it does not fit."""
146
+ for compact in (False, True):
147
+ texts = texts_of(compact)
148
+ widest = max(texts, key=lambda s: E.measure(s, "sans_black", 100, tabular=True).width)
149
+ size = E.fit_text(widest, "sans_black", width, max_size, min_size, tabular=True)
150
+ if E.measure(widest, "sans_black", size, tabular=True).width <= width + 0.5 or compact:
151
+ return size, compact
152
+ return min_size, True
153
+
154
+
155
+ # ------------------------------------------------------------------ layout (once per item)
156
+
157
+ COL_VALUE_MIN = 24.0
158
+ COL_LABEL_MIN = 24.0
159
+
160
+
161
+ def _columns_fit(p: dict, box: E.Box, pad: float, texts_of) -> bool:
162
+ """Standing bars need every value on one line over its bar and every name word inside its slot."""
163
+ n = len(p["bars"])
164
+ slot = min(200.0, (box.w - 2 * pad) / n)
165
+ if p["show_values"]:
166
+ size, _compact = _fit_values(texts_of, slot * 0.98, 46.0, COL_VALUE_MIN)
167
+ widest = max(E.measure(s, "sans_black", size, tabular=True).width for s in texts_of(_compact))
168
+ if widest > slot * 0.98 + 0.5:
169
+ return False
170
+ for b in p["bars"]:
171
+ for word in b["label"].split():
172
+ if E.measure(word, "text_semibold", COL_LABEL_MIN).width > slot * 0.96:
173
+ return False
174
+ lines = E.wrap_text(b["label"], "text_semibold", COL_LABEL_MIN, slot * 0.96, 2)
175
+ if len(lines) > 2 or any(E.measure(ln, "text_semibold", COL_LABEL_MIN).width > slot * 0.96 for ln in lines):
176
+ return False
177
+ return True
178
+
179
+ @lru_cache(maxsize=64)
180
+ def _layout(params_json: str, box: E.Box) -> dict:
181
+ p = json.loads(params_json)
182
+ bars = p["bars"]
183
+ n = len(bars)
184
+ dec = _decimals(p)
185
+ prefix, suffix = p["prefix"].strip(), p["suffix"].strip()
186
+ card = p["card"]
187
+ theme = box.theme
188
+ pad = 48.0 if card else 10.0
189
+ title = p["title"].strip()
190
+ vmax = max(b["value"] for b in bars)
191
+ fracs = [(b["value"] / vmax) if vmax > 0 else 0.0 for b in bars]
192
+ hi = _highlighted(p)
193
+ accent_ok = not card or E.contrast(theme.accent, theme.card) >= 2.4
194
+ out = {"n": n, "dec": dec, "prefix": prefix, "suffix": suffix, "card": card, "fracs": fracs, "hi": hi,
195
+ "hi_text": "accent" if accent_ok else "text", "mode": p["mode"], "title": title,
196
+ "show_values": p["show_values"]}
197
+
198
+ def counted(b: dict, compact: bool) -> list[str]:
199
+ """Every text the bar shows while it counts (a count can be wider than its final value: 0,9 tri)."""
200
+ return [_value_text(b["value"] * c, dec, prefix, suffix, compact, b["value"]) for _, c in _count_steps()]
201
+
202
+ def texts_of(compact: bool) -> list[str]:
203
+ return [_widest(counted(b, compact)) for b in bars]
204
+
205
+ title_size = E.fit_text(title, "text_bold", (box.w - 2 * pad) * FIT, 42.0, 30.0) if title else 0.0
206
+ title_cap = E.measure("H", "text_bold", title_size).cap if title else 0.0
207
+ title_w = E.measure(title, "text_bold", title_size).width if title else 0.0
208
+ title_gap = 48.0 if title else 0.0
209
+ out.update(title_size=title_size)
210
+
211
+ mode = p["mode"]
212
+ if mode == "colunas" and not _columns_fit(p, box, pad, texts_of):
213
+ mode = "barras" # names or values too long for standing bars: lay them down
214
+ out["mode"] = mode
215
+ if mode == "colunas":
216
+ slot = min(200.0, (box.w - 2 * pad) / n)
217
+ card_w = min(box.w, max(n * slot + 2 * pad, title_w + 2 * pad, 0.55 * box.w))
218
+ slot = min(200.0, (card_w - 2 * pad) / n)
219
+ col_w = min(slot * 0.58, 118.0)
220
+ val_size, compact = _fit_values(texts_of, slot * 0.98, 46.0, COL_VALUE_MIN)
221
+ val_cap = E.measure("0", "sans_black", val_size).cap if p["show_values"] else 0.0
222
+ val_gap = 16.0 if p["show_values"] else 0.0
223
+ # names: up to two balanced lines inside the slot, the same size for every bar
224
+ lab_size = 34.0
225
+ names = [b["label"].strip() for b in bars]
226
+ widest_name = max(names, key=lambda s: E.measure(s, "text_semibold", 100).width)
227
+ one_line = E.fit_text(widest_name, "text_semibold", slot * 0.96, 34.0, 12.0) if widest_name else 34.0
228
+ if one_line >= 29.0: # a slightly smaller name on one line reads better than two lines
229
+ lab_size = one_line
230
+ for _ in range(12):
231
+ wrapped = [E.wrap_text(s, "text_semibold", lab_size, slot * 0.96, 2) if s else [] for s in names]
232
+ too_wide = any(E.measure(ln, "text_semibold", lab_size).width > slot * 0.96
233
+ for lines in wrapped for ln in lines)
234
+ if not too_wide or lab_size <= COL_LABEL_MIN:
235
+ break
236
+ lab_size = max(COL_LABEL_MIN, lab_size * 0.93)
237
+ lab_cap = E.measure("H", "text_semibold", lab_size).cap
238
+ lines = max((len(w) for w in wrapped), default=0)
239
+ pitch = lab_size * 1.18
240
+ lab_block = (20.0 + lab_cap + pitch * (lines - 1)) if lines else 0.0
241
+ fixed = 2 * pad + title_cap + title_gap + val_cap + val_gap + lab_block + 4.0
242
+ plot_h = max(120.0, min(400.0, box.h - ROOM - fixed))
243
+ card_h = fixed + plot_h
244
+ top = (box.h - card_h) / 2.0
245
+ x0 = (box.w - card_w) / 2.0
246
+ base = top + pad + title_cap + title_gap + val_cap + val_gap + plot_h
247
+ inner_x = x0 + (card_w - n * slot) / 2.0
248
+ cols = [inner_x + slot * (i + 0.5) for i in range(n)]
249
+ out.update(slot=slot, col_w=col_w, val_size=val_size, compact=compact, lab_size=lab_size,
250
+ lab_cap=lab_cap, pitch=pitch, wrapped=wrapped, plot_h=plot_h, base=base, cols=cols,
251
+ card_box=(x0, top, card_w, card_h), title_base=top + pad + title_cap,
252
+ title_x=x0 + pad if n > 2 or title_w > card_w * 0.7 else x0 + card_w / 2.0,
253
+ title_anchor="ls" if n > 2 or title_w > card_w * 0.7 else "ms",
254
+ axis=(inner_x + slot * 0.08, inner_x + n * slot - slot * 0.08),
255
+ col_radius=min(18.0, col_w * 0.24))
256
+ else:
257
+ card_w = box.w
258
+ inner_w = card_w - 2 * pad
259
+ x0 = (box.w - card_w) / 2.0
260
+ names = [b["label"].strip() for b in bars]
261
+ show = p["show_values"]
262
+ # stacked rows (name and value on a line, the bar under them at full width) when they fit the box;
263
+ # otherwise one line per bar: name | bar | value
264
+ lab_size, val_size = 36.0, 38.0
265
+ bar_h = 26.0 if n <= 4 else 22.0
266
+ lab_cap = E.measure("H", "text_semibold", lab_size).cap
267
+ row_h = lab_cap + 14.0 + bar_h
268
+ row_gap = 26.0 if n <= 4 else 18.0
269
+ stacked_h = 2 * pad + title_cap + title_gap + n * row_h + (n - 1) * row_gap
270
+ texts = texts_of(False)
271
+ compact = False
272
+ if stacked_h <= box.h - ROOM:
273
+ for k, name in enumerate(names):
274
+ vw = E.measure(texts[k], "sans_black", val_size, tabular=True).width if show else 0.0
275
+ room = (inner_w - vw - 24.0) * FIT
276
+ if name and E.measure(name, "text_semibold", lab_size).width > room:
277
+ lab_size = min(lab_size, E.fit_text(name, "text_semibold", room, lab_size, 26.0))
278
+ lab_cap = E.measure("H", "text_semibold", lab_size).cap
279
+ row_h = lab_cap + 14.0 + bar_h
280
+ rows_h = n * row_h + (n - 1) * row_gap
281
+ card_h = 2 * pad + title_cap + title_gap + rows_h
282
+ top = (box.h - card_h) / 2.0
283
+ first = top + pad + title_cap + title_gap
284
+ tops = [first + i * (row_h + row_gap) for i in range(n)]
285
+ pad_top = pad
286
+ out.update(layout="stacked", val_size=val_size, compact=False, lab_size=lab_size, names=names,
287
+ bar_h=bar_h, axis_x=x0 + pad, bar_max=inner_w, lab_base=[y + lab_cap for y in tops],
288
+ centers=[y + lab_cap + 14.0 + bar_h / 2.0 for y in tops], value_x=x0 + pad + inner_w)
289
+ else:
290
+ widest = max(names, key=lambda s: E.measure(s, "text_semibold", 100).width) if any(names) else ""
291
+ lab_size = E.fit_text(widest, "text_semibold", inner_w * 0.34, 34.0, 26.0) if widest else 34.0
292
+ lab_w = E.measure(widest, "text_semibold", lab_size).width if widest else 0.0
293
+ axis_x = x0 + pad + (lab_w + 24.0 if widest else 0.0)
294
+ area = x0 + pad + inner_w - axis_x
295
+ fracs = out["fracs"]
296
+ if show:
297
+ val_size, compact = _fit_values(texts_of, area * 0.42, 34.0, 26.0)
298
+ # the longest bar that still leaves room for every value after its own bar, on every frame of
299
+ # the growth (the bar overshoots a little and the counting text can be wider than the final one)
300
+ limits = [area]
301
+ for b, f in zip(bars, fracs):
302
+ if f <= 0:
303
+ continue
304
+ for (grow, _), txt in zip(_count_steps(), counted(b, compact)):
305
+ if grow > 0:
306
+ w = E.measure(txt, "sans_black", val_size, tabular=True).width
307
+ limits.append((area - 16.0 - w) / (f * grow))
308
+ bar_max = min(limits)
309
+ else:
310
+ bar_max = area
311
+ bar_h = 30.0
312
+ row_h = max(bar_h, lab_size * 1.1)
313
+ # squeeze the gaps (never the text) when the rows need more height than the region has
314
+ pad_y, t_gap = pad, title_gap
315
+ over = 2 * pad_y + title_cap + t_gap + n * row_h + (n - 1) * row_gap - (box.h - ROOM)
316
+ if over > 0 and n > 1:
317
+ cut = min(over, (row_gap - 12.0) * (n - 1))
318
+ row_gap -= cut / (n - 1)
319
+ over -= cut
320
+ if over > 0 and title:
321
+ cut = min(over, t_gap - 28.0)
322
+ t_gap -= cut
323
+ over -= cut
324
+ if over > 0:
325
+ pad_y -= min(over / 2.0, pad_y - 24.0)
326
+ rows_h = n * row_h + (n - 1) * row_gap
327
+ card_h = 2 * pad_y + title_cap + t_gap + rows_h
328
+ top = (box.h - card_h) / 2.0
329
+ first = top + pad_y + title_cap + t_gap
330
+ out.update(layout="inline", val_size=val_size, compact=compact, lab_size=lab_size, names=names,
331
+ bar_h=bar_h, axis_x=axis_x, bar_max=max(60.0, bar_max),
332
+ centers=[first + row_h / 2.0 + i * (row_h + row_gap) for i in range(n)],
333
+ rows=(first - row_gap / 2.0, first + rows_h + row_gap / 2.0))
334
+ pad_top = pad_y
335
+ out.update(card_box=(x0, top, card_w, card_h), title_base=top + pad_top + title_cap, title_x=x0 + pad,
336
+ title_anchor="ls", label_x=x0 + pad)
337
+ out["radius"] = min(44.0, out["card_box"][3] * 0.12)
338
+ return out
339
+
340
+
341
+ # ------------------------------------------------------------------ drawing
342
+
343
+ def _growth(params: dict, i: int, t: float) -> tuple[float, float]:
344
+ """(eased length with a small overshoot, count progress 0..1) of bar i at time t."""
345
+ start = params["delay"] + i * params["stagger"]
346
+ g = E.progress(t, start, params["duration"])
347
+ return E.out_back(g, OVERSHOOT), E.out_cubic(g)
348
+
349
+
350
+ def _land(params: dict, i: int, t: float) -> float:
351
+ return E.pulse(t, params["delay"] + i * params["stagger"] + params["duration"] - 0.05, LAND)
352
+
353
+
354
+ STATIC_AFTER = 0.5 # seconds: from here on the card, title, axis, tracks and names never change
355
+
356
+
357
+ def _draw_static(cv: E.Canvas, L: dict, names_in: float) -> None:
358
+ """Everything that does not move once the card is in: card, title, axis, tracks and names."""
359
+ card = L["card"]
360
+ outline = {} if card else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
361
+ name_kw = {} if card else {"stroke": 4.0, "stroke_color": "black"}
362
+ name_color = "muted" if card else "text"
363
+ if card:
364
+ cx0, cy0, cw, ch = L["card_box"]
365
+ cv.card(cx0, cy0, cw, ch, radius=L["radius"])
366
+ if L["title"]:
367
+ cv.text(L["title_x"], L["title_base"], L["title"], "text_bold", L["title_size"],
368
+ color="text", anchor=L["title_anchor"], **outline)
369
+ axis_color = "track" if card else (255, 255, 255, 120)
370
+ if L["mode"] == "colunas":
371
+ base = L["base"]
372
+ ax0, ax1 = L["axis"]
373
+ cv.line(ax0, base + 1.5, ax1, base + 1.5, width=3.0, color=axis_color)
374
+ if names_in > 0:
375
+ for i, lines in enumerate(L["wrapped"]):
376
+ y = base + 20.0 + L["lab_cap"]
377
+ for k, ln in enumerate(lines):
378
+ cv.text(L["cols"][i], y + k * L["pitch"], ln, "text_semibold", L["lab_size"],
379
+ color=name_color, opacity=names_in, anchor="ms", **name_kw)
380
+ return
381
+ ax = L["axis_x"]
382
+ bh = L["bar_h"]
383
+ stacked = L["layout"] == "stacked"
384
+ if not stacked:
385
+ y0, y1 = L["rows"]
386
+ cv.line(ax - 1.5, y0 + 6.0, ax - 1.5, y1 - 6.0, width=3.0, color=axis_color)
387
+ for i, name in enumerate(L["names"]):
388
+ cy = L["centers"][i]
389
+ if names_in > 0 and name:
390
+ if stacked:
391
+ cv.text(L["label_x"], L["lab_base"][i], name, "text_semibold", L["lab_size"],
392
+ color=name_color, opacity=names_in, anchor="ls", **name_kw)
393
+ else:
394
+ cv.text(L["label_x"], cy, name, "text_semibold", L["lab_size"],
395
+ color=name_color, opacity=names_in, anchor="lc", **name_kw)
396
+ if stacked: # the track shows where the bar runs
397
+ cv.pill(ax, cy - bh / 2.0, L["bar_max"], bh, fill="track" if card else (0, 0, 0, 110), opacity=names_in)
398
+
399
+
400
+ def _static_sprite(params_json: str, box: E.Box):
401
+ """The static part drawn once (card, title, axis, tracks and names)."""
402
+ return E.static_sprite(("grafico_barras", params_json), box,
403
+ lambda c: _draw_static(c, _layout(params_json, box), 1.0))
404
+
405
+
406
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
407
+ cv = E.Canvas(box)
408
+ t = frame_index / fps
409
+ key = json.dumps(params, sort_keys=True)
410
+ L = _layout(key, box)
411
+ bars = params["bars"]
412
+ cx0, cy0, cw, ch = L["card_box"]
413
+
414
+ t_in = t + 1.0 / fps
415
+ alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
416
+ scale = 0.92 + 0.08 * E.out_back(E.progress(t_in, 0.0, 0.5), 1.4)
417
+ rise = 30.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
418
+ rest = _rest_color(box.theme, L["card"])
419
+ outline = {} if L["card"] else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
420
+
421
+ with cv.group(opacity=alpha, scale=scale, dy=rise, pivot=(cx0 + cw / 2.0, cy0 + ch / 2.0)):
422
+ if t_in >= STATIC_AFTER: # the entrance is over: the group is the identity
423
+ cv.paste_sprite(_static_sprite(key, box))
424
+ else:
425
+ _draw_static(cv, L, E.eased(t, 0.06, 0.3))
426
+
427
+ if L["mode"] == "colunas":
428
+ base = L["base"]
429
+ base_px = cv.px(0.0, base)[1]
430
+ for i, b in enumerate(bars):
431
+ grow, count = _growth(params, i, t)
432
+ cx = L["cols"][i]
433
+ h = L["plot_h"] * L["fracs"][i] * grow
434
+ color = "accent" if L["hi"][i] else rest
435
+ land = _land(params, i, t) if L["hi"][i] else 0.0
436
+ r = L["col_radius"]
437
+ if h > 0.05:
438
+ def column(cx=cx, h=h, color=color, r=r) -> None:
439
+ m = cv.rect_mask(cx - L["col_w"] / 2.0, base - h, L["col_w"], h + r + 2.0, radius=r)
440
+ _fill_bar(cv, E.clip_mask(m, y1=base_px), color, L["card"])
441
+ if land > 0.02:
442
+ cv.glow(column, radius=22.0, opacity=0.6 * land)
443
+ else:
444
+ column()
445
+ if L["show_values"] and count > 0:
446
+ txt = _value_text(b["value"] * count, L["dec"], L["prefix"], L["suffix"], L["compact"], b["value"])
447
+ va = E.clamp01(count * 3.0)
448
+ with cv.group(scale=1.0 + 0.08 * land, pivot=(cx, base - h - 16.0)):
449
+ cv.text(cx, base - h - 16.0, txt, "sans_black", L["val_size"],
450
+ color=L["hi_text"] if L["hi"][i] else "text", opacity=va * (1.0 if L["hi"][i] else 0.86),
451
+ anchor="ms", tabular=True, **outline)
452
+ else:
453
+ ax = L["axis_x"]
454
+ stacked = L["layout"] == "stacked"
455
+ ax_px = cv.px(ax, 0.0)[0]
456
+ bh = L["bar_h"]
457
+ for i, b in enumerate(bars):
458
+ grow, count = _growth(params, i, t)
459
+ cy = L["centers"][i]
460
+ w = L["bar_max"] * L["fracs"][i] * grow
461
+ if stacked:
462
+ w = min(w, L["bar_max"]) # the track is the limit
463
+ color = "accent" if L["hi"][i] else rest
464
+ land = _land(params, i, t) if L["hi"][i] else 0.0
465
+ r = bh / 2.0
466
+ if w > 0.05:
467
+ def bar(cy=cy, w=w, color=color, r=r) -> None:
468
+ if stacked:
469
+ m = cv.rect_mask(ax, cy - bh / 2.0, max(bh, w), bh, radius=r)
470
+ else:
471
+ m = E.clip_mask(cv.rect_mask(ax - r - 2.0, cy - bh / 2.0, w + r + 2.0, bh, radius=r),
472
+ x0=ax_px)
473
+ _fill_bar(cv, m, color, L["card"])
474
+ if land > 0.02:
475
+ cv.glow(bar, radius=20.0, opacity=0.6 * land)
476
+ else:
477
+ bar()
478
+ if L["show_values"] and count > 0:
479
+ txt = _value_text(b["value"] * count, L["dec"], L["prefix"], L["suffix"], L["compact"], b["value"])
480
+ if stacked:
481
+ vx, vy, anchor = L["value_x"], L["lab_base"][i], "rs"
482
+ else:
483
+ vx, vy, anchor = ax + w + 16.0, cy, "lc"
484
+ with cv.group(scale=1.0 + 0.08 * land, pivot=(vx, vy)):
485
+ cv.text(vx, vy, txt, "sans_black", L["val_size"],
486
+ color=L["hi_text"] if L["hi"][i] else "text",
487
+ opacity=E.clamp01(count * 3.0) * (1.0 if L["hi"][i] else 0.86),
488
+ anchor=anchor, tabular=True, **outline)
489
+ return cv
490
+
491
+
492
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
493
+ cues = [{"at": 0.0, "name": "pop", "why": "entrada"}]
494
+ for i in range(len(params["bars"])):
495
+ cues.append({"at": params["delay"] + i * params["stagger"], "name": "tick", "volume": 0.8,
496
+ "why": f"barra {i + 1}"})
497
+ return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
498
+
499
+
500
+ def _end(params: dict) -> float:
501
+ return params["delay"] + (len(params["bars"]) - 1) * params["stagger"] + params["duration"]
502
+
503
+
504
+ def suggested_duration(params: dict) -> float:
505
+ """All bars grown, the glow, and about 1.8 s to read."""
506
+ return round(max(TEMPLATE["default_duration"], _end(params) + LAND + 1.8), 2)
507
+
508
+
509
+ def settle(params: dict, fps: int) -> float:
510
+ return _end(params) + LAND + 0.05