primocode 10.15.2 → 10.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +74 -9
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +38 -8
  198. package/lib/app-chat.js +97 -0
  199. package/lib/catalogo.js +7 -1
  200. package/lib/chamada-torta.js +81 -0
  201. package/lib/editor-pro.js +786 -0
  202. package/lib/entrega.js +58 -0
  203. package/lib/estudio/conferencia-visual.js +76 -0
  204. package/lib/estudio/diretor.js +6 -1
  205. package/lib/estudio/exportar.js +3 -1
  206. package/lib/estudio/gosto/tela.html +1349 -0
  207. package/lib/estudio/gosto.js +305 -0
  208. package/lib/estudio/index.js +12 -1
  209. package/lib/estudio/kit-ae/base.jsx +119 -13
  210. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  211. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  212. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  213. package/lib/estudio/maquina-diretor.js +70 -12
  214. package/lib/estudio/maquina-foto.js +177 -0
  215. package/lib/estudio/maquina-guarda.js +207 -0
  216. package/lib/estudio/maquina-layout.js +22 -1
  217. package/lib/estudio/maquina.js +64 -11
  218. package/lib/estudio/referencias.js +1 -1
  219. package/lib/estudio/visao/visao.swift +97 -0
  220. package/lib/estudio/visao.js +60 -0
  221. package/lib/headless.js +15 -3
  222. package/lib/local/cerebro.json +285 -7
  223. package/lib/local/motor.js +5 -2
  224. package/lib/memoria-usuario.js +164 -0
  225. package/lib/nuvem.js +33 -2
  226. package/lib/passagem.js +50 -0
  227. package/lib/perigo.js +19 -11
  228. package/lib/protecao.js +134 -0
  229. package/lib/repeticao.js +1 -1
  230. package/lib/sandbox.js +57 -9
  231. package/lib/titulo-conversa.js +46 -0
  232. package/lib/tools.js +23 -4
  233. package/lib/ui.js +2 -2
  234. package/package.json +3 -2
  235. package/skills/editar-meu-video/SKILL.md +6 -2
  236. package/skills/editar-video/SKILL.md +113 -0
  237. package/studio/pessoa.py +52 -0
  238. package/voz/primo_voz/__main__.py +55 -40
  239. package/voz/primo_voz/app.py +500 -4
  240. package/voz/primo_voz/chat.html +1773 -0
  241. package/voz/primo_voz/conversa.py +0 -26
  242. package/voz/primo_voz/hud.html +8 -2
  243. package/voz/primo_voz/hud.py +129 -5
  244. package/voz/primo_voz/identidade.py +1 -1
  245. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,462 @@
