primocode 10.15.2 → 10.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +74 -9
- 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 +38 -8
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +7 -1
- 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/tela.html +1349 -0
- package/lib/estudio/gosto.js +305 -0
- package/lib/estudio/index.js +12 -1
- 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 +70 -12
- 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 +64 -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/headless.js +15 -3
- package/lib/local/cerebro.json +285 -7
- package/lib/local/motor.js +5 -2
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +33 -2
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +23 -4
- package/lib/ui.js +2 -2
- 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/__main__.py +55 -40
- package/voz/primo_voz/app.py +500 -4
- package/voz/primo_voz/chat.html +1773 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""checklist: a list on a card where each item slides in and gets a check mark, one after the other.
|
|
2
|
+
|
|
3
|
+
Frame 0 already shows the card, the title and the empty markers (the skeleton of the list), so the
|
|
4
|
+
entrance sound lands on something even when the first item comes later. Each item then slides in and its
|
|
5
|
+
marker fills with the accent and draws a check. After the last check the card border glows once.
|
|
6
|
+
Timing: `delay` + `stagger`, or `times` (one second per item, 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": "checklist",
|
|
19
|
+
"label_pt": "Lista com check",
|
|
20
|
+
"description_pt": "Uma lista num cartão em que cada item entra deslizando e ganha um check na cor de destaque, "
|
|
21
|
+
"um por vez. Pode ter um título em cima e o cartão brilha quando o último item é marcado.",
|
|
22
|
+
"use_when_pt": "Quando a fala enumera o que a pessoa recebe, faz ou ganha: acesso na hora, suporte, garantia. "
|
|
23
|
+
"Para cada item entrar junto com a fala, use times (segundos desde o começo do item).",
|
|
24
|
+
"notes_pt": "Itens curtos leem melhor (até 32 letras; textos longos quebram em duas linhas e a letra diminui). "
|
|
25
|
+
"Com 5 ou 6 itens, prefira a região centro ou tela.",
|
|
26
|
+
"category": "listas",
|
|
27
|
+
"version": 1,
|
|
28
|
+
"params": {
|
|
29
|
+
"items": {"type": "list", "item": {"type": "text", "max_len": 32, "required_text": True},
|
|
30
|
+
"min_items": 1, "max_items": 6,
|
|
31
|
+
"default": ["Acesso na hora", "Suporte no WhatsApp", "Garantia de 7 dias"],
|
|
32
|
+
"label_pt": "Itens da lista, de cima para baixo (1 a 6, cada um com até 32 letras)"},
|
|
33
|
+
"title": {"type": "text", "default": "", "max_len": 36,
|
|
34
|
+
"label_pt": "Título em cima da lista (opcional). *Palavra* entre asteriscos fica na cor de destaque"},
|
|
35
|
+
"delay": {"type": "number", "default": 0.25, "min": 0, "max": 30,
|
|
36
|
+
"label_pt": "Segundos até o primeiro item entrar (o cartão já aparece no começo)"},
|
|
37
|
+
"stagger": {"type": "number", "default": 0.45, "min": 0.1, "max": 5,
|
|
38
|
+
"label_pt": "Segundos entre um item e o próximo"},
|
|
39
|
+
"times": {"type": "list", "item": {"type": "number", "min": 0, "max": 60}, "nullable": True,
|
|
40
|
+
"default": None, "min_items": 1, "max_items": 6,
|
|
41
|
+
"label_pt": "Segundo em que cada item entra, contado do começo do item (um por item, em ordem "
|
|
42
|
+
"crescente). Vazio: usa delay e stagger"},
|
|
43
|
+
"mode": {"type": "choice", "choices": ["circulo", "caixa"], "default": "circulo",
|
|
44
|
+
"label_pt": "Marcador do check: círculo ou caixinha"},
|
|
45
|
+
"size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
|
|
46
|
+
"label_pt": "Tamanho do texto"},
|
|
47
|
+
"card": {"type": "bool", "default": True,
|
|
48
|
+
"label_pt": "Cartão atrás da lista (sem cartão: texto com contorno e sombra)"},
|
|
49
|
+
},
|
|
50
|
+
"default_duration": 3.0,
|
|
51
|
+
"default_region": "baixo",
|
|
52
|
+
"sfx": [{"at": 0.0, "name": "pop"}, {"at": 0.35, "name": "tick"}, {"at": 0.8, "name": "tick"},
|
|
53
|
+
{"at": 1.25, "name": "tick"}, {"at": 1.75, "name": "success", "volume": 0.8}],
|
|
54
|
+
"example": {"title": "O que você *recebe*:",
|
|
55
|
+
"items": ["Acesso na hora", "Suporte no WhatsApp", "Aulas curtas e diretas", "Garantia de 7 dias"]},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
FONT = "text_bold"
|
|
59
|
+
TITLE_FONT = "sans_black"
|
|
60
|
+
BASE_SIZE = {"normal": 56.0, "grande": 68.0}
|
|
61
|
+
MIN_SIZE = 34.0
|
|
62
|
+
|
|
63
|
+
SKELETON = -0.08 # the empty markers start before frame 0, so frame 0 always shows them
|
|
64
|
+
ENTER = 0.5 # card entrance (seconds)
|
|
65
|
+
ROW_IN = 0.32 # item slide and fade
|
|
66
|
+
CHECK_AT = 0.10 # the marker fills this long after the item starts entering
|
|
67
|
+
CHECK_POP = 0.40 # marker pop (out_back)
|
|
68
|
+
CHECK_DRAW = 0.26 # the check stroke draws
|
|
69
|
+
GLOW_AFTER = 0.30 # card border glow starts this long after the last check
|
|
70
|
+
GLOW = 0.55
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _times(p: dict) -> list[float]:
|
|
74
|
+
"""Entry second of every item (validated against the item count)."""
|
|
75
|
+
n = len(p["items"])
|
|
76
|
+
times = p.get("times")
|
|
77
|
+
if times is None:
|
|
78
|
+
return [p["delay"] + i * p["stagger"] for i in range(n)]
|
|
79
|
+
if len(times) != n:
|
|
80
|
+
raise MotionError(f"checklist.params.times precisa de um tempo para cada item ({n} itens, veio {len(times)})")
|
|
81
|
+
for i in range(1, n):
|
|
82
|
+
if times[i] < times[i - 1]:
|
|
83
|
+
raise MotionError(f"checklist.params.times precisa estar em ordem crescente (o item {i + 1} entra em "
|
|
84
|
+
f"{times[i]:g} s, antes do item {i} em {times[i - 1]:g} s)")
|
|
85
|
+
return [float(v) for v in times]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate(params: dict) -> None:
|
|
89
|
+
"""One time per item, in order: refused when the item is planned, not only when it renders."""
|
|
90
|
+
_times(params)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@lru_cache(maxsize=64)
|
|
94
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
95
|
+
p = json.loads(params_json)
|
|
96
|
+
items = p["items"]
|
|
97
|
+
card = p["card"]
|
|
98
|
+
title = p["title"].strip()
|
|
99
|
+
title_plain = " ".join(w for w, _ in E.parse_accent(title))
|
|
100
|
+
pad_x = 54.0 if card else 6.0
|
|
101
|
+
pad_y = 48.0 if card else 6.0
|
|
102
|
+
|
|
103
|
+
def build(size: float) -> tuple[dict, bool]:
|
|
104
|
+
marker = size * 1.04
|
|
105
|
+
gap = size * 0.44
|
|
106
|
+
text_w = box.w - 2 * pad_x - marker - gap
|
|
107
|
+
m = E.measure("H", FONT, size)
|
|
108
|
+
line_h = size * 1.17
|
|
109
|
+
row_gap = size * 0.46
|
|
110
|
+
rows, fits, y = [], True, 0.0
|
|
111
|
+
title_size = title_h = title_w = 0.0
|
|
112
|
+
if title:
|
|
113
|
+
title_size = min(size * 0.94, E.fit_text(title_plain, TITLE_FONT, box.w - 2 * pad_x, size * 0.94, 26.0))
|
|
114
|
+
title_cap = E.measure("H", TITLE_FONT, title_size).cap
|
|
115
|
+
title_w = E.measure(title_plain, TITLE_FONT, title_size).width
|
|
116
|
+
title_h = title_cap + size * 0.74
|
|
117
|
+
y = title_h
|
|
118
|
+
widest = 0.0
|
|
119
|
+
for k, text in enumerate(items):
|
|
120
|
+
lines = E.wrap_text(text, FONT, size, text_w, 2)
|
|
121
|
+
sizes = []
|
|
122
|
+
for ln in lines:
|
|
123
|
+
w = E.measure(ln, FONT, size).width
|
|
124
|
+
if w > text_w + 0.5:
|
|
125
|
+
fits = False
|
|
126
|
+
sizes.append(E.fit_text(ln, FONT, text_w, size, 20.0))
|
|
127
|
+
else:
|
|
128
|
+
sizes.append(size)
|
|
129
|
+
widths = [E.measure(ln, FONT, s).width for ln, s in zip(lines, sizes)]
|
|
130
|
+
widest = max(widest, max(widths))
|
|
131
|
+
base0 = y + marker / 2.0 + m.cap / 2.0
|
|
132
|
+
bottom = max(y + marker, base0 + (len(lines) - 1) * line_h)
|
|
133
|
+
rows.append({"lines": lines, "sizes": sizes, "widths": widths, "top": y,
|
|
134
|
+
"cy": y + marker / 2.0, "base0": base0})
|
|
135
|
+
y = bottom + (row_gap if k < len(items) - 1 else 0.0)
|
|
136
|
+
content_h = y
|
|
137
|
+
card_h = content_h + 2 * pad_y
|
|
138
|
+
content_w = max(marker + gap + widest, title_w)
|
|
139
|
+
card_w = min(box.w, max(content_w + 2 * pad_x, 380.0 if card else 0.0))
|
|
140
|
+
L = {"size": size, "marker": marker, "gap": gap, "line_h": line_h, "rows": rows,
|
|
141
|
+
"title_size": title_size, "title_base": E.measure("H", TITLE_FONT, title_size).cap if title else 0.0,
|
|
142
|
+
"card_w": card_w, "card_h": card_h, "pad_x": pad_x, "pad_y": pad_y}
|
|
143
|
+
return L, fits and card_h <= box.h
|
|
144
|
+
|
|
145
|
+
size = BASE_SIZE[p["size"]]
|
|
146
|
+
L, ok = build(size)
|
|
147
|
+
while not ok and size > MIN_SIZE:
|
|
148
|
+
size = max(MIN_SIZE, size * 0.95)
|
|
149
|
+
L, ok = build(size)
|
|
150
|
+
card_x = (box.w - L["card_w"]) / 2.0
|
|
151
|
+
card_y = (box.h - L["card_h"]) / 2.0
|
|
152
|
+
L["card_box"] = (card_x, card_y, L["card_w"], L["card_h"])
|
|
153
|
+
L["x0"] = card_x + pad_x
|
|
154
|
+
L["y0"] = card_y + pad_y
|
|
155
|
+
L["radius"] = min(40.0, L["card_h"] * 0.2)
|
|
156
|
+
theme = box.theme
|
|
157
|
+
# a light accent on a white card gets a darker rim so the filled marker keeps its edge
|
|
158
|
+
L["rim"] = card and E.contrast(theme.accent, theme.card) < 1.6
|
|
159
|
+
L["times"] = _times(p)
|
|
160
|
+
L["title"] = title
|
|
161
|
+
L["card"] = card
|
|
162
|
+
L["mode"] = p["mode"]
|
|
163
|
+
return L
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _marker(cv: E.Canvas, mode: str, cx: float, cy: float, d: float, filled: bool, color,
|
|
167
|
+
halo: bool = False, rim: bool = False) -> None:
|
|
168
|
+
"""Empty or filled marker. `halo` (no card): a dark edge so it reads on any video."""
|
|
169
|
+
w = d * 0.085
|
|
170
|
+
if mode == "caixa":
|
|
171
|
+
r = d * 0.26
|
|
172
|
+
if filled:
|
|
173
|
+
cv.rect(cx - d / 2, cy - d / 2, d, d, radius=r, fill=color,
|
|
174
|
+
shadow=E.Shadow(blur=10.0, dy=4.0, opacity=0.5) if halo else None,
|
|
175
|
+
stroke=(0, 0, 0, 40) if rim else None, stroke_width=2.0 if rim else 0.0)
|
|
176
|
+
return
|
|
177
|
+
if halo:
|
|
178
|
+
e = w * 0.9
|
|
179
|
+
cv.rect(cx - d / 2 - e, cy - d / 2 - e, d + 2 * e, d + 2 * e, radius=r + e, fill=None,
|
|
180
|
+
stroke=(0, 0, 0, 120), stroke_width=w + 2 * e)
|
|
181
|
+
cv.rect(cx - d / 2, cy - d / 2, d, d, radius=r, fill=None, stroke=color, stroke_width=w)
|
|
182
|
+
return
|
|
183
|
+
if filled:
|
|
184
|
+
cv.circle(cx, cy, d / 2, fill=color, shadow=E.Shadow(blur=10.0, dy=4.0, opacity=0.5) if halo else None,
|
|
185
|
+
stroke=(0, 0, 0, 40) if rim else None, stroke_width=2.0 if rim else 0.0)
|
|
186
|
+
return
|
|
187
|
+
if halo:
|
|
188
|
+
cv.ring(cx, cy, d / 2 - w / 2, w * 2.8, color=(0, 0, 0, 120))
|
|
189
|
+
cv.ring(cx, cy, d / 2 - w / 2, w, color=color)
|
|
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 = L["card"]
|
|
198
|
+
size, d = L["size"], L["marker"]
|
|
199
|
+
times = L["times"]
|
|
200
|
+
|
|
201
|
+
alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
|
|
202
|
+
scale = 0.9 + 0.1 * E.out_back(E.progress(t_in, 0.0, ENTER), 1.4)
|
|
203
|
+
rise = 32.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
|
|
204
|
+
outline = {} if card else {"stroke": size * 0.07, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
|
|
205
|
+
ink = "text" if card else "white" # no card: white letters with a black outline in both themes
|
|
206
|
+
halo = not card
|
|
207
|
+
empty_color = "track" if card else (255, 255, 255, 215)
|
|
208
|
+
|
|
209
|
+
with cv.group(opacity=alpha, scale=scale, dy=rise):
|
|
210
|
+
cx0, cy0, cw, ch = L["card_box"]
|
|
211
|
+
if card:
|
|
212
|
+
cv.card(cx0, cy0, cw, ch, radius=L["radius"])
|
|
213
|
+
glow = E.pulse(t, times[-1] + CHECK_AT + CHECK_POP * 0.5 + GLOW_AFTER, GLOW)
|
|
214
|
+
if glow > 0.01:
|
|
215
|
+
cv.rect(cx0, cy0, cw, ch, radius=L["radius"], fill=None, stroke="accent", stroke_width=3.0,
|
|
216
|
+
opacity=0.9 * glow)
|
|
217
|
+
x0, y0 = L["x0"], L["y0"]
|
|
218
|
+
if L["title"]:
|
|
219
|
+
cv.rich_text(x0, y0 + L["title_base"], L["title"], TITLE_FONT, L["title_size"], color=ink,
|
|
220
|
+
**outline)
|
|
221
|
+
mx = x0 + d / 2.0
|
|
222
|
+
tx = x0 + d + L["gap"]
|
|
223
|
+
for i, row in enumerate(L["rows"]):
|
|
224
|
+
ti = times[i]
|
|
225
|
+
cy = y0 + row["cy"]
|
|
226
|
+
# skeleton: the empty marker pops in with the card
|
|
227
|
+
sk = E.out_back(E.progress(t_in, SKELETON + 0.05 * i, 0.34), 1.8)
|
|
228
|
+
fill_p = E.progress(t, ti + CHECK_AT, CHECK_POP)
|
|
229
|
+
if sk > 0.01 and fill_p < 1.0:
|
|
230
|
+
with cv.group(scale=max(0.01, sk), pivot=(mx, cy)):
|
|
231
|
+
_marker(cv, L["mode"], mx, cy, d, False, empty_color, halo=halo)
|
|
232
|
+
if fill_p > 0:
|
|
233
|
+
pop = max(0.01, E.out_back(fill_p, 2.2))
|
|
234
|
+
with cv.group(scale=pop, pivot=(mx, cy)):
|
|
235
|
+
_marker(cv, L["mode"], mx, cy, d, True, "accent", halo=halo, rim=L["rim"])
|
|
236
|
+
draw = E.eased(t, ti + CHECK_AT + 0.05, CHECK_DRAW, "in_out_cubic")
|
|
237
|
+
if draw > 0:
|
|
238
|
+
cv.check(mx, cy + d * 0.02, d * 0.54, width=d * 0.12, color="on_accent", progress=draw)
|
|
239
|
+
# the text slides in from the marker
|
|
240
|
+
a = E.eased(t, ti, ROW_IN, "out_cubic")
|
|
241
|
+
if cv.visible(a):
|
|
242
|
+
dx = -min(L["gap"] * 0.8, 18.0) * (1.0 - a)
|
|
243
|
+
dy = 10.0 * (1.0 - a)
|
|
244
|
+
for k, (ln, sz) in enumerate(zip(row["lines"], row["sizes"])):
|
|
245
|
+
cv.text(tx + dx, y0 + row["base0"] + k * L["line_h"] + dy, ln, FONT, sz, color=ink,
|
|
246
|
+
opacity=a, **outline)
|
|
247
|
+
return cv
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
251
|
+
times = _times(params)
|
|
252
|
+
cues = [{"at": 0.0, "name": "pop", "why": "entrada do cartão"}]
|
|
253
|
+
for i, ti in enumerate(times):
|
|
254
|
+
cues.append({"at": ti + CHECK_AT, "name": "tick", "why": f"check do item {i + 1}"})
|
|
255
|
+
cues.append({"at": times[-1] + CHECK_AT + CHECK_POP * 0.5 + GLOW_AFTER, "name": "success", "volume": 0.8,
|
|
256
|
+
"why": "lista completa"})
|
|
257
|
+
return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def suggested_duration(params: dict) -> float:
|
|
261
|
+
"""Last check, the glow and about 1.4 s to read the whole list."""
|
|
262
|
+
last = _times(params)[-1]
|
|
263
|
+
return round(max(TEMPLATE["default_duration"], last + CHECK_AT + CHECK_POP + 1.4 + 0.15 * len(params["items"])), 2)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def settle(params: dict, fps: int) -> float:
|
|
267
|
+
last = _times(params)[-1]
|
|
268
|
+
n = len(params["items"])
|
|
269
|
+
return max(ENTER, SKELETON + 0.05 * (n - 1) + 0.34,
|
|
270
|
+
last + CHECK_AT + CHECK_POP * 0.5 + GLOW_AFTER + GLOW,
|
|
271
|
+
last + CHECK_AT + CHECK_POP, last + CHECK_AT + 0.05 + CHECK_DRAW, last + ROW_IN) + 0.05
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""comentario_cta: a generic comment bar where a keyword is typed letter by letter, then sent with a tap.
|
|
2
|
+
|
|
3
|
+
The look is a neutral social app (no logo, name or icon of a real platform): a round avatar with a generic
|
|
4
|
+
silhouette, a pill-shaped field with the placeholder, a blinking cursor, and a send button with a paper-plane
|
|
5
|
+
glyph (or the word "Enviar"). After the tap the comment shows up above the bar and the call-to-action text
|
|
6
|
+
moves up to make room, like a real comment list.
|
|
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": "comentario_cta",
|
|
20
|
+
"label_pt": "Comentário com palavra-chave",
|
|
21
|
+
"description_pt": "Uma barra de comentário genérica em que a palavra-chave (ex.: IDEIA) é digitada letra por "
|
|
22
|
+
"letra com cursor, o botão de enviar é tocado e o comentário aparece publicado acima.",
|
|
23
|
+
"use_when_pt": "Na chamada para comentar uma palavra e receber algo (link, material, aula) por mensagem. "
|
|
24
|
+
"Coloque no fim do vídeo, junto com a fala da chamada.",
|
|
25
|
+
"notes_pt": "Visual genérico de propósito: não imita o Instagram nem outro app (sem logo, nome ou ícone de "
|
|
26
|
+
"marca). Use {palavra} no texto de cima para repetir a palavra-chave.",
|
|
27
|
+
"category": "chamada",
|
|
28
|
+
"version": 1,
|
|
29
|
+
"params": {
|
|
30
|
+
"keyword": {"type": "text", "default": "IDEIA", "max_len": 20, "required_text": True,
|
|
31
|
+
"label_pt": "Palavra-chave digitada na barra (ex.: IDEIA, LINK, QUERO)"},
|
|
32
|
+
"prompt_text": {"type": "text", "default": "Comente a palavra", "max_len": 48,
|
|
33
|
+
"label_pt": "Texto acima da barra (vazio: sem texto). *palavra* na cor de destaque; "
|
|
34
|
+
"{palavra} vira a palavra-chave"},
|
|
35
|
+
"placeholder": {"type": "text", "default": "Adicione um comentário...", "max_len": 32,
|
|
36
|
+
"label_pt": "Texto apagado da barra antes de digitar"},
|
|
37
|
+
"user_name": {"type": "text", "default": "você", "max_len": 20,
|
|
38
|
+
"label_pt": "Nome que aparece no comentário publicado"},
|
|
39
|
+
"upper": {"type": "bool", "default": True, "label_pt": "Palavra-chave em letra maiúscula"},
|
|
40
|
+
"delay": {"type": "number", "default": 0.7, "min": 0.3, "max": 5,
|
|
41
|
+
"label_pt": "Segundos até começar a digitar"},
|
|
42
|
+
"typing_speed": {"type": "number", "default": 7.0, "min": 2, "max": 30,
|
|
43
|
+
"label_pt": "Letras por segundo (palavra longa é digitada mais rápido, em até 1,6 s)"},
|
|
44
|
+
"button": {"type": "choice", "choices": ["icone", "texto"], "default": "icone",
|
|
45
|
+
"label_pt": "Botão de enviar: ícone de aviãozinho ou a palavra Enviar"},
|
|
46
|
+
"show_sent": {"type": "bool", "default": True,
|
|
47
|
+
"label_pt": "Depois do toque, mostrar o comentário publicado acima da barra"},
|
|
48
|
+
"size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
|
|
49
|
+
"label_pt": "Tamanho da barra"},
|
|
50
|
+
},
|
|
51
|
+
"default_duration": 3.8,
|
|
52
|
+
"default_region": "baixo",
|
|
53
|
+
"sfx": [{"at": 0.0, "name": "pop", "volume": 0.8, "why": "barra aparece"},
|
|
54
|
+
{"at": 0.7, "name": "keyboard", "volume": 0.9, "why": "digitando"},
|
|
55
|
+
{"at": 1.76, "name": "mouse_click", "why": "toque em enviar"},
|
|
56
|
+
{"at": 1.84, "name": "pop", "volume": 0.7, "why": "comentário publicado"}],
|
|
57
|
+
"example": {"keyword": "IDEIA", "prompt_text": "Comente *{palavra}* que eu te mando o link"},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ENTER = 0.40 # entrance of the bar
|
|
61
|
+
TAP_AFTER = 0.45 # pause between the last letter and the tap
|
|
62
|
+
SENT_AFTER = 0.08 # the comment starts this long after the tap
|
|
63
|
+
SENT_T = 0.42 # comment slide
|
|
64
|
+
PROMPT_MOVE = 0.30 # the prompt moves up first, so the comment never covers it
|
|
65
|
+
HOLD = 1.6 # seconds to read after the comment lands
|
|
66
|
+
BLINK = 1.0 # cursor period
|
|
67
|
+
MAX_TYPING = 1.6 # seconds
|
|
68
|
+
RIPPLE = 0.36 # the tap ripple grows to 1.36x the button
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------ text and timing
|
|
72
|
+
|
|
73
|
+
def _keyword(p: dict) -> str:
|
|
74
|
+
kw = E.clean_text(p["keyword"], "text_bold").strip()
|
|
75
|
+
if not kw:
|
|
76
|
+
raise MotionError("comentario_cta.params.keyword precisa de letras ou números que a fonte desenha")
|
|
77
|
+
return kw.upper() if p["upper"] else kw
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@lru_cache(maxsize=128)
|
|
81
|
+
def _timeline(params_json: str) -> dict:
|
|
82
|
+
"""Seconds (item time) of every beat; typing has a small human rhythm, fixed by the keyword."""
|
|
83
|
+
p = json.loads(params_json)
|
|
84
|
+
kw = _keyword(p)
|
|
85
|
+
r = E.rng("comentario_cta", kw)
|
|
86
|
+
# long keywords are typed faster so the typing never takes much more than MAX_TYPING
|
|
87
|
+
base = 1.0 / max(p["typing_speed"], (len(kw) - 1) / MAX_TYPING if len(kw) > 1 else 1.0)
|
|
88
|
+
times = []
|
|
89
|
+
t = p["delay"]
|
|
90
|
+
for i, ch in enumerate(kw):
|
|
91
|
+
times.append(t)
|
|
92
|
+
step = base * r.uniform(0.7, 1.3)
|
|
93
|
+
if ch == " " or (i + 1 < len(kw) and kw[i + 1] == " "):
|
|
94
|
+
step *= 1.35
|
|
95
|
+
t += step
|
|
96
|
+
t_end = times[-1]
|
|
97
|
+
t_tap = t_end + TAP_AFTER
|
|
98
|
+
return {"keyword": kw, "chars": times, "focus": min(0.45, max(0.25, p["delay"] - 0.35)), "type": p["delay"],
|
|
99
|
+
"type_end": t_end, "tap": t_tap, "sent": t_tap + SENT_AFTER}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _prompt(p: dict, kw: str) -> str:
|
|
103
|
+
return p["prompt_text"].replace("{palavra}", kw).strip()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _prompt_lines(text: str, max_width: float, k: float) -> tuple[list[str], float]:
|
|
107
|
+
"""The prompt on one line at a good size, or on two balanced lines (the *accent* marks are kept per word)."""
|
|
108
|
+
words = E.parse_accent(text)
|
|
109
|
+
if not words:
|
|
110
|
+
return [], 0.0
|
|
111
|
+
plain = " ".join(w for w, _ in words)
|
|
112
|
+
one = E.fit_text(plain, "sans_extrabold", max_width, 56.0 * k, 20.0)
|
|
113
|
+
marked = [("*" + w.replace("*", "\\*") + "*") if a else w.replace("*", "\\*") for w, a in words]
|
|
114
|
+
if one >= 46.0 * k or len(words) < 2:
|
|
115
|
+
return [" ".join(marked)], max(30.0, one)
|
|
116
|
+
size = 52.0 * k
|
|
117
|
+
lines = E.wrap_text(plain, "sans_extrabold", size, max_width, 2)
|
|
118
|
+
widest = max(E.measure(ln, "sans_extrabold", size).width for ln in lines)
|
|
119
|
+
size = max(30.0, min(size, size * max_width / widest))
|
|
120
|
+
out, i = [], 0
|
|
121
|
+
for ln in lines:
|
|
122
|
+
n = len(ln.split())
|
|
123
|
+
out.append(" ".join(marked[i:i + n]))
|
|
124
|
+
i += n
|
|
125
|
+
return out, size
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def validate(params: dict) -> None:
|
|
129
|
+
"""The keyword needs letters the font draws (refused when the item is planned)."""
|
|
130
|
+
_keyword(params)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@lru_cache(maxsize=64)
|
|
134
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
135
|
+
p = json.loads(params_json)
|
|
136
|
+
T = _timeline(params_json)
|
|
137
|
+
kw = T["keyword"]
|
|
138
|
+
k = 1.18 if p["size"] == "grande" else 1.0
|
|
139
|
+
theme = box.theme
|
|
140
|
+
W = box.w
|
|
141
|
+
bar_h = 116.0 * k
|
|
142
|
+
av_r = 44.0 * k
|
|
143
|
+
gap = 18.0 * k
|
|
144
|
+
field_x = 2 * av_r + gap
|
|
145
|
+
field_w = W - field_x
|
|
146
|
+
btn_r = 38.0 * k
|
|
147
|
+
pad = 34.0 * k
|
|
148
|
+
btn_cx = W - 14.0 * k - btn_r
|
|
149
|
+
text_w_max = field_w - pad - (2 * btn_r + 30.0 * k)
|
|
150
|
+
send_label = "Enviar"
|
|
151
|
+
if p["button"] == "texto":
|
|
152
|
+
send_size = 38.0 * k
|
|
153
|
+
send_w = E.measure(send_label, "text_bold", send_size).width
|
|
154
|
+
btn_cx = W - 26.0 * k - send_w / 2.0
|
|
155
|
+
text_w_max = field_w - pad - (send_w + 52.0 * k)
|
|
156
|
+
else:
|
|
157
|
+
send_size = 0.0
|
|
158
|
+
send_w = 0.0
|
|
159
|
+
kw_size = E.fit_text(kw, "text_bold", text_w_max - 14.0 * k, 50.0 * k, 26.0)
|
|
160
|
+
ph_size = E.fit_text(p["placeholder"], "text_semibold", text_w_max, 40.0 * k, 24.0) if p["placeholder"] else 0.0
|
|
161
|
+
kw_cap = E.measure("H", "text_bold", kw_size).cap
|
|
162
|
+
|
|
163
|
+
prompt_lines, pr_size = _prompt_lines(_prompt(p, kw), W - 20.0, k)
|
|
164
|
+
pr_cap = E.measure("H", "sans_extrabold", pr_size).cap if prompt_lines else 0.0
|
|
165
|
+
pr_pitch = pr_size * 1.22
|
|
166
|
+
pr_h = pr_cap + pr_pitch * (len(prompt_lines) - 1) if prompt_lines else 0.0
|
|
167
|
+
pr_gap = 34.0 * k
|
|
168
|
+
|
|
169
|
+
# the published comment (content-sized bubble)
|
|
170
|
+
row_h = 126.0 * k
|
|
171
|
+
cav_r = 34.0 * k
|
|
172
|
+
name_size, time_size, ckw_size = 31.0 * k, 27.0 * k, 50.0 * k
|
|
173
|
+
name = E.clean_text(p["user_name"], "text_bold").strip() or "você"
|
|
174
|
+
name_w = E.measure(name, "text_bold", name_size).width
|
|
175
|
+
time_w = E.measure("agora", "text_semibold", time_size).width
|
|
176
|
+
heart = 34.0 * k
|
|
177
|
+
ckw_size = E.fit_text(kw, "text_bold", field_w - 2 * pad - heart - 30.0 * k, ckw_size, 24.0)
|
|
178
|
+
ckw_w = E.measure(kw, "text_bold", ckw_size).width
|
|
179
|
+
bubble_w = min(field_w, max(name_w + 16.0 * k + time_w, ckw_w) + 2 * pad + heart + 34.0 * k)
|
|
180
|
+
bubble_w = max(bubble_w, min(field_w, 360.0 * k))
|
|
181
|
+
|
|
182
|
+
sent = p["show_sent"]
|
|
183
|
+
stack_h = (pr_h + pr_gap if prompt_lines else 0.0) + (row_h + gap if sent else 0.0) + bar_h
|
|
184
|
+
top = (box.h - stack_h) / 2.0
|
|
185
|
+
bar_y = top + stack_h - bar_h
|
|
186
|
+
row_y = bar_y - gap - row_h
|
|
187
|
+
prompt_base_end = (row_y if sent else bar_y) - pr_gap
|
|
188
|
+
prompt_base_start = bar_y - pr_gap
|
|
189
|
+
|
|
190
|
+
acc = theme.accent
|
|
191
|
+
card = theme.card
|
|
192
|
+
on_card_accent = "accent" if E.contrast(acc, card[:3]) >= 2.2 else "text"
|
|
193
|
+
dark = E.mix_color(acc, (0, 0, 0), 0.35)
|
|
194
|
+
light = E.mix_color(acc, (255, 255, 255), 0.25)
|
|
195
|
+
return {
|
|
196
|
+
"k": k, "kw": kw, "bar_y": bar_y, "bar_h": bar_h, "av_r": av_r, "field_x": field_x, "field_w": field_w,
|
|
197
|
+
"btn_r": btn_r, "btn_cx": btn_cx, "pad": pad,
|
|
198
|
+
"kw_size": kw_size, "kw_cap": kw_cap, "ph_size": ph_size, "send_size": send_size, "send_w": send_w,
|
|
199
|
+
"send_label": send_label, "prompt": prompt_lines, "pr_size": pr_size, "pr_pitch": pr_pitch,
|
|
200
|
+
"prompt_start": prompt_base_start, "prompt_end": prompt_base_end,
|
|
201
|
+
"row_y": row_y, "row_h": row_h, "cav_r": cav_r, "bubble_w": bubble_w, "name": name,
|
|
202
|
+
"name_size": name_size, "name_w": name_w, "time_size": time_size, "ckw_size": ckw_size, "heart": heart,
|
|
203
|
+
"accent_text": on_card_accent, "grad": (light, dark),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------ small drawings (mask helpers)
|
|
208
|
+
|
|
209
|
+
def _avatar(cv: E.Canvas, L: dict, cx: float, cy: float, r: float) -> None:
|
|
210
|
+
"""Round avatar with a generic person silhouette."""
|
|
211
|
+
light, dark = L["grad"]
|
|
212
|
+
cv.circle(cx, cy, r, fill=E.Gradient(light, dark, (cx - r, cy - r), (cx + r, cy + r)),
|
|
213
|
+
stroke="card_border", stroke_width=2.0)
|
|
214
|
+
white = (255, 255, 255, 235)
|
|
215
|
+
cv.circle(cx, cy - 0.17 * r, 0.33 * r, fill=white)
|
|
216
|
+
body = E.intersect_masks(cv.circle_mask(cx, cy + 0.98 * r, 0.64 * r), cv.circle_mask(cx, cy, r - 1.0))
|
|
217
|
+
cv.fill_mask(body, white)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
PLANE = [(2.01, 21.0), (23.0, 12.0), (2.01, 3.0), (2.0, 10.0), (17.0, 12.0), (2.0, 14.0)]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _plane(cv: E.Canvas, cx: float, cy: float, size: float, color, opacity: float = 1.0) -> None:
|
|
224
|
+
"""Generic 'send' glyph (paper plane), tilted up."""
|
|
225
|
+
s = size / 21.0
|
|
226
|
+
a = math.radians(-22.0)
|
|
227
|
+
ca, sa = math.cos(a), math.sin(a)
|
|
228
|
+
pts = []
|
|
229
|
+
for x, y in PLANE:
|
|
230
|
+
u, v = (x - 12.2) * s, (y - 12.0) * s
|
|
231
|
+
pts.append((cx + u * ca - v * sa + size * 0.04, cy + u * sa + v * ca))
|
|
232
|
+
cv.polygon(pts, fill=color, opacity=opacity)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------ render
|
|
236
|
+
|
|
237
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
238
|
+
key = json.dumps(params, sort_keys=True)
|
|
239
|
+
L = _layout(key, box)
|
|
240
|
+
T = _timeline(key)
|
|
241
|
+
cv = E.Canvas(box)
|
|
242
|
+
t = frame_index / fps
|
|
243
|
+
t_in = t + 1.0 / fps
|
|
244
|
+
k = L["k"]
|
|
245
|
+
|
|
246
|
+
# --- the call-to-action text above (moves up when the comment arrives)
|
|
247
|
+
if L["prompt"]:
|
|
248
|
+
pa = E.eased(t_in, 0.08, 0.30, "out_quad")
|
|
249
|
+
if cv.visible(pa):
|
|
250
|
+
move = E.eased(t, T["sent"], PROMPT_MOVE, "out_cubic") if params["show_sent"] else 0.0
|
|
251
|
+
base = E.lerp(L["prompt_start"], L["prompt_end"], move) + 26.0 * (1.0 - E.eased(t_in, 0.08, 0.45))
|
|
252
|
+
last = len(L["prompt"]) - 1
|
|
253
|
+
for i, line in enumerate(L["prompt"]):
|
|
254
|
+
cv.rich_text(cv.cx, base - (last - i) * L["pr_pitch"], line, "sans_extrabold", L["pr_size"],
|
|
255
|
+
color="white", opacity=pa, anchor="ms", stroke=L["pr_size"] * 0.09,
|
|
256
|
+
stroke_color="black", shadow=E.TEXT_SHADOW)
|
|
257
|
+
|
|
258
|
+
# --- the published comment
|
|
259
|
+
if params["show_sent"]:
|
|
260
|
+
ca = E.eased(t, T["sent"] + 0.12, 0.20, "out_quad")
|
|
261
|
+
if cv.visible(ca):
|
|
262
|
+
rise = 24.0 * k * (1.0 - E.eased(t, T["sent"] + 0.06, SENT_T - 0.06, "out_cubic"))
|
|
263
|
+
y0 = L["row_y"] + rise
|
|
264
|
+
with cv.group(opacity=ca):
|
|
265
|
+
rcx = L["av_r"]
|
|
266
|
+
rcy = y0 + L["row_h"] / 2.0
|
|
267
|
+
_avatar(cv, L, rcx, rcy, L["cav_r"])
|
|
268
|
+
bx = L["field_x"]
|
|
269
|
+
cv.card(bx, y0, L["bubble_w"], L["row_h"], radius=30.0 * k)
|
|
270
|
+
name_base = y0 + L["pad"] * 0.62 + E.measure("H", "text_bold", L["name_size"]).cap
|
|
271
|
+
cv.text(bx + L["pad"], name_base, L["name"], "text_bold", L["name_size"], color="muted")
|
|
272
|
+
cv.text(bx + L["pad"] + L["name_w"] + 14.0 * k, name_base, "agora", "text_semibold",
|
|
273
|
+
L["time_size"], color="muted", opacity=0.7)
|
|
274
|
+
kw_base = y0 + L["row_h"] - L["pad"] * 0.62
|
|
275
|
+
cv.text(bx + L["pad"], kw_base, L["kw"], "text_bold", L["ckw_size"], color="text")
|
|
276
|
+
hx = bx + L["bubble_w"] - L["pad"] - L["heart"] / 2.0
|
|
277
|
+
cv.polyline(E.heart_points(hx, rcy, L["heart"]), 3.2 * k, color="muted")
|
|
278
|
+
|
|
279
|
+
# --- the bar
|
|
280
|
+
ba = E.eased(t_in, 0.0, 0.22, "out_quad")
|
|
281
|
+
rise = 40.0 * k * (1.0 - E.eased(t_in, 0.0, ENTER, "out_cubic"))
|
|
282
|
+
with cv.group(opacity=ba, dy=rise):
|
|
283
|
+
cy = L["bar_y"] + L["bar_h"] / 2.0
|
|
284
|
+
_avatar(cv, L, L["av_r"], cy, L["av_r"])
|
|
285
|
+
cv.card(L["field_x"], L["bar_y"], L["field_w"], L["bar_h"], radius=L["bar_h"] / 2.0)
|
|
286
|
+
tx = L["field_x"] + L["pad"]
|
|
287
|
+
|
|
288
|
+
n = sum(1 for c in T["chars"] if t >= c)
|
|
289
|
+
cleared = params["show_sent"] and t >= T["sent"]
|
|
290
|
+
typed = L["kw"][:n]
|
|
291
|
+
# placeholder: until the first letter, and back after the comment is sent
|
|
292
|
+
ph = 0.0
|
|
293
|
+
if L["ph_size"]:
|
|
294
|
+
if n == 0:
|
|
295
|
+
ph = 1.0
|
|
296
|
+
elif cleared:
|
|
297
|
+
ph = E.eased(t, T["sent"] + 0.06, 0.16, "out_quad")
|
|
298
|
+
if cv.visible(ph):
|
|
299
|
+
cv.text(tx, cy, params["placeholder"], "text_semibold", L["ph_size"], color="muted", opacity=0.62 * ph,
|
|
300
|
+
anchor="lc")
|
|
301
|
+
text_end = tx - 9.0 * k
|
|
302
|
+
if typed:
|
|
303
|
+
ka = 1.0 - E.eased(t, T["sent"], 0.10, "out_quad") if cleared else 1.0
|
|
304
|
+
if cv.visible(ka):
|
|
305
|
+
x0, _, x1, _ = cv.text(tx, cy, typed, "text_bold", L["kw_size"], color="text", opacity=ka,
|
|
306
|
+
anchor="lc")
|
|
307
|
+
text_end = x1
|
|
308
|
+
# cursor: blinks while waiting, stays on while typing, gone after the tap
|
|
309
|
+
if T["focus"] <= t < T["tap"]:
|
|
310
|
+
typing = T["type"] <= t <= T["type_end"] + 0.12
|
|
311
|
+
ref = T["focus"] if t < T["type"] else T["type_end"] + 0.12
|
|
312
|
+
on = typing or ((t - ref) % BLINK) < BLINK * 0.55
|
|
313
|
+
if on:
|
|
314
|
+
ch = L["kw_cap"] * 1.45
|
|
315
|
+
cv.rect(text_end + 3.0 * k, cy - ch / 2.0, 4.0 * k, ch, radius=2.0 * k, fill=L["accent_text"])
|
|
316
|
+
|
|
317
|
+
# send button: gray until the first letter, then the accent; gray again once the field is cleared
|
|
318
|
+
act = E.eased(t, T["chars"][0], 0.14, "out_quad") if n > 0 else 0.0
|
|
319
|
+
if cleared:
|
|
320
|
+
act *= 1.0 - E.eased(t, T["sent"], 0.14, "out_quad")
|
|
321
|
+
pop = E.pulse(t, T["chars"][0], 0.26) * 0.12
|
|
322
|
+
press = E.pulse(t, T["tap"] - 0.05, 0.22) * 0.16
|
|
323
|
+
bs = 1.0 + pop - press
|
|
324
|
+
bcx = L["btn_cx"]
|
|
325
|
+
with cv.group(scale=bs, pivot=(bcx, cy)):
|
|
326
|
+
if params["button"] == "texto":
|
|
327
|
+
col = L["accent_text"]
|
|
328
|
+
cv.text(bcx, cy, L["send_label"], "text_bold", L["send_size"], color="muted", opacity=0.45 * (1 - act),
|
|
329
|
+
anchor="mc")
|
|
330
|
+
if act > 0.001:
|
|
331
|
+
cv.text(bcx, cy, L["send_label"], "text_bold", L["send_size"], color=col, opacity=act,
|
|
332
|
+
anchor="mc")
|
|
333
|
+
else:
|
|
334
|
+
r = L["btn_r"]
|
|
335
|
+
cv.circle(bcx, cy, r, fill="track", opacity=1.0 - act)
|
|
336
|
+
if act > 0.001:
|
|
337
|
+
cv.circle(bcx, cy, r, fill="accent", opacity=act)
|
|
338
|
+
_plane(cv, bcx, cy, r * 1.02, "muted", opacity=0.55 * (1.0 - act))
|
|
339
|
+
if act > 0.001:
|
|
340
|
+
_plane(cv, bcx, cy, r * 1.02, "on_accent", opacity=act)
|
|
341
|
+
|
|
342
|
+
# the finger tap: a soft touch dot, then a ripple
|
|
343
|
+
tap = T["tap"]
|
|
344
|
+
# the touch reads as a light mark on the dark bar and a dark one on the light bar
|
|
345
|
+
touch, touch_k = ("white", 1.0) if box.theme.dark else ("ink", 0.55)
|
|
346
|
+
if tap - 0.2 <= t <= tap + 0.45:
|
|
347
|
+
dot = E.eased(t, tap - 0.2, 0.12, "out_quad") * (1.0 - E.eased(t, tap + 0.02, 0.2, "out_quad"))
|
|
348
|
+
if cv.visible(dot):
|
|
349
|
+
rr = L["btn_r"] * (1.25 - 0.25 * E.eased(t, tap - 0.2, 0.18))
|
|
350
|
+
cv.circle(bcx, cy, rr, fill=touch, opacity=0.38 * touch_k * dot)
|
|
351
|
+
rp = E.progress(t, tap, 0.4)
|
|
352
|
+
if 0 < rp < 1:
|
|
353
|
+
# the ripple stays inside the box (the right rail of the app starts right after it)
|
|
354
|
+
cv.ring(bcx, cy, L["btn_r"] * (1.0 + RIPPLE * E.out_cubic(rp)), max(1.0, 7.0 * k * (1 - rp)),
|
|
355
|
+
color=touch, opacity=0.7 * touch_k * (1 - rp))
|
|
356
|
+
return cv
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
360
|
+
T = _timeline(json.dumps(params, sort_keys=True))
|
|
361
|
+
cues = [{"at": 0.0, "name": "pop", "volume": 0.8, "why": "barra aparece"},
|
|
362
|
+
{"at": T["type"], "name": "keyboard", "volume": 0.9, "why": "digitando",
|
|
363
|
+
"length": round(T["type_end"] - T["type"] + 0.2, 3)},
|
|
364
|
+
{"at": T["tap"], "name": "mouse_click", "volume": 1.0, "why": "toque em enviar"}]
|
|
365
|
+
if params["show_sent"]:
|
|
366
|
+
cues.append({"at": T["sent"], "name": "pop", "volume": 0.7, "why": "comentário publicado"})
|
|
367
|
+
return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def suggested_duration(params: dict) -> float:
|
|
371
|
+
"""Typing, the tap, the comment and about 1.6 s to read."""
|
|
372
|
+
T = _timeline(json.dumps(params, sort_keys=True))
|
|
373
|
+
return round(max(2.5, T["sent"] + SENT_T + HOLD), 2)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def settle(params: dict, fps: int) -> float:
|
|
377
|
+
"""After the comment lands (or the tap ripple ends) nothing moves: the cursor is gone after the tap."""
|
|
378
|
+
T = _timeline(json.dumps(params, sort_keys=True))
|
|
379
|
+
end = T["sent"] + SENT_T if params["show_sent"] else T["tap"] + 0.45
|
|
380
|
+
return end + 1.5 / fps
|