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.
- package/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +28 -8
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +36 -8
- package/lib/catalogo.js +4 -0
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto.js +45 -4
- package/lib/estudio/index.js +8 -5
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +61 -7
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +60 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/local/cerebro.json +192 -6
- package/lib/local/motor.js +5 -2
- package/lib/mcp.js +105 -7
- package/lib/nuvem.js +11 -1
- package/lib/outra-pasta.js +1 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +29 -12
- package/lib/protecao.js +134 -0
- package/lib/sandbox.js +11 -4
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +3 -0
- package/lib/ui.js +1 -1
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/app.py +43 -4
- package/voz/primo_voz/chat.html +17 -6
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""passos: a 2 to 4 step flow. Numbered circles light up one by one, joined by arrows that draw themselves.
|
|
2
|
+
|
|
3
|
+
Frame 0 already shows the card and the steps as faint placeholders (the shape of the flow). Each step then
|
|
4
|
+
pops in the accent color with a pulse ring and its name rises underneath (or beside it, vertically). The
|
|
5
|
+
arrow into a step draws during the moment before that step lights up, so the eye travels along the flow.
|
|
6
|
+
Timing: `delay` + `stagger`, or `times` (one second per step, counted from the item start).
|
|
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
|
+
from motion.engine import MotionError
|
|
16
|
+
|
|
17
|
+
TEMPLATE = {
|
|
18
|
+
"id": "passos",
|
|
19
|
+
"label_pt": "Passo a passo",
|
|
20
|
+
"description_pt": "De 2 a 4 passos em círculos numerados, ligados por setas que se desenham de um para o outro. "
|
|
21
|
+
"Cada passo acende na cor de destaque, com o nome embaixo (ou ao lado, na vertical).",
|
|
22
|
+
"use_when_pt": "Quando a fala descreve um processo curto: comprou, acessou, usou. Compra, conecta, usa. "
|
|
23
|
+
"Para cada passo acender junto com a fala, use times (segundos desde o começo do item).",
|
|
24
|
+
"notes_pt": "Nomes curtos (uma ou duas palavras) leem melhor. A região padrão é uma faixa no alto da tela, "
|
|
25
|
+
"acima do rosto; na vertical os passos ficam mais altos: use a região centro ou baixo.",
|
|
26
|
+
"category": "fluxos",
|
|
27
|
+
"version": 1,
|
|
28
|
+
"params": {
|
|
29
|
+
"items": {"type": "list", "item": {"type": "text", "max_len": 20, "required_text": True},
|
|
30
|
+
"min_items": 2, "max_items": 4, "default": ["Comprou", "Acessou", "Usou"],
|
|
31
|
+
"label_pt": "Nome de cada passo, na ordem (2 a 4, cada um com até 20 letras)"},
|
|
32
|
+
"title": {"type": "text", "default": "", "max_len": 36,
|
|
33
|
+
"label_pt": "Título em cima dos passos (opcional). *Palavra* entre asteriscos fica na cor de "
|
|
34
|
+
"destaque"},
|
|
35
|
+
"direction": {"type": "choice", "choices": ["horizontal", "vertical"], "default": "horizontal",
|
|
36
|
+
"label_pt": "Passos lado a lado (horizontal) ou um embaixo do outro (vertical)"},
|
|
37
|
+
"icon": {"type": "choice", "choices": ["numero", "check"], "default": "numero",
|
|
38
|
+
"label_pt": "Dentro do círculo: o número do passo ou um check"},
|
|
39
|
+
"delay": {"type": "number", "default": 0.1, "min": 0, "max": 30,
|
|
40
|
+
"label_pt": "Segundos até o primeiro passo acender"},
|
|
41
|
+
"stagger": {"type": "number", "default": 0.6, "min": 0.15, "max": 5,
|
|
42
|
+
"label_pt": "Segundos entre um passo e o próximo"},
|
|
43
|
+
"times": {"type": "list", "item": {"type": "number", "min": 0, "max": 60}, "nullable": True,
|
|
44
|
+
"default": None, "min_items": 2, "max_items": 4,
|
|
45
|
+
"label_pt": "Segundo em que cada passo acende, contado do começo do item (um por passo, em "
|
|
46
|
+
"ordem crescente). Vazio: usa delay e stagger"},
|
|
47
|
+
"size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
|
|
48
|
+
"label_pt": "Tamanho dos círculos e dos nomes"},
|
|
49
|
+
"card": {"type": "bool", "default": True,
|
|
50
|
+
"label_pt": "Cartão atrás dos passos (sem cartão: contorno e sombra)"},
|
|
51
|
+
},
|
|
52
|
+
"default_duration": 3.0,
|
|
53
|
+
"default_region": [90, 240, 900, 330],
|
|
54
|
+
"sfx": [{"at": 0.1, "name": "pop"}, {"at": 0.7, "name": "pop"}, {"at": 1.3, "name": "pop"}],
|
|
55
|
+
"example": {"items": ["Compra", "Conecta", "Usa"], "title": "Simples *assim*:"},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
LABEL_FONT = "text_bold"
|
|
59
|
+
TITLE_FONT = "sans_black"
|
|
60
|
+
NUM_FONT = "sans_black"
|
|
61
|
+
BASE_R = {"normal": 56.0, "grande": 66.0}
|
|
62
|
+
BASE_LABEL = {"normal": 46.0, "grande": 54.0}
|
|
63
|
+
MIN_LABEL = 30.0
|
|
64
|
+
MAX_COL = 270.0
|
|
65
|
+
|
|
66
|
+
SKELETON = -0.08 # the empty markers start before frame 0, so frame 0 always shows them
|
|
67
|
+
ENTER = 0.5
|
|
68
|
+
POP = 0.42 # circle pop (out_back)
|
|
69
|
+
ARROW = 0.34 # longest arrow draw, ending when the next step lights up
|
|
70
|
+
RING = 0.6 # pulse ring after a pop
|
|
71
|
+
LABEL_AT = 0.06
|
|
72
|
+
LABEL_IN = 0.34
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _times(p: dict) -> list[float]:
|
|
76
|
+
n = len(p["items"])
|
|
77
|
+
times = p.get("times")
|
|
78
|
+
if times is None:
|
|
79
|
+
return [p["delay"] + i * p["stagger"] for i in range(n)]
|
|
80
|
+
if len(times) != n:
|
|
81
|
+
raise MotionError(f"passos.params.times precisa de um tempo para cada passo ({n} passos, veio {len(times)})")
|
|
82
|
+
for i in range(1, n):
|
|
83
|
+
if times[i] < times[i - 1]:
|
|
84
|
+
raise MotionError(f"passos.params.times precisa estar em ordem crescente (o passo {i + 1} acende em "
|
|
85
|
+
f"{times[i]:g} s, antes do passo {i} em {times[i - 1]:g} s)")
|
|
86
|
+
return [float(v) for v in times]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate(params: dict) -> None:
|
|
90
|
+
"""One time per item, in order: refused when the item is planned, not only when it renders."""
|
|
91
|
+
_times(params)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def warnings(params: dict, box: E.Box) -> list[str]:
|
|
95
|
+
L = _layout(json.dumps(params, sort_keys=True), box)
|
|
96
|
+
if L["vertical"] and L["R"] < BASE_R[params["size"]] * 0.8:
|
|
97
|
+
return [f"os passos na vertical ficaram menores porque a região tem só {box.h:.0f} px de altura; "
|
|
98
|
+
f"use a região tela, uma região mais alta ou menos passos"]
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _arrow_window(times: list[float], i: int) -> tuple[float, float]:
|
|
103
|
+
"""(start, duration) of the arrow that leads into step i (i >= 1)."""
|
|
104
|
+
end = times[i]
|
|
105
|
+
start = max(times[i - 1] + 0.12, end - ARROW)
|
|
106
|
+
return min(start, end), max(0.0, end - start)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _title_block(title: str, width: float, size: float) -> tuple[float, float, float]:
|
|
110
|
+
"""(size, cap, width) of the title line."""
|
|
111
|
+
if not title:
|
|
112
|
+
return 0.0, 0.0, 0.0
|
|
113
|
+
plain = " ".join(w for w, _ in E.parse_accent(title))
|
|
114
|
+
ts = E.fit_text(plain, TITLE_FONT, width, size, 26.0)
|
|
115
|
+
return ts, E.measure("H", TITLE_FONT, ts).cap, E.measure(plain, TITLE_FONT, ts).width
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _fit_labels(items: list[str], size: float, width: float, max_lines: int) -> tuple[float, list[list[str]]]:
|
|
119
|
+
"""Largest common label size (<= size) where every label fits `width` in at most max_lines."""
|
|
120
|
+
s = size
|
|
121
|
+
while True:
|
|
122
|
+
wraps = [E.wrap_text(it, LABEL_FONT, s, width, max_lines) for it in items]
|
|
123
|
+
widest = max(E.measure(ln, LABEL_FONT, s).width for lines in wraps for ln in lines)
|
|
124
|
+
if widest <= width + 0.5 or s <= MIN_LABEL:
|
|
125
|
+
break
|
|
126
|
+
s = max(MIN_LABEL, s * 0.95)
|
|
127
|
+
if widest > width + 0.5: # one long word: shrink below the floor rather than overflow
|
|
128
|
+
s = min(E.fit_text(ln, LABEL_FONT, width, s, 20.0) for lines in wraps for ln in lines)
|
|
129
|
+
wraps = [E.wrap_text(it, LABEL_FONT, s, width, max_lines) for it in items]
|
|
130
|
+
return s, wraps
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@lru_cache(maxsize=64)
|
|
134
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
135
|
+
p = json.loads(params_json)
|
|
136
|
+
items = p["items"]
|
|
137
|
+
n = len(items)
|
|
138
|
+
card = p["card"]
|
|
139
|
+
title = p["title"].strip()
|
|
140
|
+
pad_x = 36.0 if card else 4.0
|
|
141
|
+
pad_y = 38.0 if card else 4.0
|
|
142
|
+
R = BASE_R[p["size"]]
|
|
143
|
+
lab = BASE_LABEL[p["size"]]
|
|
144
|
+
steps = []
|
|
145
|
+
if p["direction"] == "horizontal":
|
|
146
|
+
col = min(MAX_COL, (box.w - 2 * pad_x) / n)
|
|
147
|
+
R = min(R, col * 0.30)
|
|
148
|
+
ts, tcap, tw = _title_block(title, box.w - 2 * pad_x, lab * 1.12)
|
|
149
|
+
title_h = tcap + lab * 0.62 if title else 0.0
|
|
150
|
+
lab_size, wraps = _fit_labels(items, lab, col - 14.0, 2)
|
|
151
|
+
cap = E.measure("H", LABEL_FONT, lab_size).cap
|
|
152
|
+
line_h = lab_size * 1.14
|
|
153
|
+
lines = max(len(w) for w in wraps)
|
|
154
|
+
gap = R * 0.40
|
|
155
|
+
# a label block leaves room for descenders (p, g, ç)
|
|
156
|
+
lab_h = cap + (lines - 1) * line_h + lab_size * 0.22
|
|
157
|
+
content_h = title_h + 2 * R + gap + lab_h
|
|
158
|
+
needed = content_h + 2 * pad_y
|
|
159
|
+
if needed > box.h: # a short region: shrink circles and labels together
|
|
160
|
+
k = max(0.55, (box.h - 2 * pad_y) / content_h)
|
|
161
|
+
R *= k
|
|
162
|
+
gap *= k
|
|
163
|
+
lab_size, wraps = _fit_labels(items, max(MIN_LABEL * 0.9, lab_size * k), col - 14.0, 2)
|
|
164
|
+
cap = E.measure("H", LABEL_FONT, lab_size).cap
|
|
165
|
+
line_h = lab_size * 1.14
|
|
166
|
+
lab_h = cap + (lines - 1) * line_h + lab_size * 0.22
|
|
167
|
+
ts, tcap, tw = _title_block(title, box.w - 2 * pad_x, min(ts, lab_size * 1.12))
|
|
168
|
+
title_h = tcap + lab_size * 0.62 if title else 0.0
|
|
169
|
+
content_h = title_h + 2 * R + gap + lab_h
|
|
170
|
+
content_w = col * n
|
|
171
|
+
card_w = min(box.w, max(content_w, tw) + 2 * pad_x)
|
|
172
|
+
card_h = content_h + 2 * pad_y
|
|
173
|
+
cx0 = (box.w - card_w) / 2.0
|
|
174
|
+
cy0 = (box.h - card_h) / 2.0
|
|
175
|
+
top = cy0 + pad_y
|
|
176
|
+
left = (box.w - content_w) / 2.0
|
|
177
|
+
cy = top + title_h + R
|
|
178
|
+
for i in range(n):
|
|
179
|
+
x = left + col * (i + 0.5)
|
|
180
|
+
steps.append({"cx": x, "cy": cy, "lines": wraps[i], "anchor": "ms",
|
|
181
|
+
"lx": x, "ly": cy + R + gap + cap})
|
|
182
|
+
g = R * 0.30
|
|
183
|
+
arrows = [((steps[i - 1]["cx"] + R + g, cy), (steps[i]["cx"] - R - g, cy)) for i in range(1, n)]
|
|
184
|
+
title_pos = (box.w / 2.0, top + tcap, "ms")
|
|
185
|
+
else:
|
|
186
|
+
ts, tcap, tw = _title_block(title, box.w - 2 * pad_x, lab * 1.12)
|
|
187
|
+
title_h = tcap + lab * 0.70 if title else 0.0
|
|
188
|
+
ratio = 1.2 # arrow length in circle radii
|
|
189
|
+
content_h = title_h + n * 2 * R + (n - 1) * ratio * R
|
|
190
|
+
if content_h > box.h - 2 * pad_y:
|
|
191
|
+
# a short region: tighter card padding, then shorter arrows, then smaller circles (never below 45%)
|
|
192
|
+
pad_y = min(pad_y, 18.0)
|
|
193
|
+
avail = box.h - 2 * pad_y - title_h
|
|
194
|
+
k = avail / ((n * 2 + (n - 1) * ratio) * R)
|
|
195
|
+
if k < 0.6:
|
|
196
|
+
ratio = 0.6
|
|
197
|
+
k = avail / ((n * 2 + (n - 1) * ratio) * R)
|
|
198
|
+
R *= max(0.45, min(1.0, k))
|
|
199
|
+
arrow_len = ratio * R
|
|
200
|
+
content_h = title_h + n * 2 * R + (n - 1) * arrow_len
|
|
201
|
+
gap = max(R * 0.45, 20.0)
|
|
202
|
+
lab_size = min(lab * 1.12, max(MIN_LABEL, R * 1.05))
|
|
203
|
+
lab_w = box.w - 2 * pad_x - 2 * R - gap
|
|
204
|
+
lab_size, wraps = _fit_labels(items, lab_size, lab_w, 1)
|
|
205
|
+
cap = E.measure("H", LABEL_FONT, lab_size).cap
|
|
206
|
+
widest = max(E.measure(w[0], LABEL_FONT, lab_size).width for w in wraps)
|
|
207
|
+
content_w = 2 * R + gap + widest
|
|
208
|
+
card_w = min(box.w, max(content_w, tw) + 2 * pad_x, box.w)
|
|
209
|
+
card_w = max(card_w, min(box.w, 420.0 if card else 0.0))
|
|
210
|
+
card_h = content_h + 2 * pad_y
|
|
211
|
+
cx0 = (box.w - card_w) / 2.0
|
|
212
|
+
cy0 = (box.h - card_h) / 2.0
|
|
213
|
+
top = cy0 + pad_y
|
|
214
|
+
left = cx0 + pad_x
|
|
215
|
+
if not title or tw <= content_w:
|
|
216
|
+
left = (box.w - content_w) / 2.0
|
|
217
|
+
x = left + R
|
|
218
|
+
for i in range(n):
|
|
219
|
+
cy = top + title_h + R + i * (2 * R + arrow_len)
|
|
220
|
+
steps.append({"cx": x, "cy": cy, "lines": wraps[i], "anchor": "ls",
|
|
221
|
+
"lx": x + R + gap, "ly": cy + cap / 2.0})
|
|
222
|
+
g = R * 0.16
|
|
223
|
+
arrows = [((x, steps[i - 1]["cy"] + R + g), (x, steps[i]["cy"] - R - g)) for i in range(1, n)]
|
|
224
|
+
title_pos = (cx0 + card_w / 2.0, top + tcap, "ms")
|
|
225
|
+
return {
|
|
226
|
+
"steps": steps, "arrows": arrows, "R": R, "lab_size": lab_size, "line_h": lab_size * 1.14,
|
|
227
|
+
"card_box": (cx0, cy0, card_w, card_h), "radius": min(40.0, card_h * 0.24), "card": card,
|
|
228
|
+
"title": title, "title_size": ts, "title_pos": title_pos, "times": _times(p), "icon": p["icon"],
|
|
229
|
+
"vertical": p["direction"] == "vertical",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
234
|
+
cv = E.Canvas(box)
|
|
235
|
+
t = frame_index / fps
|
|
236
|
+
t_in = t + 1.0 / fps
|
|
237
|
+
L = _layout(json.dumps(params, sort_keys=True), box)
|
|
238
|
+
card = L["card"]
|
|
239
|
+
R = L["R"]
|
|
240
|
+
times = L["times"]
|
|
241
|
+
ink = "text" if card else "white"
|
|
242
|
+
outline = {} if card else {"stroke": L["lab_size"] * 0.07, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
|
|
243
|
+
circle_shadow = E.Shadow(blur=12.0, dy=5.0, opacity=0.35 if card else 0.55)
|
|
244
|
+
arrow_shadow = None if card else E.Shadow(blur=6.0, dy=2.0, opacity=0.5)
|
|
245
|
+
arrow_color = "muted" if card else (255, 255, 255, 235)
|
|
246
|
+
|
|
247
|
+
alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
|
|
248
|
+
scale = 0.92 + 0.08 * E.out_back(E.progress(t_in, 0.0, ENTER), 1.4)
|
|
249
|
+
rise = 26.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
|
|
250
|
+
with cv.group(opacity=alpha, scale=scale, dy=rise):
|
|
251
|
+
if card:
|
|
252
|
+
x, y, w, h = L["card_box"]
|
|
253
|
+
cv.card(x, y, w, h, radius=L["radius"])
|
|
254
|
+
if L["title"]:
|
|
255
|
+
tx, ty, anchor = L["title_pos"]
|
|
256
|
+
cv.rich_text(tx, ty, L["title"], TITLE_FONT, L["title_size"], color=ink, anchor=anchor, **outline)
|
|
257
|
+
# arrows first: circles sit on top of their ends
|
|
258
|
+
for i, (a, b) in enumerate(L["arrows"], start=1):
|
|
259
|
+
start, dur = _arrow_window(times, i)
|
|
260
|
+
prog = E.eased(t, start, dur, "in_out_cubic") if dur > 0 else (1.0 if t >= start else 0.0)
|
|
261
|
+
if prog > 0:
|
|
262
|
+
cv.arrow(a[0], a[1], b[0], b[1], width=max(4.0, R * 0.13), color=arrow_color, progress=prog,
|
|
263
|
+
head=max(14.0, R * 0.34), shadow=arrow_shadow)
|
|
264
|
+
for i, st in enumerate(L["steps"]):
|
|
265
|
+
ti = times[i]
|
|
266
|
+
cx, cy = st["cx"], st["cy"]
|
|
267
|
+
pop_p = E.progress(t_in, ti, POP)
|
|
268
|
+
# placeholder: the faint step that is waiting
|
|
269
|
+
sk = E.out_back(E.progress(t_in, SKELETON + 0.06 * i, 0.36), 1.6)
|
|
270
|
+
if sk > 0.01 and pop_p < 1.0:
|
|
271
|
+
with cv.group(scale=max(0.01, sk), pivot=(cx, cy)):
|
|
272
|
+
if card:
|
|
273
|
+
cv.circle(cx, cy, R, fill="track")
|
|
274
|
+
else:
|
|
275
|
+
cv.circle(cx, cy, R, fill=(0, 0, 0, 120), stroke=(255, 255, 255, 200),
|
|
276
|
+
stroke_width=max(2.0, R * 0.06))
|
|
277
|
+
if L["icon"] == "numero":
|
|
278
|
+
cv.text(cx, cy, str(i + 1), NUM_FONT, R * 0.95, color=ink, opacity=0.45, anchor="mc")
|
|
279
|
+
if pop_p <= 0:
|
|
280
|
+
continue
|
|
281
|
+
# pulse ring
|
|
282
|
+
e = E.eased(t, ti, RING, "out_cubic")
|
|
283
|
+
if 0 < e < 1:
|
|
284
|
+
cv.ring(cx, cy, R * (1.0 + 0.5 * e), max(2.0, R * 0.08 * (1.0 - e) + 1.0), color="accent",
|
|
285
|
+
opacity=0.7 * (1.0 - e))
|
|
286
|
+
with cv.group(scale=max(0.01, E.out_back(pop_p, 2.0)), pivot=(cx, cy)):
|
|
287
|
+
cv.circle(cx, cy, R, fill="accent", shadow=circle_shadow)
|
|
288
|
+
if L["icon"] == "numero":
|
|
289
|
+
cv.text(cx, cy, str(i + 1), NUM_FONT, R * 0.95, color="on_accent", anchor="mc")
|
|
290
|
+
if L["icon"] == "check":
|
|
291
|
+
draw = E.eased(t, ti + 0.08, 0.26, "in_out_cubic")
|
|
292
|
+
if draw > 0:
|
|
293
|
+
cv.check(cx, cy + R * 0.03, R * 0.98, width=R * 0.19, color="on_accent", progress=draw)
|
|
294
|
+
la = E.eased(t, ti + LABEL_AT, LABEL_IN, "out_cubic")
|
|
295
|
+
if cv.visible(la):
|
|
296
|
+
off = 14.0 * (1.0 - la)
|
|
297
|
+
for k, ln in enumerate(st["lines"]):
|
|
298
|
+
if L["vertical"]:
|
|
299
|
+
cv.text(st["lx"] - off, st["ly"], ln, LABEL_FONT, L["lab_size"], color=ink, opacity=la,
|
|
300
|
+
anchor="ls", **outline)
|
|
301
|
+
else:
|
|
302
|
+
cv.text(st["lx"], st["ly"] + k * L["line_h"] + off, ln, LABEL_FONT, L["lab_size"],
|
|
303
|
+
color=ink, opacity=la, anchor="ms", **outline)
|
|
304
|
+
return cv
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
308
|
+
return [{"frame": E.frame_of(ti, fps), "name": "pop", "why": f"passo {i + 1}"}
|
|
309
|
+
for i, ti in enumerate(_times(params))]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def suggested_duration(params: dict) -> float:
|
|
313
|
+
"""Last step, its label and about 1.5 s to read the flow."""
|
|
314
|
+
return round(max(TEMPLATE["default_duration"], _times(params)[-1] + max(POP, LABEL_AT + LABEL_IN) + 1.5), 2)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def settle(params: dict, fps: int) -> float:
|
|
318
|
+
times = _times(params)
|
|
319
|
+
n = len(times)
|
|
320
|
+
return max(ENTER, SKELETON + 0.06 * (n - 1) + 0.36,
|
|
321
|
+
times[-1] + max(POP, RING, LABEL_AT + LABEL_IN, 0.08 + 0.26)) + 0.05
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""riscado: items appear on a card, then each one gets crossed out (a red strike line or a red X).
|
|
2
|
+
|
|
3
|
+
For "você não precisa de...": the list builds first (`delay` + `stagger`, or `times`), then the items are
|
|
4
|
+
crossed one by one (`cross_delay` + `cross_stagger`, or `cross_times`). A crossed item fades to half.
|
|
5
|
+
Frame 0 already shows the card, the title and the empty markers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
|
|
14
|
+
from motion import engine as E
|
|
15
|
+
from motion.engine import MotionError
|
|
16
|
+
|
|
17
|
+
TEMPLATE = {
|
|
18
|
+
"id": "riscado",
|
|
19
|
+
"label_pt": "Lista riscada",
|
|
20
|
+
"description_pt": "Uma lista num cartão em que os itens aparecem e depois são riscados um a um, com uma linha "
|
|
21
|
+
"vermelha por cima do texto ou um X vermelho no lugar do marcador.",
|
|
22
|
+
"use_when_pt": "Quando a fala derruba objeções ou crenças: você não precisa de estoque, de aparecer, de "
|
|
23
|
+
"investir alto. Para riscar junto com a fala, use cross_times (segundos desde o começo do item).",
|
|
24
|
+
"notes_pt": "Itens curtos leem melhor (até 32 letras). Sem título, o cartão mostra só a lista.",
|
|
25
|
+
"category": "listas",
|
|
26
|
+
"version": 1,
|
|
27
|
+
"params": {
|
|
28
|
+
"items": {"type": "list", "item": {"type": "text", "max_len": 32, "required_text": True},
|
|
29
|
+
"min_items": 1, "max_items": 6,
|
|
30
|
+
"default": ["Ter estoque", "Aparecer nos vídeos", "Investir alto"],
|
|
31
|
+
"label_pt": "Itens que vão ser riscados, de cima para baixo (1 a 6, cada um com até 32 letras)"},
|
|
32
|
+
"title": {"type": "text", "default": "Você *não* precisa de:", "max_len": 36,
|
|
33
|
+
"label_pt": "Título em cima da lista (vazio: sem título). *Palavra* entre asteriscos fica na "
|
|
34
|
+
"cor de destaque"},
|
|
35
|
+
"mode": {"type": "choice", "choices": ["linha", "x"], "default": "linha",
|
|
36
|
+
"label_pt": "Como riscar: linha (traço vermelho por cima do texto) ou x (X vermelho no marcador)"},
|
|
37
|
+
"delay": {"type": "number", "default": 0.2, "min": 0, "max": 30,
|
|
38
|
+
"label_pt": "Segundos até o primeiro item aparecer (o cartão já aparece no começo)"},
|
|
39
|
+
"stagger": {"type": "number", "default": 0.3, "min": 0.1, "max": 5,
|
|
40
|
+
"label_pt": "Segundos entre um item aparecer e o próximo"},
|
|
41
|
+
"times": {"type": "list", "item": {"type": "number", "min": 0, "max": 60}, "nullable": True,
|
|
42
|
+
"default": None, "min_items": 1, "max_items": 6,
|
|
43
|
+
"label_pt": "Segundo em que cada item aparece, contado do começo do item (um por item, em ordem "
|
|
44
|
+
"crescente). Vazio: usa delay e stagger"},
|
|
45
|
+
"cross_delay": {"type": "number", "default": 0.55, "min": 0, "max": 30,
|
|
46
|
+
"label_pt": "Segundos entre o último item aparecer e o primeiro risco"},
|
|
47
|
+
"cross_stagger": {"type": "number", "default": 0.4, "min": 0.1, "max": 5,
|
|
48
|
+
"label_pt": "Segundos entre um risco e o próximo"},
|
|
49
|
+
"cross_times": {"type": "list", "item": {"type": "number", "min": 0, "max": 60}, "nullable": True,
|
|
50
|
+
"default": None, "min_items": 1, "max_items": 6,
|
|
51
|
+
"label_pt": "Segundo em que cada item é riscado, contado do começo do item (um por item, "
|
|
52
|
+
"depois de ele aparecer). Vazio: usa cross_delay e cross_stagger"},
|
|
53
|
+
"size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
|
|
54
|
+
"label_pt": "Tamanho do texto"},
|
|
55
|
+
"card": {"type": "bool", "default": True,
|
|
56
|
+
"label_pt": "Cartão atrás da lista (sem cartão: texto com contorno e sombra)"},
|
|
57
|
+
},
|
|
58
|
+
"default_duration": 3.5,
|
|
59
|
+
"default_region": "baixo",
|
|
60
|
+
"sfx": [{"at": 0.0, "name": "pop"}, {"at": 1.47, "name": "tick", "volume": 1.2},
|
|
61
|
+
{"at": 1.87, "name": "tick", "volume": 1.2}, {"at": 2.27, "name": "tick", "volume": 1.2}],
|
|
62
|
+
"example": {"title": "Você *não* precisa de:",
|
|
63
|
+
"items": ["Ter estoque", "Aparecer nos vídeos", "Investir alto", "Experiência"]},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
FONT = "text_bold"
|
|
67
|
+
TITLE_FONT = "sans_black"
|
|
68
|
+
BASE_SIZE = {"normal": 56.0, "grande": 68.0}
|
|
69
|
+
MIN_SIZE = 34.0
|
|
70
|
+
|
|
71
|
+
SKELETON = -0.08 # the empty markers start before frame 0, so frame 0 always shows them
|
|
72
|
+
ENTER = 0.5 # card entrance
|
|
73
|
+
ROW_IN = 0.32 # item slide and fade
|
|
74
|
+
STRIKE = 0.30 # the strike line draws (per item, all its lines)
|
|
75
|
+
X_POP = 0.38 # the red marker pops
|
|
76
|
+
X_DRAW = 0.24 # the X strokes draw
|
|
77
|
+
DIM_AT = 0.12 # the text starts fading this long after the cross starts
|
|
78
|
+
DIM = 0.30
|
|
79
|
+
DIM_TO = 0.5 # opacity of a crossed text
|
|
80
|
+
SOUND_AT = {"linha": 0.12, "x": 0.04} # the sound lands when the stroke is visibly under way
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _times(p: dict) -> tuple[list[float], list[float]]:
|
|
84
|
+
"""(appear times, cross times), validated."""
|
|
85
|
+
n = len(p["items"])
|
|
86
|
+
times = p.get("times")
|
|
87
|
+
if times is None:
|
|
88
|
+
shown = [p["delay"] + i * p["stagger"] for i in range(n)]
|
|
89
|
+
else:
|
|
90
|
+
if len(times) != n:
|
|
91
|
+
raise MotionError(f"riscado.params.times precisa de um tempo para cada item ({n} itens, veio {len(times)})")
|
|
92
|
+
for i in range(1, n):
|
|
93
|
+
if times[i] < times[i - 1]:
|
|
94
|
+
raise MotionError(f"riscado.params.times precisa estar em ordem crescente (o item {i + 1} aparece "
|
|
95
|
+
f"em {times[i]:g} s, antes do item {i} em {times[i - 1]:g} s)")
|
|
96
|
+
shown = [float(v) for v in times]
|
|
97
|
+
crosses = p.get("cross_times")
|
|
98
|
+
if crosses is None:
|
|
99
|
+
first = shown[-1] + p["cross_delay"]
|
|
100
|
+
crossed = [first + i * p["cross_stagger"] for i in range(n)]
|
|
101
|
+
else:
|
|
102
|
+
if len(crosses) != n:
|
|
103
|
+
raise MotionError(f"riscado.params.cross_times precisa de um tempo para cada item ({n} itens, "
|
|
104
|
+
f"veio {len(crosses)})")
|
|
105
|
+
for i, (a, c) in enumerate(zip(shown, crosses)):
|
|
106
|
+
if c < a:
|
|
107
|
+
raise MotionError(f"riscado.params.cross_times: o item {i + 1} aparece em {a:g} s e seria riscado "
|
|
108
|
+
f"antes, em {c:g} s. Cada risco precisa vir depois de o item aparecer")
|
|
109
|
+
crossed = [float(v) for v in crosses]
|
|
110
|
+
return shown, crossed
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate(params: dict) -> None:
|
|
114
|
+
"""One time per item, in order: refused when the item is planned, not only when it renders."""
|
|
115
|
+
_times(params)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@lru_cache(maxsize=64)
|
|
119
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
120
|
+
p = json.loads(params_json)
|
|
121
|
+
items = p["items"]
|
|
122
|
+
card = p["card"]
|
|
123
|
+
mode = p["mode"]
|
|
124
|
+
title = p["title"].strip()
|
|
125
|
+
title_plain = " ".join(w for w, _ in E.parse_accent(title))
|
|
126
|
+
pad_x = 54.0 if card else 6.0
|
|
127
|
+
pad_y = 48.0 if card else 6.0
|
|
128
|
+
|
|
129
|
+
def build(size: float) -> tuple[dict, bool]:
|
|
130
|
+
marker = size * (1.04 if mode == "x" else 0.30)
|
|
131
|
+
gap = size * (0.44 if mode == "x" else 0.50)
|
|
132
|
+
text_w = box.w - 2 * pad_x - marker - gap - size * 0.12 # the strike runs a little past the text
|
|
133
|
+
m = E.measure("H", FONT, size)
|
|
134
|
+
line_h = size * 1.17
|
|
135
|
+
row_gap = size * 0.46
|
|
136
|
+
slot = size * 1.04 # row height of a one-line item (same rhythm in both modes)
|
|
137
|
+
rows, fits, y = [], True, 0.0
|
|
138
|
+
title_size = title_w = 0.0
|
|
139
|
+
if title:
|
|
140
|
+
title_size = min(size * 0.94, E.fit_text(title_plain, TITLE_FONT, box.w - 2 * pad_x, size * 0.94, 26.0))
|
|
141
|
+
title_w = E.measure(title_plain, TITLE_FONT, title_size).width
|
|
142
|
+
y = E.measure("H", TITLE_FONT, title_size).cap + size * 0.74
|
|
143
|
+
widest = 0.0
|
|
144
|
+
for k, text in enumerate(items):
|
|
145
|
+
lines = E.wrap_text(text, FONT, size, text_w, 2)
|
|
146
|
+
sizes = []
|
|
147
|
+
for ln in lines:
|
|
148
|
+
if E.measure(ln, FONT, size).width > text_w + 0.5:
|
|
149
|
+
fits = False
|
|
150
|
+
sizes.append(E.fit_text(ln, FONT, text_w, size, 20.0))
|
|
151
|
+
else:
|
|
152
|
+
sizes.append(size)
|
|
153
|
+
widths = [E.measure(ln, FONT, s).width for ln, s in zip(lines, sizes)]
|
|
154
|
+
widest = max(widest, max(widths))
|
|
155
|
+
base0 = y + slot / 2.0 + m.cap / 2.0
|
|
156
|
+
bottom = max(y + slot, base0 + (len(lines) - 1) * line_h)
|
|
157
|
+
rows.append({"lines": lines, "sizes": sizes, "widths": widths, "cy": y + slot / 2.0, "base0": base0})
|
|
158
|
+
y = bottom + (row_gap if k < len(items) - 1 else 0.0)
|
|
159
|
+
card_h = y + 2 * pad_y
|
|
160
|
+
content_w = max(marker + gap + widest + size * 0.12, title_w)
|
|
161
|
+
card_w = min(box.w, max(content_w + 2 * pad_x, 380.0 if card else 0.0))
|
|
162
|
+
L = {"size": size, "marker": marker, "gap": gap, "line_h": line_h, "rows": rows, "x_height": m.x_height,
|
|
163
|
+
"title_size": title_size, "title_base": E.measure("H", TITLE_FONT, title_size).cap if title else 0.0,
|
|
164
|
+
"card_w": card_w, "card_h": card_h}
|
|
165
|
+
return L, fits and card_h <= box.h
|
|
166
|
+
|
|
167
|
+
size = BASE_SIZE[p["size"]]
|
|
168
|
+
L, ok = build(size)
|
|
169
|
+
while not ok and size > MIN_SIZE:
|
|
170
|
+
size = max(MIN_SIZE, size * 0.95)
|
|
171
|
+
L, ok = build(size)
|
|
172
|
+
card_x = (box.w - L["card_w"]) / 2.0
|
|
173
|
+
card_y = (box.h - L["card_h"]) / 2.0
|
|
174
|
+
L.update({"card_box": (card_x, card_y, L["card_w"], L["card_h"]), "x0": card_x + pad_x, "y0": card_y + pad_y,
|
|
175
|
+
"radius": min(40.0, L["card_h"] * 0.2), "title": title, "card": card, "mode": mode})
|
|
176
|
+
L["shown"], L["crossed"] = _times(p)
|
|
177
|
+
# strike geometry per item: one stroke per line, a slight upward slant and bow like a marker
|
|
178
|
+
for k, row in enumerate(L["rows"]):
|
|
179
|
+
strokes = []
|
|
180
|
+
for j, (w, sz) in enumerate(zip(row["widths"], row["sizes"])):
|
|
181
|
+
xh = E.measure("x", FONT, sz).x_height
|
|
182
|
+
y = row["base0"] + j * L["line_h"] - xh * 0.52
|
|
183
|
+
x_a = -sz * 0.04
|
|
184
|
+
x_b = w + sz * 0.10
|
|
185
|
+
slant = min(sz * 0.07, (x_b - x_a) * 0.04) * (1 if (k + j) % 2 == 0 else 0.6)
|
|
186
|
+
pts = E.bezier_points((x_a, y + slant), ((x_a + x_b) / 2.0, y + sz * 0.025), (x_b, y - slant), n=16)
|
|
187
|
+
strokes.append({"points": pts, "width": sz * 0.16, "length": E.path_length(pts)})
|
|
188
|
+
row["strokes"] = strokes
|
|
189
|
+
return L
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
193
|
+
cv = E.Canvas(box)
|
|
194
|
+
t = frame_index / fps
|
|
195
|
+
t_in = t + 1.0 / fps
|
|
196
|
+
L = _layout(json.dumps(params, sort_keys=True), box)
|
|
197
|
+
card, mode = L["card"], L["mode"]
|
|
198
|
+
size, d = L["size"], L["marker"]
|
|
199
|
+
|
|
200
|
+
alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
|
|
201
|
+
scale = 0.9 + 0.1 * E.out_back(E.progress(t_in, 0.0, ENTER), 1.4)
|
|
202
|
+
rise = 32.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
|
|
203
|
+
outline = {} if card else {"stroke": size * 0.07, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
|
|
204
|
+
ink = "text" if card else "white" # no card: white letters with a black outline in both themes
|
|
205
|
+
empty = "track" if card else (255, 255, 255, 215)
|
|
206
|
+
strike_shadow = E.Shadow(blur=6.0, dy=2.0, opacity=0.35 if card else 0.55)
|
|
207
|
+
|
|
208
|
+
with cv.group(opacity=alpha, scale=scale, dy=rise):
|
|
209
|
+
cx0, cy0, cw, ch = L["card_box"]
|
|
210
|
+
if card:
|
|
211
|
+
cv.card(cx0, cy0, cw, ch, radius=L["radius"])
|
|
212
|
+
x0, y0 = L["x0"], L["y0"]
|
|
213
|
+
if L["title"]:
|
|
214
|
+
cv.rich_text(x0, y0 + L["title_base"], L["title"], TITLE_FONT, L["title_size"], color=ink, **outline)
|
|
215
|
+
mx = x0 + d / 2.0
|
|
216
|
+
tx = x0 + d + L["gap"]
|
|
217
|
+
for i, row in enumerate(L["rows"]):
|
|
218
|
+
ts, tc = L["shown"][i], L["crossed"][i]
|
|
219
|
+
cy = y0 + row["cy"]
|
|
220
|
+
sk = E.out_back(E.progress(t_in, SKELETON + 0.05 * i, 0.34), 1.8)
|
|
221
|
+
if mode == "x":
|
|
222
|
+
pop_p = E.progress(t, tc, X_POP)
|
|
223
|
+
if sk > 0.01 and pop_p < 1.0:
|
|
224
|
+
with cv.group(scale=max(0.01, sk), pivot=(mx, cy)):
|
|
225
|
+
w = d * 0.085
|
|
226
|
+
if not card:
|
|
227
|
+
cv.ring(mx, cy, d / 2 - w / 2, w * 2.8, color=(0, 0, 0, 120))
|
|
228
|
+
cv.ring(mx, cy, d / 2 - w / 2, w, color=empty)
|
|
229
|
+
if pop_p > 0:
|
|
230
|
+
with cv.group(scale=max(0.01, E.out_back(pop_p, 2.2)), pivot=(mx, cy)):
|
|
231
|
+
cv.circle(mx, cy, d / 2, fill="danger",
|
|
232
|
+
shadow=None if card else E.Shadow(blur=10.0, dy=4.0, opacity=0.5))
|
|
233
|
+
draw = E.eased(t, tc + 0.05, X_DRAW, "in_out_cubic")
|
|
234
|
+
if draw > 0:
|
|
235
|
+
cv.cross(mx, cy, d * 0.36, width=d * 0.12, color="white", progress=draw)
|
|
236
|
+
else:
|
|
237
|
+
if sk > 0.01:
|
|
238
|
+
dot = d / 2.0 * max(0.01, sk)
|
|
239
|
+
turn = E.eased(t, tc, STRIKE * 0.6)
|
|
240
|
+
if not card:
|
|
241
|
+
cv.circle(mx, cy, dot + 2.5, fill=(0, 0, 0, 120))
|
|
242
|
+
if turn < 1:
|
|
243
|
+
cv.circle(mx, cy, dot, fill=empty if card else (255, 255, 255, 230))
|
|
244
|
+
if turn > 0:
|
|
245
|
+
cv.circle(mx, cy, dot, fill="danger", opacity=turn)
|
|
246
|
+
# the text: slides in, then fades to half once crossed
|
|
247
|
+
a = E.eased(t, ts, ROW_IN, "out_cubic")
|
|
248
|
+
if cv.visible(a):
|
|
249
|
+
dim = 1.0 - (1.0 - DIM_TO) * E.eased(t, tc + DIM_AT, DIM, "out_quad")
|
|
250
|
+
dx = -min(L["gap"] * 0.8, 18.0) * (1.0 - a)
|
|
251
|
+
dy = 10.0 * (1.0 - a)
|
|
252
|
+
for k, (ln, sz) in enumerate(zip(row["lines"], row["sizes"])):
|
|
253
|
+
cv.text(tx + dx, y0 + row["base0"] + k * L["line_h"] + dy, ln, FONT, sz, color=ink,
|
|
254
|
+
opacity=a * dim, **outline)
|
|
255
|
+
if mode == "linha" and t >= tc:
|
|
256
|
+
p = E.eased(t, tc, STRIKE, "in_out_cubic")
|
|
257
|
+
total = sum(s["length"] for s in row["strokes"]) or 1.0
|
|
258
|
+
done = 0.0
|
|
259
|
+
for s in row["strokes"]:
|
|
260
|
+
part = E.clamp01((p * total - done) / max(1e-6, s["length"]))
|
|
261
|
+
done += s["length"]
|
|
262
|
+
if part <= 0:
|
|
263
|
+
break
|
|
264
|
+
pts = [(tx + u, y0 + v) for u, v in s["points"]]
|
|
265
|
+
n = len(pts)
|
|
266
|
+
widths = [s["width"] * (0.78 + 0.22 * math.sin(math.pi * min(1.0, k / (n - 1) * 1.15)))
|
|
267
|
+
for k in range(n)]
|
|
268
|
+
cv.polyline(pts, s["width"], color="danger", progress=part, widths=widths, shadow=strike_shadow)
|
|
269
|
+
return cv
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
273
|
+
_, crossed = _times(params)
|
|
274
|
+
mode = params["mode"]
|
|
275
|
+
cues = [{"at": 0.0, "name": "pop", "why": "entrada do cartão"}]
|
|
276
|
+
for i, tc in enumerate(crossed):
|
|
277
|
+
if mode == "x":
|
|
278
|
+
cues.append({"at": tc + SOUND_AT["x"], "name": "error", "volume": 0.7, "why": f"X no item {i + 1}"})
|
|
279
|
+
else:
|
|
280
|
+
cues.append({"at": tc + SOUND_AT["linha"], "name": "tick", "volume": 1.2, "why": f"risco no item {i + 1}"})
|
|
281
|
+
return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _end(params: dict) -> float:
|
|
285
|
+
shown, crossed = _times(params)
|
|
286
|
+
n = len(params["items"])
|
|
287
|
+
last_cross = max(crossed)
|
|
288
|
+
return max(ENTER, SKELETON + 0.05 * (n - 1) + 0.34, shown[-1] + ROW_IN,
|
|
289
|
+
last_cross + max(STRIKE, X_POP, 0.05 + X_DRAW, DIM_AT + DIM))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def suggested_duration(params: dict) -> float:
|
|
293
|
+
"""Every item crossed and about 1.2 s to take it in."""
|
|
294
|
+
return round(max(TEMPLATE["default_duration"], _end(params) + 1.2), 2)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def settle(params: dict, fps: int) -> float:
|
|
298
|
+
return _end(params) + 0.05
|