1
+ """grafico_linha: a line that draws itself from left to right through the values, with a soft area under it,
2
+ a dot that pops on the last point, and the value at the tip counting in the header (SPEC Contract 12).
3
+
4
+ The curve is a monotone cubic (Fritsch-Carlson): smooth, and it never goes above or below the data between
5
+ two points. The area under the line is filled column by column (faster than a polygon with many sides).
6
+ `_area_mask` is the area under a curve up to a baseline. The card, grid and names are drawn once
7
+ (`E.static_sprite`) and pasted after the entrance.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import math
14
+ from functools import lru_cache
15
+
16
+ import numpy as np
17
+
18
+ from motion import engine as E
19
+
20
+ TEMPLATE = {
21
+ "id": "grafico_linha",
22
+ "label_pt": "Gráfico de linha",
23
+ "description_pt": "Uma linha que se desenha da esquerda para a direita passando pelos valores, com uma área "
24
+ "suave embaixo e uma bolinha que pulsa no último ponto. O número no topo acompanha a ponta "
25
+ "da linha até o valor final.",
26
+ "use_when_pt": "Para mostrar evolução no tempo: crescimento de seguidores, alunos, visitas, faturamento, "
27
+ "semanas de prática. Só com números que existem no material da pessoa, sem inventar.",
28
+ "category": "números",
29
+ "version": 1,
30
+ "params": {
31
+ "values": {"type": "list", "item": {"type": "number", "min": -1e13, "max": 1e13},
32
+ "min_items": 2, "max_items": 24, "default": [12, 15, 14, 21, 26, 24, 33, 41],
33
+ "label_pt": "Valores em ordem, de 2 a 24 (use ponto para decimais)"},
34
+ "labels": {"type": "list", "item": {"type": "text", "max_len": 10}, "max_items": 24, "default": [],
35
+ "label_pt": "Nomes embaixo do gráfico: vazio, 2 (primeiro e último) ou um por valor"},
36
+ "title": {"type": "text", "default": "", "max_len": 34, "label_pt": "Título em cima (opcional)"},
37
+ "prefix": {"type": "text", "default": "", "max_len": 4, "label_pt": "Antes do número, ex.: R$ ou +"},
38
+ "suffix": {"type": "text", "default": "", "max_len": 10, "label_pt": "Depois do número, ex.: %, mil ou alunos"},
39
+ "decimals": {"type": "integer", "default": None, "nullable": True, "min": 0, "max": 2,
40
+ "label_pt": "Casas decimais (vazio: automático)"},
41
+ "label": {"type": "text", "default": "", "max_len": 24, "label_pt": "Texto curto ao lado do número, ex.: em 8 semanas"},
42
+ "badge": {"type": "text", "default": "", "max_len": 12,
43
+ "label_pt": "Selo que aparece no fim, ex.: +240% (opcional)"},
44
+ "zero": {"type": "bool", "default": True,
45
+ "label_pt": "O gráfico começa no zero (desligue para mostrar variações pequenas)"},
46
+ "dots": {"type": "bool", "default": True, "label_pt": "Bolinhas em cada ponto (até 12 valores)"},
47
+ "area": {"type": "bool", "default": True, "label_pt": "Área colorida embaixo da linha"},
48
+ "duration": {"type": "number", "default": 1.6, "min": 0.3, "max": 8,
49
+ "label_pt": "Segundos que a linha leva para se desenhar"},
50
+ "delay": {"type": "number", "default": 0.2, "min": 0, "max": 5,
51
+ "label_pt": "Segundos antes de a linha começar"},
52
+ "card": {"type": "bool", "default": True, "label_pt": "Cartão atrás do gráfico"},
53
+ },
54
+ "default_duration": 4.0,
55
+ "default_region": "baixo",
56
+ "sfx": [{"at": 0.0, "name": "pop"}, {"at": 0.2, "name": "count", "volume": 1.3}, {"at": 1.8, "name": "pop"}],
57
+ "example": {"title": "Seguidores do perfil", "values": [800, 950, 1400, 1300, 2600, 3900, 5200],
58
+ "labels": ["Jan", "Jul"], "label": "em 6 meses", "badge": "+550%"},
59
+ }
60
+
61
+ EASE = "in_out_cubic"
62
+ DOT_POP = 0.3
63
+ HALO = 0.7
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
+
67
+
68
+ # ------------------------------------------------------------------ helpers
69
+
70
+ def _decimals(p: dict) -> int:
71
+ if p["decimals"] is not None:
72
+ return int(p["decimals"])
73
+ d = max(E.auto_decimals(v) for v in p["values"])
74
+ money = E.looks_like_money(p["prefix"], p["suffix"])
75
+ return 2 if money and d else d
76
+
77
+
78
+ def _affix_gap(affix: str, size: float) -> float:
79
+ """Space between the number and a prefix/suffix: none for one symbol (%, x, +), a word space otherwise."""
80
+ return size * (0.02 if len(affix) <= 1 else 0.14) if affix else 0.0
81
+
82
+
83
+ def _slopes(ys: list[float]) -> list[float]:
84
+ """Fritsch-Carlson tangents for x spacing 1: the curve keeps the shape of the data (no overshoot)."""
85
+ n = len(ys)
86
+ d = [ys[i + 1] - ys[i] for i in range(n - 1)]
87
+ m = [0.0] * n
88
+ m[0], m[-1] = d[0], d[-1]
89
+ for i in range(1, n - 1):
90
+ m[i] = 0.0 if d[i - 1] * d[i] <= 0 else (d[i - 1] + d[i]) / 2.0
91
+ for i in range(n - 1):
92
+ if d[i] == 0:
93
+ m[i] = m[i + 1] = 0.0
94
+ continue
95
+ a, b = m[i] / d[i], m[i + 1] / d[i]
96
+ if a < 0:
97
+ m[i], a = 0.0, 0.0
98
+ if b < 0:
99
+ m[i + 1], b = 0.0, 0.0
100
+ s = a * a + b * b
101
+ if s > 9.0:
102
+ tau = 3.0 / math.sqrt(s)
103
+ m[i], m[i + 1] = tau * a * d[i], tau * b * d[i]
104
+ return m
105
+
106
+
107
+ def _hermite(ys: list[float], m: list[float], u: float) -> float:
108
+ """Value of the curve at u (0 .. n-1, in points)."""
109
+ n = len(ys)
110
+ u = min(max(u, 0.0), n - 1.0)
111
+ i = min(int(u), n - 2)
112
+ t = u - i
113
+ t2, t3 = t * t, t * t * t
114
+ return ((2 * t3 - 3 * t2 + 1) * ys[i] + (t3 - 2 * t2 + t) * m[i] + (-2 * t3 + 3 * t2) * ys[i + 1]
115
+ + (t3 - t2) * m[i + 1])
116
+
117
+
118
+ def _area_mask(cv: E.Canvas, pts: list[tuple[float, float]], base: float):
119
+ """Coverage of the region between a curve (x increasing, design px) and the horizontal line y = base."""
120
+ if len(pts) < 2:
121
+ return None
122
+ P = [cv.px(x, y) for x, y in pts]
123
+ X = np.array([p[0] for p in P], np.float64)
124
+ Y = np.array([p[1] for p in P], np.float64)
125
+ B = cv.px(0.0, base)[1]
126
+ x0, x1 = float(X[0]), float(X[-1])
127
+ if x1 - x0 < 0.05:
128
+ return None
129
+ ix0, ix1 = max(0, int(math.floor(x0))), min(cv.W, int(math.ceil(x1)))
130
+ iy0, iy1 = max(0, int(math.floor(Y.min()))), min(cv.H, int(math.ceil(B)))
131
+ if ix1 <= ix0 or iy1 <= iy0:
132
+ return None
133
+ cols = np.arange(ix0, ix1, dtype=np.float64)
134
+ ycurve = np.interp(cols + 0.5, X, Y)
135
+ rows = np.arange(iy0, iy1, dtype=np.float64)
136
+ a = np.clip(rows[:, None] + 0.5 - ycurve[None, :] + 0.5, 0.0, 1.0) # under the curve
137
+ a *= np.clip(B - rows, 0.0, 1.0)[:, None] # above the base line
138
+ a *= (np.clip(cols + 1.0 - x0, 0.0, 1.0) * np.clip(x1 - cols, 0.0, 1.0))[None, :]
139
+ return E.Mask(a.astype(np.float32), ix0, iy0)
140
+
141
+
142
+ def _time_at(progress: float, delay: float, duration: float) -> float:
143
+ """Seconds when the eased line progress reaches `progress` (bisection, deterministic)."""
144
+ lo, hi = 0.0, 1.0
145
+ for _ in range(40):
146
+ mid = (lo + hi) / 2.0
147
+ if E.ease(EASE, mid) < progress:
148
+ lo = mid
149
+ else:
150
+ hi = mid
151
+ return delay + duration * hi
152
+
153
+
154
+ # ------------------------------------------------------------------ layout (once per item)
155
+
156
+ @lru_cache(maxsize=64)
157
+ def _layout(params_json: str, box: E.Box) -> dict:
158
+ p = json.loads(params_json)
159
+ vals = [float(v) for v in p["values"]]
160
+ n = len(vals)
161
+ labels = [s.strip() for s in p["labels"]]
162
+ if labels and len(labels) not in (2, n):
163
+ raise E.MotionError(f"grafico_linha.params.labels tem {len(labels)} nomes e values tem {n} números: "
164
+ f"use 2 nomes (primeiro e último), um nome por número, ou deixe vazio")
165
+ dec = _decimals(p)
166
+ prefix, suffix = p["prefix"].strip(), p["suffix"].strip()
167
+ card = p["card"]
168
+ theme = box.theme
169
+ pad = 46.0 if card else 12.0
170
+ card_w = box.w
171
+ inner = card_w - 2 * pad
172
+ x_card = (box.w - card_w) / 2.0
173
+
174
+ # header: title / big value + label ... badge
175
+ title = p["title"].strip()
176
+ t_size = E.fit_text(title, "text_bold", inner * FIT, 38.0, 28.0) if title else 0.0
177
+ t_cap = E.measure("H", "text_bold", t_size).cap if title else 0.0
178
+ t_gap = 26.0 if title else 0.0
179
+ widest = max((E.format_number(v, dec) for v in (min(vals), max(vals), vals[0], vals[-1])),
180
+ key=lambda s: E.measure(s, "sans_black", 100, tabular=True).width)
181
+ pre, suf = prefix, suffix
182
+ badge = p["badge"].strip()
183
+ b_size = 36.0
184
+ b_w = (E.measure(badge, "sans_black", b_size).width + 2 * 22.0) if badge else 0.0
185
+ b_h = 64.0
186
+ label = p["label"].strip()
187
+
188
+ def value_w(size: float) -> float:
189
+ aff = size * 0.52
190
+ return (E.measure(pre, "sans_black", aff).width + _affix_gap(pre, size)
191
+ + E.measure(widest, "sans_black", size, tabular=True).width
192
+ + _affix_gap(suf, size) + E.measure(suf, "sans_black", aff).width)
193
+
194
+ v_size = 92.0
195
+ room = inner - (b_w + 24.0 if badge else 0.0)
196
+ if value_w(v_size) > room * 0.72:
197
+ v_size = max(48.0, v_size * room * 0.72 / value_w(v_size))
198
+ v_cap = E.measure("0", "sans_black", v_size).cap
199
+ l_size = 36.0
200
+ label_below = False
201
+ if label:
202
+ left = room - value_w(v_size) - 22.0
203
+ l_size = E.fit_text(label, "text_semibold", max(10.0, left * FIT), 36.0, 10.0)
204
+ if l_size < 28.0:
205
+ label_below = True
206
+ l_size = E.fit_text(label, "text_semibold", room * FIT, 34.0, 26.0)
207
+ l_cap = E.measure("H", "text_semibold", l_size).cap if label else 0.0
208
+ head_h = t_cap + t_gap + v_cap + ((18.0 + l_cap) if label_below else 0.0)
209
+
210
+ # x labels
211
+ xl_size = 32.0
212
+ xl_cap = E.measure("H", "text_semibold", xl_size).cap if labels else 0.0
213
+ xl_block = (20.0 + xl_cap) if labels else 0.0
214
+
215
+ plot_gap = 40.0
216
+ fixed = 2 * pad + head_h + plot_gap + xl_block + 16.0
217
+ plot_h = max(110.0, min(360.0, box.h - ROOM - fixed))
218
+ card_h = fixed + plot_h
219
+ top = (box.h - card_h) / 2.0
220
+ plot_top = top + pad + head_h + plot_gap + 16.0 # 16: room for the end dot above the highest point
221
+ base = plot_top + plot_h
222
+ x0 = x_card + pad + 14.0
223
+ x1 = x_card + card_w - pad - 18.0
224
+
225
+ lo, hi = min(vals), max(vals)
226
+ if p["zero"]:
227
+ lo = min(0.0, lo)
228
+ hi = max(0.0, hi)
229
+ else:
230
+ span = hi - lo
231
+ lo -= span * 0.12 if span > 0 else 1.0
232
+ if hi - lo <= 1e-12:
233
+ lo, hi = lo - 1.0, hi + 1.0
234
+
235
+ def ymap(v: float) -> float:
236
+ return base - (v - lo) / (hi - lo) * plot_h
237
+
238
+ slopes = _slopes(vals)
239
+ xs = [x0 + (x1 - x0) * i / (n - 1) for i in range(n)]
240
+ per = max(3, min(48, int((x1 - x0) / (n - 1) / 9.0))) # a curve sample about every 9 px
241
+ dense = []
242
+ for k in range((n - 1) * per + 1):
243
+ u = k / per
244
+ dense.append((x0 + (x1 - x0) * u / (n - 1), ymap(_hermite(vals, slopes, u))))
245
+ dots = p["dots"] and n <= 12
246
+ dot_times = [_time_at(i / (n - 1), p["delay"], p["duration"]) for i in range(n)]
247
+
248
+ # x labels: 2 = first and last; n = every point (thinned when they would touch)
249
+ xlabels = []
250
+ if labels:
251
+ idx = [0, n - 1] if len(labels) == 2 else list(range(n))
252
+ names = labels if len(labels) == n else [labels[0]] + [""] * (n - 2) + [labels[1]]
253
+ if len(idx) > 2:
254
+ widest_l = max(E.measure(s, "text_semibold", xl_size).width for s in names) + 18.0
255
+ step = max(1, math.ceil(widest_l / ((x1 - x0) / (n - 1))))
256
+ idx = [i for i in idx if i % step == 0 or i == n - 1]
257
+ if len(idx) >= 2 and idx[-1] - idx[-2] < step:
258
+ idx.pop(-2)
259
+ for i in idx:
260
+ if not names[i]:
261
+ continue
262
+ anchor = "ls" if i == 0 else "rs" if i == n - 1 else "ms"
263
+ x = x0 - 14.0 if i == 0 else x1 + 18.0 if i == n - 1 else xs[i]
264
+ xlabels.append((x, names[i], anchor))
265
+
266
+ accent_ok = not card or E.contrast(theme.accent, theme.card) >= 2.4
267
+ head_base = top + pad + t_cap + t_gap + v_cap
268
+ return {
269
+ "n": n, "vals": vals, "slopes": slopes, "dec": dec, "pre": pre, "suf": suf, "card": card,
270
+ "card_box": (x_card, top, card_w, card_h), "radius": min(44.0, card_h * 0.12),
271
+ "title": title, "t_size": t_size, "title_base": top + pad + t_cap, "x_left": x_card + pad,
272
+ "v_size": v_size, "value_w": value_w(v_size), "head_base": head_base, "value_color": "accent" if accent_ok else "text",
273
+ "label": label, "l_size": l_size, "label_below": label_below,
274
+ "label_base": head_base + 18.0 + l_cap if label_below else head_base,
275
+ "badge": badge, "b_size": b_size, "b_w": b_w, "b_h": b_h,
276
+ "badge_box": (x_card + card_w - pad - b_w, head_base - v_cap / 2.0 - b_h / 2.0, b_w, b_h),
277
+ "plot_top": plot_top, "base": base, "plot_h": plot_h, "lo": lo, "hi": hi, "x0": x0, "x1": x1, "xs": xs,
278
+ "ys": [ymap(v) for v in vals], "dense": dense, "dots": dots, "dot_times": dot_times,
279
+ "xlabels": xlabels, "xl_size": xl_size, "xl_base": base + 20.0 + xl_cap,
280
+ "grid": [plot_top + plot_h * k / 3.0 for k in range(4)], "zero_line": ymap(0.0) if lo < 0 < hi else None,
281
+ }
282
+
283
+
284
+ # ------------------------------------------------------------------ drawing
285
+
286
+ def _t_end(params: dict) -> float:
287
+ return params["delay"] + params["duration"]
288
+
289
+
290
+ STATIC_AFTER = 0.5 # seconds: from here on the card, grid, names, title and label never change
291
+
292
+
293
+ def _draw_static(cv: E.Canvas, L: dict, ui_in: float) -> None:
294
+ """Everything that does not move once the card is in."""
295
+ card = L["card"]
296
+ stroke = {} if card else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
297
+ soft = {} if card else {"stroke": 4.0, "stroke_color": "black"}
298
+ x0, x1 = L["x0"], L["x1"]
299
+ if card:
300
+ cx0, cy0, cw, ch = L["card_box"]
301
+ cv.card(cx0, cy0, cw, ch, radius=L["radius"])
302
+ grid_c = (255, 255, 255, 26) if (cv.theme.dark or not card) else (0, 0, 0, 20)
303
+ strong_c = "track" if card else (255, 255, 255, 110)
304
+ for k, gy in enumerate(L["grid"]):
305
+ strong = k == len(L["grid"]) - 1
306
+ cv.line(x0 - 14.0, gy, x1 + 18.0, gy, width=2.5 if strong else 2.0, color=strong_c if strong else grid_c,
307
+ opacity=ui_in)
308
+ if L["zero_line"] is not None:
309
+ cv.line(x0 - 14.0, L["zero_line"], x1 + 18.0, L["zero_line"], width=2.5, color=strong_c, opacity=ui_in)
310
+ for x, name, anchor in L["xlabels"]:
311
+ cv.text(x, L["xl_base"], name, "text_semibold", L["xl_size"], color="muted" if card else "text",
312
+ opacity=ui_in, anchor=anchor, **soft)
313
+ if L["title"]:
314
+ cv.text(L["x_left"], L["title_base"], L["title"], "text_bold", L["t_size"], color="text", **stroke)
315
+ if L["label"]:
316
+ lx = L["x_left"] if L["label_below"] else L["x_left"] + L["value_w"] + 22.0
317
+ cv.text(lx, L["label_base"], L["label"], "text_semibold", L["l_size"],
318
+ color="muted" if card else "text", opacity=ui_in, **soft)
319
+
320
+
321
+ def _static_sprite(params_json: str, box: E.Box):
322
+ """The static part drawn once (card, grid, names, title and label)."""
323
+ return E.static_sprite(("grafico_linha", params_json), box,
324
+ lambda c: _draw_static(c, _layout(params_json, box), 1.0))
325
+
326
+
327
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
328
+ cv = E.Canvas(box)
329
+ t = frame_index / fps
330
+ key = json.dumps(params, sort_keys=True)
331
+ L = _layout(key, box)
332
+ cx0, cy0, cw, ch = L["card_box"]
333
+ theme = box.theme
334
+ t_end = _t_end(params)
335
+
336
+ t_in = t + 1.0 / fps
337
+ alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
338
+ scale = 0.92 + 0.08 * E.out_back(E.progress(t_in, 0.0, 0.5), 1.4)
339
+ rise = 30.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
340
+ card = L["card"]
341
+ stroke = {} if card else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
342
+ ui_in = E.eased(t, 0.05, 0.3)
343
+ p = E.eased(t, params["delay"], params["duration"], EASE)
344
+ x0, x1 = L["x0"], L["x1"]
345
+ tip_x = x0 + (x1 - x0) * p
346
+ u_tip = p * (L["n"] - 1)
347
+ if p >= 1.0:
348
+ tip_v = L["vals"][-1]
349
+ elif p <= 0.0:
350
+ tip_v = L["vals"][0]
351
+ else:
352
+ tip_v = _hermite(L["vals"], L["slopes"], u_tip)
353
+ base = L["base"]
354
+
355
+ with cv.group(opacity=alpha, scale=scale, dy=rise, pivot=(cx0 + cw / 2.0, cy0 + ch / 2.0)):
356
+ if t_in >= STATIC_AFTER: # the entrance is over: the group is the identity
357
+ cv.paste_sprite(_static_sprite(key, box))
358
+ else:
359
+ _draw_static(cv, L, ui_in)
360
+
361
+ # header (the title and the label are in the static part)
362
+ size = L["v_size"]
363
+ aff = size * 0.52
364
+ num = E.format_number(tip_v, L["dec"])
365
+ x = L["x_left"]
366
+ hb = L["head_base"]
367
+ if L["pre"]:
368
+ x = cv.text(x, hb, L["pre"], "sans_black", aff, color="text", opacity=0.92, **stroke)[2]
369
+ x += _affix_gap(L["pre"], size)
370
+ x = cv.text(x, hb, num, "sans_black", size, color=L["value_color"], tabular=True, **stroke)[2]
371
+ if L["suf"]:
372
+ x += _affix_gap(L["suf"], size)
373
+ cv.text(x, hb, L["suf"], "sans_black", aff, color="text", opacity=0.92, **stroke)
374
+
375
+ # area and line
376
+ if p > 0:
377
+ if p >= 1.0:
378
+ pts = list(L["dense"])
379
+ tip = pts[-1]
380
+ else:
381
+ pts = [q for q in L["dense"] if q[0] < tip_x]
382
+ tip = (tip_x, _y_of(L, tip_v))
383
+ pts.append(tip)
384
+ if len(pts) == 1:
385
+ pts = [pts[0], pts[0]]
386
+ if params["area"]:
387
+ acc = theme.accent
388
+ grad = E.Gradient((acc[0], acc[1], acc[2], 105 if card else 90), (acc[0], acc[1], acc[2], 0),
389
+ start=(0.0, L["plot_top"]), end=(0.0, base))
390
+ cv.fill_mask(_area_mask(cv, pts, base), grad)
391
+ # on the card the area already separates the line; on the video a shadow does
392
+ cv.polyline(pts, width=9.0, color="accent",
393
+ shadow=None if card else E.Shadow(blur=10.0, dy=5.0, opacity=0.55))
394
+
395
+ # point markers
396
+ if L["dots"]:
397
+ for i in range(L["n"] - 1):
398
+ q = E.progress(t, L["dot_times"][i], DOT_POP)
399
+ if q <= 0:
400
+ continue
401
+ s = E.out_back(q, 2.2)
402
+ cv.circle(L["xs"][i], L["ys"][i], 11.0 * s, fill="accent")
403
+ cv.circle(L["xs"][i], L["ys"][i], 5.5 * s, fill="card" if card else (20, 20, 22, 255))
404
+
405
+ if p < 1.0:
406
+ cv.circle(tip[0], tip[1], 11.0, fill="accent")
407
+ cv.circle(tip[0], tip[1], 5.0, fill="white")
408
+
409
+ # end: dot, halo, badge
410
+ q_end = E.progress(t, t_end - 0.02, 0.35)
411
+ if q_end > 0:
412
+ ex, ey = L["xs"][-1], L["ys"][-1]
413
+ h = E.progress(t, t_end, HALO)
414
+ if 0 < h < 1:
415
+ cv.ring(ex, ey, 16.0 + 34.0 * E.out_cubic(h), 5.0 * (1.0 - h) + 1.0, color="accent",
416
+ opacity=0.8 * (1.0 - h))
417
+ s = E.out_back(q_end, 2.4)
418
+
419
+ def end_dot() -> None:
420
+ cv.circle(ex, ey, 17.0 * s, fill="accent")
421
+ cv.circle(ex, ey, 7.0 * s, fill="white")
422
+
423
+ glow = E.pulse(t, t_end - 0.02, 0.5)
424
+ if glow > 0.02:
425
+ cv.glow(end_dot, radius=18.0, opacity=0.7 * glow)
426
+ else:
427
+ end_dot()
428
+
429
+ if L["badge"]:
430
+ qb = E.progress(t, t_end + 0.1, 0.35)
431
+ if qb > 0:
432
+ bx, by, bw, bh = L["badge_box"]
433
+ with cv.group(scale=0.4 + 0.6 * E.out_back(qb, 2.0), opacity=E.clamp01(qb * 3.0),
434
+ pivot=(bx + bw / 2.0, by + bh / 2.0)):
435
+ cv.pill(bx, by, bw, bh, fill="accent", shadow=E.Shadow(blur=14.0, dy=6.0, opacity=0.35))
436
+ cv.text(bx + bw / 2.0, by + bh / 2.0, L["badge"], "sans_black", L["b_size"],
437
+ color="on_accent", anchor="mc")
438
+ return cv
439
+
440
+
441
+ def _y_of(L: dict, v: float) -> float:
442
+ """Design y of a value on the plot scale."""
443
+ return L["base"] - (v - L["lo"]) / (L["hi"] - L["lo"]) * L["plot_h"]
444
+
445
+
446
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
447
+ start, dur = params["delay"], params["duration"]
448
+ cues = [{"at": 0.0, "name": "pop", "why": "entrada"}]
449
+ if params["values"][0] != params["values"][-1]:
450
+ for k in range(max(1, int(dur / 0.6 + 0.25))):
451
+ cues.append({"at": start + k * 0.6, "name": "count", "volume": 1.3, "why": "número subindo"})
452
+ cues.append({"at": start + dur, "name": "pop", "volume": 0.9, "why": "ponto final"})
453
+ return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
454
+
455
+
456
+ def suggested_duration(params: dict) -> float:
457
+ """The line, the dot and badge, and about 1.8 s to read."""
458
+ return round(max(TEMPLATE["default_duration"], _t_end(params) + HALO + 1.8), 2)
459
+
460
+
461
+ def settle(params: dict, fps: int) -> float:
462
+ return _t_end(params) + HALO + 0.05