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,266 @@
1
+ """notificacao: a generic phone notification card that slides down from the top of the screen.
2
+
3
+ Title and text come from the person; the app icon is a generic glyph (speech bubble, bell, check, heart or a
4
+ letter) on the accent color, never the logo or the look of a real app. The card springs down from above the
5
+ screen, holds, and slides back up at the end (the template draws its own exit, so `exit` is "none").
6
+
7
+ A notification shows only what really happened in the person's material (a message, a sale, a pix): `notes_pt`
8
+ says so; the template cannot check it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from functools import lru_cache
15
+
16
+ from motion import engine as E
17
+ from motion.engine import MotionError
18
+
19
+ TEMPLATE = {
20
+ "id": "notificacao",
21
+ "label_pt": "Notificação de celular",
22
+ "description_pt": "Um cartão de notificação de celular que desce do alto da tela com um balanço leve, fica "
23
+ "parado para leitura e sobe de novo no fim. Ícone genérico, sem marca de app real.",
24
+ "use_when_pt": "Quando a fala cita uma mensagem, um lembrete, um aviso ou uma venda: pergunta de cliente, "
25
+ "aula liberada, lembrete de live, pedido de ajuda, venda que caiu.",
26
+ "notes_pt": "Mostre só o que aconteceu de verdade no material da pessoa: não invente venda, pix, valor, "
27
+ "mensagem nem depoimento. Também não imite nome, ícone ou cor de app real "
28
+ "(banco, WhatsApp, Hotmart, Kiwify): o ícone daqui é genérico de propósito.",
29
+ "category": "chamada",
30
+ "version": 1,
31
+ "params": {
32
+ "app_name": {"type": "text", "default": "Mensagens", "max_len": 24,
33
+ "label_pt": "Nome do app no topo do cartão (genérico, ex.: Mensagens, Lembretes, Agenda)"},
34
+ "title": {"type": "text", "default": "Nova mensagem", "max_len": 48, "required_text": True,
35
+ "label_pt": "Título em negrito (ex.: nome de quem mandou)"},
36
+ "text": {"type": "text", "default": "Oi! Vi seu vídeo e quero saber como funciona.", "max_len": 120,
37
+ "label_pt": "Texto da notificação (até 3 linhas; vazio: só o título)"},
38
+ "time_label": {"type": "text", "default": "agora", "max_len": 12,
39
+ "label_pt": "Hora no canto (ex.: agora, 2 min, 09:41)"},
40
+ "icon": {"type": "choice", "choices": ["mensagem", "sino", "check", "coracao", "letra"],
41
+ "default": "mensagem",
42
+ "label_pt": "Ícone genérico: balão de mensagem, sino, check, coração ou a primeira letra do app"},
43
+ "exit_mode": {"type": "choice", "choices": ["subir", "apagar"], "default": "subir",
44
+ "label_pt": "Saída no fim: sobe de volta para fora da tela ou apaga no lugar"},
45
+ "stack": {"type": "bool", "default": False,
46
+ "label_pt": "Mostrar outra notificação atrás (parece que chegaram várias)"},
47
+ },
48
+ "default_duration": 3.5,
49
+ "default_region": "faixa_topo",
50
+ "sfx": [{"at": 0.0, "name": "notification", "why": "notificação chegando"}],
51
+ "exit": "none",
52
+ "bleed": 260,
53
+ "movable": False,
54
+ "example": {"app_name": "Mensagens", "title": "Juliana",
55
+ "text": "Comecei ontem e já entendi o passo a passo. Posso tirar uma dúvida?"},
56
+ }
57
+
58
+ LAND = 0.80 # the spring is forced to rest exactly here
59
+ DAMPING, FREQ = 0.68, 1.9
60
+ EXIT_T = 0.36
61
+ EXIT_DIP = 0.18 # share of the exit spent on a small dip before the flick up
62
+ PAD = 32.0
63
+ ICON = 100.0
64
+ PEEK = 44.0 # design px of the card already on screen on frame 0
65
+
66
+
67
+ # ------------------------------------------------------------------ layout
68
+
69
+ def validate(params: dict) -> None:
70
+ """The title needs letters the font draws (refused when the item is planned)."""
71
+ if not E.clean_text(params["title"], "text_bold").strip():
72
+ raise MotionError("notificacao.params.title precisa de letras ou números que a fonte desenha")
73
+
74
+
75
+ def _truncate(text: str, font_key: str, size: float, max_width: float) -> str:
76
+ if E.measure(text, font_key, size).width <= max_width:
77
+ return text
78
+ words = text
79
+ while words and E.measure(words.rstrip() + "...", font_key, size).width > max_width:
80
+ words = words[:-1]
81
+ return words.rstrip(" ,.;:") + "..."
82
+
83
+
84
+ @lru_cache(maxsize=64)
85
+ def _layout(params_json: str, box: E.Box) -> dict:
86
+ p = json.loads(params_json)
87
+ W = box.w
88
+ content_x = PAD + ICON + 26.0
89
+ content_w = W - content_x - PAD
90
+ app = E.clean_text(p["app_name"], "text_semibold").strip()
91
+ time_label = E.clean_text(p["time_label"], "text_semibold").strip()
92
+ title = E.clean_text(p["title"], "text_bold").strip()
93
+ body = E.clean_text(p["text"], "text_semibold").strip()
94
+ if not title:
95
+ raise MotionError("notificacao.params.title precisa de letras ou números que a fonte desenha")
96
+
97
+ head_size = 30.0
98
+ head_cap = E.measure("H", "text_semibold", head_size).cap
99
+ time_w = E.measure(time_label, "text_semibold", head_size).width if time_label else 0.0
100
+ app_text = _truncate(app.upper(), "text_semibold", head_size, content_w - time_w - 24.0) if app else ""
101
+
102
+ title_size = E.fit_text(title, "text_bold", content_w, 44.0, 34.0)
103
+ title = _truncate(title, "text_bold", title_size, content_w)
104
+ title_cap = E.measure("H", "text_bold", title_size).cap
105
+
106
+ body_size = 40.0
107
+ lines: list[str] = []
108
+ if body:
109
+ lines = E.wrap_text(body, "text_semibold", body_size, content_w, 3)
110
+ while body_size > 32.0 and (len(lines) > 3 or any(
111
+ E.measure(ln, "text_semibold", body_size).width > content_w + 0.5 for ln in lines)):
112
+ body_size = max(32.0, body_size * 0.95)
113
+ lines = E.wrap_text(body, "text_semibold", body_size, content_w, 3)
114
+ lines = [_truncate(ln, "text_semibold", body_size, content_w) for ln in lines[:3]]
115
+ body_cap = E.measure("H", "text_semibold", body_size).cap
116
+ pitch = body_size * 1.26
117
+
118
+ head_base = PAD + head_cap + 2.0
119
+ title_base = head_base + 18.0 + title_cap
120
+ body_base = title_base + 14.0 + body_cap
121
+ bottom = (body_base + pitch * (len(lines) - 1) + body_size * 0.28) if lines else title_base + title_size * 0.28
122
+ card_h = max(ICON + 2 * PAD, bottom + PAD)
123
+ extra = (card_h - (bottom + PAD)) / 2.0 # a short text sits in the middle of the icon's height
124
+ head_base, title_base, body_base = head_base + extra, title_base + extra, body_base + extra
125
+ top = min(12.0, max(0.0, box.h - card_h))
126
+
127
+ # slide distance: from above the screen (only the bottom PEEK px show on frame 0) when the canvas reaches
128
+ # it, else a short slide with a fade
129
+ canvas_top = box.y - box.bleed
130
+ off_screen = canvas_top <= -20.0
131
+ if off_screen:
132
+ a0 = _arrive(1.0 / box.fps)
133
+ travel = max(150.0, (box.y + top + card_h - PEEK) / max(0.2, 1.0 - a0))
134
+ travel = min(travel, box.bleed + box.y + top - 10.0 + card_h)
135
+ else:
136
+ travel = 150.0
137
+ escuro = box.theme.dark
138
+ acc = box.theme.accent
139
+ return {
140
+ "W": W, "top": top, "card_h": card_h, "content_x": content_x, "content_w": content_w,
141
+ "app": app_text, "time": time_label, "time_w": time_w, "head_size": head_size, "head_base": head_base,
142
+ "title": title, "title_size": title_size, "title_base": title_base,
143
+ "lines": lines, "body_size": body_size, "body_base": body_base, "pitch": pitch,
144
+ "travel": travel, "off_screen": off_screen, "letter": (app[:1] or title[:1]).upper(),
145
+ "grad": (E.mix_color(acc, (255, 255, 255), 0.18), E.mix_color(acc, (0, 0, 0), 0.22)),
146
+ "fill": (38, 38, 42, 236) if escuro else (250, 250, 250, 244),
147
+ "stack_fill": (30, 30, 34, 200) if escuro else (236, 236, 236, 220),
148
+ }
149
+
150
+
151
+ # ------------------------------------------------------------------ icon glyphs (generic)
152
+
153
+ def _icon(cv: E.Canvas, L: dict, x: float, y: float, kind: str) -> None:
154
+ s = ICON
155
+ light, dark = L["grad"]
156
+ cv.rect(x, y, s, s, radius=s * 0.24, fill=E.Gradient(light, dark, (x, y), (x + s, y + s)),
157
+ shadow=E.Shadow(blur=10, dy=4, opacity=0.25))
158
+ cx, cy = x + s / 2.0, y + s / 2.0
159
+ ink = "on_accent"
160
+ if kind == "mensagem":
161
+ bw, bh = s * 0.58, s * 0.44
162
+ bx, by = cx - bw / 2.0, cy - bh / 2.0 - s * 0.04
163
+ cv.rect(bx, by, bw, bh, radius=bh * 0.42, fill=ink)
164
+ cv.polygon([(bx + bw * 0.22, by + bh - 2.0), (bx + bw * 0.46, by + bh - 2.0),
165
+ (bx + bw * 0.14, by + bh + s * 0.13)], fill=ink)
166
+ for k in (-1, 0, 1):
167
+ cv.circle(cx + k * s * 0.12, by + bh / 2.0, s * 0.036, fill=light)
168
+ elif kind == "sino":
169
+ cv.circle(cx, cy - s * 0.30, s * 0.05, fill=ink)
170
+ body = [(cx - s * 0.13, cy - s * 0.20), (cx + s * 0.13, cy - s * 0.20),
171
+ (cx + s * 0.22, cy + s * 0.14), (cx - s * 0.22, cy + s * 0.14)]
172
+ cv.polygon(body, fill=ink, radius=s * 0.04)
173
+ cv.circle(cx, cy - s * 0.13, s * 0.14, fill=ink)
174
+ cv.pill(cx - s * 0.29, cy + s * 0.10, s * 0.58, s * 0.09, fill=ink)
175
+ cv.circle(cx, cy + s * 0.25, s * 0.065, fill=ink)
176
+ elif kind == "check":
177
+ cv.check(cx, cy + s * 0.02, s * 0.52, width=s * 0.11, color=ink)
178
+ elif kind == "coracao":
179
+ cv.polygon(E.heart_points(cx, cy + s * 0.02, s * 0.56, n=48, closed=False), fill=ink)
180
+ else:
181
+ cv.text(cx, cy, L["letter"], "sans_black", s * 0.50, color=ink, anchor="mc")
182
+
183
+
184
+ # ------------------------------------------------------------------ motion
185
+
186
+ def _arrive(t_in: float) -> float:
187
+ """0 -> 1 with a light overshoot; exactly 1 from LAND on (no endless sub-pixel wobble)."""
188
+ if t_in >= LAND:
189
+ return 1.0
190
+ v = E.spring(t_in, DAMPING, FREQ)
191
+ blend = E.eased(t_in, LAND - 0.25, 0.25, "in_out_sine")
192
+ return v + (1.0 - v) * blend
193
+
194
+
195
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
196
+ key = json.dumps(params, sort_keys=True)
197
+ L = _layout(key, box)
198
+ cv = E.Canvas(box)
199
+ t = frame_index / fps
200
+ t_in = t + 1.0 / fps
201
+ end = box.frames / fps
202
+
203
+ a = _arrive(t_in)
204
+ dy = -L["travel"] * (1.0 - a)
205
+ opacity = E.eased(t_in, 0.0, 0.10 if L["off_screen"] else 0.25, "out_quad")
206
+ scale = 0.96 + 0.04 * a
207
+
208
+ # exit, drawn by the template: up and out, or a fade in place
209
+ t_exit = end - EXIT_T
210
+ if t >= t_exit - 1e-9:
211
+ # the last frame is fully gone (+ 1 frame so the exit ends on the last frame)
212
+ p = E.progress(t + 1.0 / fps, t_exit, EXIT_T)
213
+ if params["exit_mode"] == "subir":
214
+ dy += 12.0 * E.pulse(p, 0.0, EXIT_DIP * 2.0)
215
+ dy -= (L["travel"] + PEEK + 60.0) * E.in_quad(E.remap(p, EXIT_DIP, 1.0))
216
+ if not L["off_screen"]:
217
+ opacity *= 1.0 - E.eased(p, 0.3, 0.7, "in_quad")
218
+ else:
219
+ opacity *= 1.0 - E.out_quad(p)
220
+ scale *= 1.0 - 0.04 * p
221
+ if p >= 1.0:
222
+ return cv
223
+
224
+ if t_in >= LAND and t < t_exit - 1e-9:
225
+ # resting: every frame is the same picture (settle() cannot be used with a template-drawn exit)
226
+ cv.image.alpha_composite(_rest_image(key, box))
227
+ return cv
228
+ _draw_card(cv, L, params, dy, scale, opacity)
229
+ return cv
230
+
231
+
232
+ @lru_cache(maxsize=8)
233
+ def _rest_image(params_json: str, box: E.Box):
234
+ cv = E.Canvas(box)
235
+ _draw_card(cv, _layout(params_json, box), json.loads(params_json), 0.0, 1.0, 1.0)
236
+ return cv.image
237
+
238
+
239
+ def _draw_card(cv: E.Canvas, L: dict, params: dict, dy: float, scale: float, opacity: float) -> None:
240
+ x, y, w, h = 0.0, L["top"], L["W"], L["card_h"]
241
+ with cv.group(dy=dy, scale=scale, pivot=(w / 2.0, y), opacity=opacity):
242
+ if params["stack"]:
243
+ cv.rect(w * 0.05, y + 22.0, w * 0.90, h, radius=40.0, fill=L["stack_fill"],
244
+ shadow=E.Shadow(blur=20, dy=10, opacity=0.25), stroke="card_border", stroke_width=2.0)
245
+ cv.rect(x, y, w, h, radius=44.0, fill=L["fill"], shadow=E.Shadow(blur=34, dy=16, opacity=0.38),
246
+ stroke="card_border", stroke_width=2.0)
247
+ _icon(cv, L, PAD, y + PAD, params["icon"])
248
+ cx = L["content_x"]
249
+ if L["app"]:
250
+ cv.text(cx, y + L["head_base"], L["app"], "text_semibold", L["head_size"], color="muted", tracking=1.5)
251
+ if L["time"]:
252
+ cv.text(w - PAD, y + L["head_base"], L["time"], "text_semibold", L["head_size"], color="muted",
253
+ anchor="rs")
254
+ cv.text(cx, y + L["title_base"], L["title"], "text_bold", L["title_size"], color="text")
255
+ for i, line in enumerate(L["lines"]):
256
+ cv.text(cx, y + L["body_base"] + i * L["pitch"], line, "text_semibold", L["body_size"], color="text",
257
+ opacity=0.9)
258
+
259
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
260
+ return [{"frame": 0, "name": "notification", "volume": 1.0, "why": "notificação chegando"}]
261
+
262
+
263
+ def suggested_duration(params: dict) -> float:
264
+ """Arrival, reading time by the size of the text, and the exit."""
265
+ words = len((params["title"] + " " + params["text"]).split())
266
+ return round(min(8.0, max(TEMPLATE["default_duration"], LAND + 0.8 + words * 0.22 + EXIT_T)), 2)