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,222 @@
|
|
|
1
|
+
"""contador: a number that counts up (or down) to its value, with prefix, suffix and a label.
|
|
2
|
+
|
|
3
|
+
REFERENCE TEMPLATE. Copy this file to write a new one (SPEC Contract 12):
|
|
4
|
+
1. TEMPLATE describes the template (id = file name) and its params; the registry checks it.
|
|
5
|
+
2. render(frame_index, fps, params, box) draws ONE frame on a fresh Canvas and returns it.
|
|
6
|
+
Time is frame_index / fps. `params` is already complete and checked. `box` gives the place
|
|
7
|
+
(design px, 1080 wide), the theme colors and the item length (box.frames).
|
|
8
|
+
3. Optional: sfx(params, fps, frames), suggested_duration(params), settle(params, fps).
|
|
9
|
+
Keep the layout math in a cached function (it runs once per item, not once per frame).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
|
|
17
|
+
from motion import engine as E
|
|
18
|
+
|
|
19
|
+
TEMPLATE = {
|
|
20
|
+
"id": "contador",
|
|
21
|
+
"label_pt": "Contador de número",
|
|
22
|
+
"description_pt": "Um número que sobe até o valor final num cartão, com prefixo (R$), sufixo (%, x, pessoas) "
|
|
23
|
+
"e uma legenda curta embaixo. Uma barra acompanha a contagem e o número dá um pulinho no fim.",
|
|
24
|
+
"use_when_pt": "Quando a fala cita um número que precisa ficar na cabeça: quantidade de alunos, porcentagem, "
|
|
25
|
+
"tempo, preço, faturamento. Use o número que a pessoa disse ou mostrou, sem inventar.",
|
|
26
|
+
"category": "números",
|
|
27
|
+
"version": 1,
|
|
28
|
+
"params": {
|
|
29
|
+
"value": {"type": "number", "default": 1000, "min": -1e13, "max": 1e13,
|
|
30
|
+
"label_pt": "Número final (use ponto para decimais: 1904152.87)"},
|
|
31
|
+
"start_value": {"type": "number", "default": 0, "min": -1e13, "max": 1e13,
|
|
32
|
+
"label_pt": "Número onde a contagem começa"},
|
|
33
|
+
"prefix": {"type": "text", "default": "", "max_len": 6, "label_pt": "Antes do número, ex.: R$ ou +"},
|
|
34
|
+
"suffix": {"type": "text", "default": "", "max_len": 12, "label_pt": "Depois do número, ex.: %, x ou pessoas"},
|
|
35
|
+
"decimals": {"type": "integer", "default": None, "nullable": True, "min": 0, "max": 3,
|
|
36
|
+
"label_pt": "Casas decimais (vazio: automático; dinheiro com centavos usa 2)"},
|
|
37
|
+
"label": {"type": "text", "default": "", "max_len": 40, "label_pt": "Legenda curta embaixo do número"},
|
|
38
|
+
"duration": {"type": "number", "default": 1.4, "min": 0.3, "max": 10,
|
|
39
|
+
"label_pt": "Segundos da contagem"},
|
|
40
|
+
"delay": {"type": "number", "default": 0.15, "min": 0, "max": 5,
|
|
41
|
+
"label_pt": "Segundos antes de começar a contar"},
|
|
42
|
+
"ease": {"type": "choice", "choices": ["suave", "linear"], "default": "suave",
|
|
43
|
+
"label_pt": "Ritmo: suave (rápido e freia no fim) ou linear (constante, bom para tempo)"},
|
|
44
|
+
"size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
|
|
45
|
+
"label_pt": "Tamanho do número"},
|
|
46
|
+
"card": {"type": "bool", "default": True, "label_pt": "Cartão atrás do número (sem cartão: contorno e sombra)"},
|
|
47
|
+
"bar": {"type": "bool", "default": True, "label_pt": "Barra que enche junto com a contagem"},
|
|
48
|
+
},
|
|
49
|
+
"default_duration": 3.0,
|
|
50
|
+
"default_region": "centro",
|
|
51
|
+
"sfx": [{"at": 0.0, "name": "pop"}, {"at": 0.15, "name": "count", "volume": 1.3}],
|
|
52
|
+
"example": {"value": 1250, "suffix": "alunos", "label": "na primeira turma"},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
LAND = 0.32 # seconds of the landing pop
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _decimals(p: dict) -> int:
|
|
59
|
+
if p["decimals"] is not None:
|
|
60
|
+
return int(p["decimals"])
|
|
61
|
+
money = E.looks_like_money(p["prefix"], p["suffix"])
|
|
62
|
+
d = max(E.auto_decimals(p["value"]), E.auto_decimals(p["start_value"]))
|
|
63
|
+
return 2 if money and d else d
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _affix_scale(text: str) -> float:
|
|
67
|
+
"""Small symbols (%, x, +) sit almost at number size; words and currencies are smaller."""
|
|
68
|
+
return 0.82 if len(text.strip()) <= 1 else 0.52
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@lru_cache(maxsize=64)
|
|
72
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
73
|
+
p = json.loads(params_json)
|
|
74
|
+
dec = _decimals(p)
|
|
75
|
+
prefix, suffix = p["prefix"].strip(), p["suffix"].strip()
|
|
76
|
+
label = p["label"].strip()
|
|
77
|
+
card = p["card"]
|
|
78
|
+
theme = box.theme
|
|
79
|
+
pad_x = 64.0 if card else 12.0
|
|
80
|
+
max_size = 200.0 if p["size"] == "grande" else 150.0
|
|
81
|
+
|
|
82
|
+
widest = max((E.format_number(v, dec) for v in (p["value"], p["start_value"])),
|
|
83
|
+
key=lambda s: E.measure(s, "sans_black", 100, tabular=True).width)
|
|
84
|
+
|
|
85
|
+
def block(size: float) -> dict:
|
|
86
|
+
num_w = E.measure(widest, "sans_black", size, tabular=True).width
|
|
87
|
+
pre_size, suf_size = size * _affix_scale(prefix), size * _affix_scale(suffix)
|
|
88
|
+
pre_w = E.measure(prefix, "sans_black", pre_size).width if prefix else 0.0
|
|
89
|
+
suf_w = E.measure(suffix, "sans_black", suf_size).width if suffix else 0.0
|
|
90
|
+
gap_p = size * (0.02 if len(prefix) <= 1 else 0.14) if prefix else 0.0
|
|
91
|
+
gap_s = size * (0.02 if len(suffix) <= 1 else 0.14) if suffix else 0.0
|
|
92
|
+
return {"size": size, "num_w": num_w, "pre_size": pre_size, "suf_size": suf_size, "pre_w": pre_w,
|
|
93
|
+
"suf_w": suf_w, "gap_p": gap_p, "gap_s": gap_s,
|
|
94
|
+
"width": pre_w + gap_p + num_w + gap_s + suf_w}
|
|
95
|
+
|
|
96
|
+
avail = box.w - 2 * pad_x
|
|
97
|
+
b = block(max_size)
|
|
98
|
+
if b["width"] > avail:
|
|
99
|
+
b = block(max(44.0, max_size * avail / b["width"] * 0.995))
|
|
100
|
+
size = b["size"]
|
|
101
|
+
cap = E.measure("0", "sans_black", size).cap
|
|
102
|
+
label_size = max(36.0, min(54.0, size * 0.40))
|
|
103
|
+
if label:
|
|
104
|
+
label_size = E.fit_text(label, "text_semibold", avail, label_size, 24.0)
|
|
105
|
+
label_cap = E.measure("H", "text_semibold", label_size).cap if label else 0.0
|
|
106
|
+
bar_h = max(8.0, size * 0.07) if p["bar"] else 0.0
|
|
107
|
+
gap_bar = size * 0.30 if p["bar"] else 0.0
|
|
108
|
+
gap_label = (size * 0.26 if p["bar"] else size * 0.30) if label else 0.0
|
|
109
|
+
pad_y = max(52.0, size * 0.50) if card else 0.0
|
|
110
|
+
content_h = cap + gap_bar + bar_h + gap_label + label_cap + (label_size * 0.25 if label else 0.0)
|
|
111
|
+
card_h = content_h + 2 * pad_y
|
|
112
|
+
card_w = min(box.w, max(b["width"], E.measure(label, "text_semibold", label_size).width if label else 0.0)
|
|
113
|
+
+ 2 * pad_x)
|
|
114
|
+
top = (box.h - card_h) / 2.0
|
|
115
|
+
x0 = (box.w - b["width"]) / 2.0
|
|
116
|
+
baseline = top + pad_y + cap
|
|
117
|
+
bar_y = baseline + gap_bar
|
|
118
|
+
number_color = "accent"
|
|
119
|
+
if card and E.contrast(theme.accent, theme.card) < 2.4:
|
|
120
|
+
number_color = "text" # a light accent on a white card: the bar keeps the accent
|
|
121
|
+
return {
|
|
122
|
+
"dec": dec, "prefix": prefix, "suffix": suffix, "label": label, "card": card,
|
|
123
|
+
"size": size, "cap": cap, "block": b, "x0": x0, "baseline": baseline,
|
|
124
|
+
"bar_y": bar_y, "bar_h": bar_h, "label_size": label_size,
|
|
125
|
+
"label_base": bar_y + bar_h + gap_label + label_cap,
|
|
126
|
+
"card_box": ((box.w - card_w) / 2.0, top, card_w, card_h), "number_color": number_color,
|
|
127
|
+
"radius": min(48.0, card_h * 0.22),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _count(p: dict, t: float) -> float:
|
|
132
|
+
fn = "linear" if p["ease"] == "linear" else "out_expo"
|
|
133
|
+
return E.eased(t, p["delay"], p["duration"], fn)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
137
|
+
cv = E.Canvas(box)
|
|
138
|
+
t = frame_index / fps
|
|
139
|
+
L = _layout(json.dumps(params, sort_keys=True), box)
|
|
140
|
+
b = L["block"]
|
|
141
|
+
t_end = params["delay"] + params["duration"]
|
|
142
|
+
|
|
143
|
+
# entrance of the whole card: fade, rise and a small overshoot. It starts one frame early, so
|
|
144
|
+
# frame 0 (where the entrance sound lands) already shows the card.
|
|
145
|
+
t_in = t + 1.0 / fps
|
|
146
|
+
alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
|
|
147
|
+
scale = 0.88 + 0.12 * E.out_back(E.progress(t_in, 0.0, 0.5), 1.4)
|
|
148
|
+
rise = 36.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
|
|
149
|
+
with cv.group(opacity=alpha, scale=scale, dy=rise):
|
|
150
|
+
cx0, cy0, cw, ch = L["card_box"]
|
|
151
|
+
if L["card"]:
|
|
152
|
+
cv.card(cx0, cy0, cw, ch, radius=L["radius"])
|
|
153
|
+
|
|
154
|
+
p = _count(params, t)
|
|
155
|
+
value = params["start_value"] + (params["value"] - params["start_value"]) * p
|
|
156
|
+
digits = E.format_number(value, L["dec"])
|
|
157
|
+
size = L["size"]
|
|
158
|
+
prefix, pre_w = L["prefix"], b["pre_w"]
|
|
159
|
+
if prefix and digits.startswith("-"):
|
|
160
|
+
# a negative amount is written -R$ 37,00, not R$ -37,00
|
|
161
|
+
prefix, digits = ("-" if prefix == "+" else "-" + prefix), digits[1:]
|
|
162
|
+
pre_w = E.measure(prefix, "sans_black", b["pre_size"]).width
|
|
163
|
+
num_w = E.measure(digits, "sans_black", size, tabular=True).width
|
|
164
|
+
# the line is centered on what it shows now; tabular digits keep it still once the digit count is final
|
|
165
|
+
x_pre = (cv.w - (pre_w + b["gap_p"] + num_w + b["gap_s"] + b["suf_w"])) / 2.0
|
|
166
|
+
x_num = x_pre + pre_w + b["gap_p"]
|
|
167
|
+
base = L["baseline"]
|
|
168
|
+
outline = {} if L["card"] else {"stroke": size * 0.05, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
|
|
169
|
+
|
|
170
|
+
land = E.pulse(t, t_end - 0.02, LAND)
|
|
171
|
+
pivot = (cv.cx, base - L["cap"] / 2.0)
|
|
172
|
+
|
|
173
|
+
def number() -> None:
|
|
174
|
+
cv.text(x_num, base, digits, "sans_black", size, color=L["number_color"], tabular=True, **outline)
|
|
175
|
+
if prefix:
|
|
176
|
+
cv.text(x_pre, base, prefix, "sans_black", b["pre_size"], color="text", opacity=0.92, **outline)
|
|
177
|
+
if L["suffix"]:
|
|
178
|
+
cv.text(x_num + num_w + b["gap_s"], base, L["suffix"], "sans_black", b["suf_size"], color="text",
|
|
179
|
+
opacity=0.92, **outline)
|
|
180
|
+
|
|
181
|
+
with cv.group(scale=1.0 + 0.07 * land, pivot=pivot):
|
|
182
|
+
if land > 0.02:
|
|
183
|
+
cv.glow(number, radius=30, opacity=0.5 * land)
|
|
184
|
+
else:
|
|
185
|
+
number()
|
|
186
|
+
|
|
187
|
+
if L["bar_h"]:
|
|
188
|
+
bw, bh = b["width"], L["bar_h"]
|
|
189
|
+
bx = L["x0"]
|
|
190
|
+
track = "track" if L["card"] else (0, 0, 0, 110)
|
|
191
|
+
cv.pill(bx, L["bar_y"], bw, bh, fill=track)
|
|
192
|
+
if p > 0:
|
|
193
|
+
cv.pill(bx, L["bar_y"], max(bh, bw * p), bh, fill="accent")
|
|
194
|
+
|
|
195
|
+
if L["label"]:
|
|
196
|
+
la = E.eased(t, 0.12, 0.35)
|
|
197
|
+
if cv.visible(la):
|
|
198
|
+
cv.text(cv.cx, L["label_base"] + 14.0 * (1.0 - la), L["label"], "text_semibold", L["label_size"],
|
|
199
|
+
color="muted" if L["card"] else "text", opacity=la, anchor="ms",
|
|
200
|
+
**({} if L["card"] else {"stroke": L["label_size"] * 0.08, "stroke_color": "black"}))
|
|
201
|
+
return cv
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
205
|
+
cues = [{"at": 0.0, "name": "pop", "why": "entrada"}]
|
|
206
|
+
if params["value"] != params["start_value"]:
|
|
207
|
+
start, dur = params["delay"], params["duration"]
|
|
208
|
+
# the ticking follows the count: once for the fast "suave" count, every 0.6 s for a linear one
|
|
209
|
+
steps = 1 if params["ease"] != "linear" else max(1, int(dur / 0.6 + 0.5))
|
|
210
|
+
for k in range(steps):
|
|
211
|
+
cues.append({"at": start + k * 0.6, "name": "count", "volume": 1.3, "why": "contagem"})
|
|
212
|
+
return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def suggested_duration(params: dict) -> float:
|
|
216
|
+
"""Count, landing, and about 1.5 s to read the number."""
|
|
217
|
+
return round(max(TEMPLATE["default_duration"], params["delay"] + params["duration"] + LAND + 1.5), 2)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def settle(params: dict, fps: int) -> float:
|
|
221
|
+
"""From here on every frame is the same (until the exit)."""
|
|
222
|
+
return params["delay"] + params["duration"] + LAND + 0.05
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""destaque: a marker stroke that draws itself at a place on the screen: a circle around it, an underline
|
|
2
|
+
under it or an arrow pointing at it, with an optional handwritten note.
|
|
3
|
+
|
|
4
|
+
The place is `target` = [x, y, w, h] in design px of the output canvas (1080x1920 for vertical video),
|
|
5
|
+
measured on a frame of the video. The template never moves (`movable: False`), draws on the whole safe
|
|
6
|
+
area ("tela") and uses a large bleed so a target near the edges is still covered.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
|
|
15
|
+
from motion import engine as E
|
|
16
|
+
from motion.engine import MotionError
|
|
17
|
+
|
|
18
|
+
TEMPLATE = {
|
|
19
|
+
"id": "destaque",
|
|
20
|
+
"label_pt": "Destaque desenhado",
|
|
21
|
+
"description_pt": "Um traço de caneta que se desenha sozinho num ponto da tela: círculo em volta, sublinhado "
|
|
22
|
+
"embaixo ou seta apontando para a área escolhida. Pode ter um texto escrito à mão ao lado.",
|
|
23
|
+
"use_when_pt": "Para levar o olho a algo que já aparece no vídeo: um preço, um botão, um número num print, uma "
|
|
24
|
+
"palavra na tela. Meça a área num quadro do vídeo (pixels da tela 1080x1920) e passe em target.",
|
|
25
|
+
"notes_pt": "Fica exatamente no alvo e não sai do lugar para fugir do rosto. Se o alvo se mexe no vídeo, o "
|
|
26
|
+
"traço não acompanha: use num trecho parado. A cor do traço é a cor de destaque do vídeo; troque "
|
|
27
|
+
"só neste item com cor (ex.: vermelho).",
|
|
28
|
+
"category": "marcações",
|
|
29
|
+
"version": 1,
|
|
30
|
+
"params": {
|
|
31
|
+
"target": {"type": "box", "default": [290, 1040, 500, 160],
|
|
32
|
+
"label_pt": "Área a destacar: [x, y, largura, altura] em pixels da tela 1080x1920"},
|
|
33
|
+
"mode": {"type": "choice", "choices": ["circulo", "sublinhado", "seta"], "default": "circulo",
|
|
34
|
+
"label_pt": "Traço: círculo em volta, sublinhado embaixo ou seta apontando"},
|
|
35
|
+
"side": {"type": "choice", "choices": ["auto", "baixo", "cima", "esquerda", "direita"], "default": "auto",
|
|
36
|
+
"label_pt": "Só para a seta: de que lado ela vem (auto: o lado com mais espaço na tela; sem "
|
|
37
|
+
"espaço no lado pedido, usa o de mais espaço)"},
|
|
38
|
+
"label": {"type": "text", "default": "", "max_len": 22,
|
|
39
|
+
"label_pt": "Texto escrito à mão junto do traço (opcional), ex.: olha isso"},
|
|
40
|
+
"thickness": {"type": "number", "default": 12, "min": 4, "max": 32,
|
|
41
|
+
"label_pt": "Espessura do traço em pixels"},
|
|
42
|
+
"duration": {"type": "number", "default": 0.6, "min": 0.2, "max": 3,
|
|
43
|
+
"label_pt": "Segundos para desenhar o traço"},
|
|
44
|
+
"shadow": {"type": "bool", "default": True,
|
|
45
|
+
"label_pt": "Sombra leve atrás do traço (ajuda a ler em fundo claro)"},
|
|
46
|
+
},
|
|
47
|
+
"default_duration": 2.0,
|
|
48
|
+
"default_region": "tela",
|
|
49
|
+
"movable": False,
|
|
50
|
+
"bleed": 160,
|
|
51
|
+
"sfx": [{"at": 0.15, "name": "whoosh", "volume": 0.7}],
|
|
52
|
+
"example": {"target": [240, 900, 600, 200], "mode": "circulo", "label": "olha isso"},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
LABEL_FONT = "marker"
|
|
56
|
+
LABEL_SIZE = 72.0
|
|
57
|
+
EDGE = 14.0 # the stroke keeps this far from the screen edges
|
|
58
|
+
LABEL_IN = 0.32
|
|
59
|
+
SIDES = ("baixo", "cima", "esquerda", "direita")
|
|
60
|
+
MIN_ROOM = 90.0 # a forced arrow side with less room than this falls back to the side with the most room
|
|
61
|
+
LABEL_APPEAR = {"circulo": 0.55, "sublinhado": 0.7, "seta": 0.0} # fraction of the draw time
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _smooth(x: float) -> float:
|
|
65
|
+
x = E.clamp01(x)
|
|
66
|
+
return x * x * (3 - 2 * x)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _draw_curve(u: float) -> float:
|
|
70
|
+
"""A hand stroke: leaves fast and eases into the end."""
|
|
71
|
+
return 0.6 * E.out_cubic(u) + 0.4 * E.in_out_cubic(u)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _taper(n: int, start: float = 0.55, end: float = 0.55) -> list[float]:
|
|
75
|
+
"""Width factor per point: thin at the pen-down, full in the middle, thinning at the lift."""
|
|
76
|
+
out = []
|
|
77
|
+
for k in range(n):
|
|
78
|
+
u = k / max(1, n - 1)
|
|
79
|
+
out.append((start + (1 - start) * _smooth(u / 0.14)) * (1 - (1 - end) * _smooth((u - 0.82) / 0.18)))
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _rotate(points, cx: float, cy: float, deg: float):
|
|
84
|
+
a = math.radians(deg)
|
|
85
|
+
c, s = math.cos(a), math.sin(a)
|
|
86
|
+
return [(cx + (x - cx) * c - (y - cy) * s, cy + (x - cx) * s + (y - cy) * c) for x, y in points]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _check_target(target, design) -> None:
|
|
90
|
+
x, y, w, h = target
|
|
91
|
+
dw, dh = design
|
|
92
|
+
if x + w <= 0 or y + h <= 0 or x >= dw or y >= dh:
|
|
93
|
+
raise MotionError(f"destaque.params.target [{x:g}, {y:g}, {w:g}, {h:g}] fica fora da tela "
|
|
94
|
+
f"({dw:.0f}x{dh:.0f}); meça a área num quadro do vídeo")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _pick_side(side: str, target, safe) -> tuple[str, dict]:
|
|
98
|
+
x, y, w, h = target
|
|
99
|
+
sx0, sy0, sx1, sy1 = safe
|
|
100
|
+
room = {"baixo": sy1 - (y + h), "cima": y - sy0, "esquerda": x - sx0, "direita": sx1 - (x + w)}
|
|
101
|
+
if side != "auto" and room[side] >= MIN_ROOM:
|
|
102
|
+
return side, room
|
|
103
|
+
# vertical arrows read more naturally on a phone: horizontal ones need clearly more room to win
|
|
104
|
+
weight = {"baixo": 1.0, "cima": 0.95, "esquerda": 0.8, "direita": 0.8}
|
|
105
|
+
return max(SIDES, key=lambda k: (room[k] * weight[k], -SIDES.index(k))), room
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@lru_cache(maxsize=64)
|
|
109
|
+
def _geometry(params_json: str, box: E.Box) -> dict:
|
|
110
|
+
"""Stroke paths and the label place, in CANVAS design px (converted to box-local when drawing)."""
|
|
111
|
+
p = json.loads(params_json)
|
|
112
|
+
target = p["target"]
|
|
113
|
+
dw, dh = box.design
|
|
114
|
+
_check_target(target, (dw, dh))
|
|
115
|
+
tx, ty, tw, th = target
|
|
116
|
+
cx, cy = tx + tw / 2.0, ty + th / 2.0
|
|
117
|
+
thick = p["thickness"]
|
|
118
|
+
seed = E.stable_seed("destaque", tuple(target), p["mode"])
|
|
119
|
+
safe = E.safe_area(box.canvas)
|
|
120
|
+
label = p["label"].strip()
|
|
121
|
+
lsize = 0.0
|
|
122
|
+
lw = lcap = 0.0
|
|
123
|
+
if label:
|
|
124
|
+
lsize = E.fit_text(label, LABEL_FONT, (safe[2] - safe[0]) * 0.9, LABEL_SIZE, 30.0)
|
|
125
|
+
m = E.measure(label, LABEL_FONT, lsize)
|
|
126
|
+
lw, lcap = m.width, m.cap
|
|
127
|
+
strokes = [] # (points, width factors, start fraction, end fraction of the draw time)
|
|
128
|
+
label_at = None # (x center, y baseline, rotation, appear time fraction)
|
|
129
|
+
|
|
130
|
+
if p["mode"] == "circulo":
|
|
131
|
+
pad = thick * 0.5 + 14.0
|
|
132
|
+
rx = tw / 2.0 * 1.22 + pad
|
|
133
|
+
ry = th / 2.0 * 1.22 + pad
|
|
134
|
+
rx = min(rx, max(tw / 2.0 + thick, min(cx, dw - cx) - EDGE))
|
|
135
|
+
ry = min(ry, max(th / 2.0 + thick, min(cy, dh - cy) - EDGE))
|
|
136
|
+
pts = E.ellipse_points(cx, cy, rx, ry, start_deg=-128.0, sweep_deg=382.0, n=72, wobble=0.035, seed=seed)
|
|
137
|
+
pts = _rotate(pts, cx, cy, -3.0)
|
|
138
|
+
strokes.append((pts, _taper(len(pts), 0.5, 0.45), 0.0, 1.0))
|
|
139
|
+
if label:
|
|
140
|
+
above = cy - ry - 22.0
|
|
141
|
+
if above - lcap - 30.0 >= safe[1]:
|
|
142
|
+
label_at = (cx + rx * 0.25, above, -5.0, LABEL_APPEAR["circulo"])
|
|
143
|
+
else:
|
|
144
|
+
label_at = (cx + rx * 0.25, cy + ry + 22.0 + lcap, -5.0, LABEL_APPEAR["circulo"])
|
|
145
|
+
elif p["mode"] == "sublinhado":
|
|
146
|
+
gap = thick * 0.6 + 12.0
|
|
147
|
+
x0 = max(EDGE, tx - tw * 0.03)
|
|
148
|
+
x1 = min(dw - EDGE, tx + tw * 1.03)
|
|
149
|
+
rise = min(24.0, (x1 - x0) * 0.045)
|
|
150
|
+
y = ty + th + gap + rise # the rising end stays under the target
|
|
151
|
+
pts = E.bezier_points((x0, y + rise * 0.15), ((x0 + x1) / 2.0, y + rise * 0.75), (x1, y - rise), n=40)
|
|
152
|
+
pts = E.wobble_points(pts, max(0.8, thick * 0.10), seed=seed, step=14.0)
|
|
153
|
+
strokes.append((pts, _taper(len(pts), 0.55, 0.5), 0.0, 0.78))
|
|
154
|
+
# a second, shorter swash under the first: the classic marker emphasis
|
|
155
|
+
y2 = y + thick * 1.35 + 6.0
|
|
156
|
+
a2, b2 = x0 + (x1 - x0) * 0.10, x0 + (x1 - x0) * 0.86
|
|
157
|
+
pts2 = E.bezier_points((a2, y2 + rise * 0.1), ((a2 + b2) / 2.0, y2 + rise * 0.55), (b2, y2 - rise * 0.7), n=30)
|
|
158
|
+
pts2 = E.wobble_points(pts2, max(0.8, thick * 0.08), seed=seed + 1, step=14.0)
|
|
159
|
+
strokes.append((pts2, [w * 0.72 for w in _taper(len(pts2), 0.55, 0.45)], 0.62, 1.0))
|
|
160
|
+
if label:
|
|
161
|
+
below = y2 + thick + 22.0 + lcap
|
|
162
|
+
if below + 20.0 <= safe[3]:
|
|
163
|
+
label_at = (cx, below, -4.0, LABEL_APPEAR["sublinhado"])
|
|
164
|
+
else:
|
|
165
|
+
label_at = (cx, ty - 24.0, -4.0, LABEL_APPEAR["sublinhado"])
|
|
166
|
+
else:
|
|
167
|
+
side, room = _pick_side(p["side"], target, safe)
|
|
168
|
+
g = 16.0 + thick
|
|
169
|
+
label_room = (lcap + 40.0) if label else 10.0
|
|
170
|
+
length = max(110.0, min(300.0, room[side] - g - label_room))
|
|
171
|
+
right_half = cx > dw / 2.0
|
|
172
|
+
if side in ("baixo", "cima"):
|
|
173
|
+
sign = 1.0 if side == "baixo" else -1.0
|
|
174
|
+
tip = (cx, (ty + th + g) if side == "baixo" else (ty - g))
|
|
175
|
+
lateral = -1.0 if right_half else 1.0
|
|
176
|
+
tail = (tip[0] + lateral * length * 0.42, tip[1] + sign * length)
|
|
177
|
+
else:
|
|
178
|
+
sign = -1.0 if side == "esquerda" else 1.0
|
|
179
|
+
tip = ((tx - g) if side == "esquerda" else (tx + tw + g), cy)
|
|
180
|
+
down = 1.0 if room["baixo"] >= room["cima"] else -1.0
|
|
181
|
+
tail = (tip[0] + sign * length, tip[1] + down * length * 0.38)
|
|
182
|
+
# keep the tail on the screen (shorten along the arrow)
|
|
183
|
+
tail = (min(max(tail[0], safe[0] + EDGE), safe[2] - EDGE), min(max(tail[1], EDGE), dh - EDGE))
|
|
184
|
+
L = math.dist(tail, tip)
|
|
185
|
+
mx, my = (tail[0] + tip[0]) / 2.0, (tail[1] + tip[1]) / 2.0
|
|
186
|
+
nx, ny = -(tip[1] - tail[1]) / max(L, 1e-6), (tip[0] - tail[0]) / max(L, 1e-6)
|
|
187
|
+
bend = 0.22 * L * (1.0 if (side in ("baixo", "direita")) != right_half else -1.0)
|
|
188
|
+
shaft = E.bezier_points(tail, (mx + nx * bend, my + ny * bend), tip, n=40)
|
|
189
|
+
shaft = E.wobble_points(shaft, max(0.6, thick * 0.08), seed=seed, step=16.0)
|
|
190
|
+
shaft[-1] = tip
|
|
191
|
+
strokes.append((shaft, _taper(len(shaft), 0.6, 1.0), 0.0, 0.72))
|
|
192
|
+
(x_a, y_a), (x_b, y_b) = shaft[-4], shaft[-1]
|
|
193
|
+
ang = math.atan2(y_b - y_a, x_b - x_a)
|
|
194
|
+
hl = max(thick * 3.4, 40.0)
|
|
195
|
+
w1 = (tip[0] + hl * math.cos(ang + math.pi - math.radians(30)), tip[1] + hl * math.sin(ang + math.pi - math.radians(30)))
|
|
196
|
+
w2 = (tip[0] + hl * math.cos(ang + math.pi + math.radians(33)), tip[1] + hl * math.sin(ang + math.pi + math.radians(33)))
|
|
197
|
+
strokes.append(([w1, tip, w2], [0.8, 1.0, 0.85], 0.66, 1.0))
|
|
198
|
+
if label:
|
|
199
|
+
# on the far side of the tail from the shaft, nudged away from the tip on horizontal arrows
|
|
200
|
+
lx = tail[0] if side in ("baixo", "cima") else tail[0] + sign * lw * 0.25
|
|
201
|
+
if tail[1] < tip[1] - 1.0:
|
|
202
|
+
label_at = (lx, tail[1] - 24.0, -4.0, LABEL_APPEAR["seta"])
|
|
203
|
+
else:
|
|
204
|
+
label_at = (lx, tail[1] + 22.0 + lcap, -4.0, LABEL_APPEAR["seta"])
|
|
205
|
+
if label_at is not None:
|
|
206
|
+
lx, ly, rot, appear = label_at
|
|
207
|
+
lx = min(max(lx, safe[0] + lw / 2.0 + 6.0), safe[2] - lw / 2.0 - 6.0)
|
|
208
|
+
ly = min(max(ly, EDGE + lcap), dh - EDGE)
|
|
209
|
+
label_at = (lx, ly, rot, appear)
|
|
210
|
+
return {"strokes": strokes, "label": label, "label_size": lsize, "label_cap": lcap, "label_at": label_at,
|
|
211
|
+
"thick": thick}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
215
|
+
cv = E.Canvas(box)
|
|
216
|
+
t = frame_index / fps
|
|
217
|
+
t_in = t + 1.0 / fps
|
|
218
|
+
G = _geometry(json.dumps(params, sort_keys=True), box)
|
|
219
|
+
dur = params["duration"]
|
|
220
|
+
shadow = E.Shadow(blur=8.0, dy=3.0, opacity=0.45) if params["shadow"] else None
|
|
221
|
+
thick = G["thick"]
|
|
222
|
+
u = E.clamp01(t_in / dur)
|
|
223
|
+
for pts, widths, a, b in G["strokes"]:
|
|
224
|
+
prog = _draw_curve(E.remap(u, a, b))
|
|
225
|
+
if prog <= 0:
|
|
226
|
+
continue
|
|
227
|
+
local = [box.from_canvas(x, y) for x, y in pts]
|
|
228
|
+
cv.polyline(local, thick, color="accent", progress=prog, widths=[thick * w for w in widths],
|
|
229
|
+
shadow=shadow)
|
|
230
|
+
if G["label_at"] is not None:
|
|
231
|
+
lx, ly, rot, appear = G["label_at"]
|
|
232
|
+
la = E.progress(t_in, appear * dur, LABEL_IN)
|
|
233
|
+
if la > 0:
|
|
234
|
+
x, y = box.from_canvas(lx, ly)
|
|
235
|
+
pivot = (x, y - G["label_cap"] / 2.0)
|
|
236
|
+
with cv.group(opacity=E.out_quad(min(1.0, la * 1.6)), scale=0.85 + 0.15 * E.out_back(la, 2.0),
|
|
237
|
+
pivot=pivot, rotate=rot):
|
|
238
|
+
cv.text(x, y, G["label"], LABEL_FONT, G["label_size"], color="accent", anchor="ms",
|
|
239
|
+
stroke=4.0, stroke_color=(0, 0, 0, 150),
|
|
240
|
+
shadow=E.Shadow(blur=8.0, dy=3.0, opacity=0.5) if params["shadow"] else None)
|
|
241
|
+
return cv
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
245
|
+
dur = params["duration"]
|
|
246
|
+
if params["mode"] == "seta":
|
|
247
|
+
cues = [{"at": dur * 0.2, "name": "whoosh", "volume": 0.7, "why": "seta se desenhando"},
|
|
248
|
+
{"at": dur * 0.72, "name": "pop", "volume": 0.8, "why": "ponta da seta"}]
|
|
249
|
+
else:
|
|
250
|
+
cues = [{"at": dur * 0.25, "name": "whoosh", "volume": 0.7, "why": "traço se desenhando"}]
|
|
251
|
+
return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def suggested_duration(params: dict) -> float:
|
|
255
|
+
return round(max(TEMPLATE["default_duration"], params["duration"] + LABEL_IN + 1.4), 2)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def settle(params: dict, fps: int) -> float:
|
|
259
|
+
"""The drawing runs on t + 1/fps: it is complete one frame before `end`; half a frame of margin for rounding."""
|
|
260
|
+
end = params["duration"]
|
|
261
|
+
if params["label"].strip():
|
|
262
|
+
end = max(end, LABEL_APPEAR[params["mode"]] * params["duration"] + LABEL_IN)
|
|
263
|
+
return end - 0.5 / fps + 0.01
|