primocode 10.16.0 → 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 +11 -2
- 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/nuvem.js +11 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- 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,410 @@
|
|
|
1
|
+
"""palavra_impacto: one to four big words that slam onto the screen one after the other (kinetic typography).
|
|
2
|
+
|
|
3
|
+
Each word starts big and faint, falls onto the screen accelerating, lands with a squash, and on the
|
|
4
|
+
landing the screen flashes, the block shakes and short impact lines shoot out. Two looks: "contorno"
|
|
5
|
+
(white letters with a thick black outline, like the `impacto` captions) and "tarja" (each word on a solid,
|
|
6
|
+
slightly tilted chip, white or in the accent color, like the `tarja` hook). In "tarja" the chip and its word
|
|
7
|
+
slam in as one piece.
|
|
8
|
+
|
|
9
|
+
Full-screen flash: `flash: "tela"` draws on `E.Canvas(box, full=True)` on EVERY frame (Contract 12). The
|
|
10
|
+
flash, the glow and the impact lines are effects, not content: `ink()` gives the area of the resting words
|
|
11
|
+
(plus the entrance scale), so face-aware placing and the caption rules only see the words.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
|
|
20
|
+
from PIL import Image
|
|
21
|
+
|
|
22
|
+
from motion import engine as E
|
|
23
|
+
from motion.engine import MotionError
|
|
24
|
+
|
|
25
|
+
TEMPLATE = {
|
|
26
|
+
"id": "palavra_impacto",
|
|
27
|
+
"label_pt": "Palavra de impacto",
|
|
28
|
+
"description_pt": "Uma ou duas palavras gigantes que batem na tela uma de cada vez: entram grandes, "
|
|
29
|
+
"encolhem com um tranco, a tela pisca e o texto treme um pouco.",
|
|
30
|
+
"use_when_pt": "Na palavra forte da fala: o gancho, uma virada ou uma ordem curta (PARE, ZERO ESTOQUE, "
|
|
31
|
+
"SEM CNPJ). Use pouco, uma ou duas vezes por vídeo. Marque a palavra principal com "
|
|
32
|
+
"*asteriscos* para ela sair na cor de destaque.",
|
|
33
|
+
"notes_pt": "Até 4 palavras em até 2 linhas. O clarão da tela inteira (flash: tela) pisca por 1 quadro "
|
|
34
|
+
"em cada palavra; em vídeo com muita coisa piscando, use flash: local ou nenhum.",
|
|
35
|
+
"category": "texto",
|
|
36
|
+
"version": 1,
|
|
37
|
+
"params": {
|
|
38
|
+
"text": {"type": "text", "default": "*ATENÇÃO*", "max_len": 32, "required_text": True,
|
|
39
|
+
"label_pt": "Até 4 palavras. *palavra* sai na cor de destaque e | quebra a linha"},
|
|
40
|
+
"style": {"type": "choice", "choices": ["contorno", "tarja"], "default": "contorno",
|
|
41
|
+
"label_pt": "Visual: contorno (letra branca com borda preta) ou tarja (cada palavra numa "
|
|
42
|
+
"plaquinha inclinada)"},
|
|
43
|
+
"font": {"type": "choice", "choices": ["condensada", "redonda"], "default": "condensada",
|
|
44
|
+
"label_pt": "Letra: condensada (alta e estreita) ou redonda (larga e grossa)"},
|
|
45
|
+
"size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
|
|
46
|
+
"label_pt": "Tamanho das palavras"},
|
|
47
|
+
"flash": {"type": "choice", "choices": ["tela", "local", "nenhum"], "default": "tela",
|
|
48
|
+
"label_pt": "Clarão na batida: tela inteira, só em volta das palavras, ou nenhum"},
|
|
49
|
+
"flash_color": {"type": "choice", "choices": ["branco", "destaque"], "default": "branco",
|
|
50
|
+
"label_pt": "Cor do clarão"},
|
|
51
|
+
"shake": {"type": "bool", "default": True, "label_pt": "Tremida curta quando cada palavra bate"},
|
|
52
|
+
"burst": {"type": "bool", "default": True, "label_pt": "Risquinhos de impacto saindo em volta"},
|
|
53
|
+
"stagger": {"type": "number", "default": 0.24, "min": 0, "max": 1.5,
|
|
54
|
+
"label_pt": "Segundos entre uma palavra e a próxima (0: todas juntas)"},
|
|
55
|
+
"upper": {"type": "bool", "default": True, "label_pt": "Tudo em letra maiúscula"},
|
|
56
|
+
"sound": {"type": "choice", "choices": ["metal", "grave", "batida"], "default": "metal",
|
|
57
|
+
"label_pt": "Som de cada batida: metal (impacto metálico), grave ou batida sintética"},
|
|
58
|
+
},
|
|
59
|
+
"default_duration": 2.0,
|
|
60
|
+
"default_region": "baixo",
|
|
61
|
+
"sfx": [{"at": 0.13, "name": "metal_hit", "why": "palavra batendo"}],
|
|
62
|
+
"bleed": 300,
|
|
63
|
+
"example": {"text": "ZERO *ESTOQUE*"},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
FONTS = {"condensada": "display_condensed", "redonda": "sans_black"}
|
|
67
|
+
MAX_SIZE = {("condensada", "normal"): 250.0, ("condensada", "grande"): 330.0,
|
|
68
|
+
("redonda", "normal"): 165.0, ("redonda", "grande"): 220.0}
|
|
69
|
+
SOUNDS = {"metal": "metal_hit", "grave": "bass_drop", "batida": "synth_hit"}
|
|
70
|
+
MAX_WORDS = 4
|
|
71
|
+
|
|
72
|
+
T_HIT = 0.15 # seconds from a word's start to its landing
|
|
73
|
+
T_FADE = 0.12 # the word fades in (in_quad) while it falls: frame 0 is faint, not yet "ink"
|
|
74
|
+
START_SCALE = 1.8 # the word starts this big (the bleed holds it)
|
|
75
|
+
SQUASH = 0.075 # scale dip on the landing
|
|
76
|
+
T_SETTLE = 0.40 # the landing wobble ends exactly here
|
|
77
|
+
FLASH_T = 0.15
|
|
78
|
+
FLASH_TAU = 0.0185 # flash decay at 30 fps: 0.60, 0.08, 0.01 (only the landing frame passes alpha 24)
|
|
79
|
+
SHAKE_T = 0.30
|
|
80
|
+
BURST_T = 0.30
|
|
81
|
+
N_RAYS = 12
|
|
82
|
+
RAY_SPREAD = math.radians(30) # the lines fan out this much above and below the horizontal
|
|
83
|
+
SIDE = 24.0 # free space kept at the box sides
|
|
84
|
+
# "tarja" chips, as fractions of the font size
|
|
85
|
+
CHIP_X, CHIP_Y, CHIP_GAP = 0.20, 0.17, 0.12
|
|
86
|
+
TILTS = (-3.0, 2.2, -1.6, 2.8)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------ text and layout
|
|
90
|
+
|
|
91
|
+
def _lines_of(text: str, upper: bool, font_key: str) -> list[list[tuple[str, bool]]]:
|
|
92
|
+
"""'ZERO | *ESTOQUE*' -> [[('ZERO', False)], [('ESTOQUE', True)]] (words the font cannot draw vanish)."""
|
|
93
|
+
lines: list[list[tuple[str, bool]]] = []
|
|
94
|
+
cur: list[tuple[str, bool]] = []
|
|
95
|
+
for word, acc in E.parse_accent(str(text).replace("|", " | ")):
|
|
96
|
+
if word == "|":
|
|
97
|
+
if cur:
|
|
98
|
+
lines.append(cur)
|
|
99
|
+
cur = []
|
|
100
|
+
continue
|
|
101
|
+
w = E.clean_text(word, font_key).strip()
|
|
102
|
+
if not w:
|
|
103
|
+
continue
|
|
104
|
+
cur.append((w.upper() if upper else w, acc))
|
|
105
|
+
if cur:
|
|
106
|
+
lines.append(cur)
|
|
107
|
+
if not lines:
|
|
108
|
+
raise MotionError("palavra_impacto.params.text precisa de pelo menos uma palavra com letras ou números")
|
|
109
|
+
count = sum(len(ln) for ln in lines)
|
|
110
|
+
if count > MAX_WORDS:
|
|
111
|
+
raise MotionError(f"palavra_impacto.params.text tem {count} palavras; o máximo é {MAX_WORDS}. "
|
|
112
|
+
"Deixe só as palavras fortes")
|
|
113
|
+
if len(lines) > 2:
|
|
114
|
+
raise MotionError(f"palavra_impacto.params.text tem {len(lines)} linhas; o máximo é 2 (use só um |)")
|
|
115
|
+
return lines
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _word_count(params: dict) -> int:
|
|
119
|
+
return sum(len(ln) for ln in _lines_of(params["text"], params["upper"], FONTS[params["font"]]))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate(params: dict) -> None:
|
|
123
|
+
"""At most 4 words in 2 lines, with letters the font draws (refused when the item is planned)."""
|
|
124
|
+
_lines_of(params["text"], params["upper"], FONTS[params["font"]])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _hit_times(count: int, stagger: float, fps: int) -> list[float]:
|
|
128
|
+
"""Landing of each word in `t_in` time ((frame + 1) / fps), snapped to a frame so the flash peaks on it."""
|
|
129
|
+
return [E.frame_of(k * stagger + T_HIT, fps) / float(fps) for k in range(count)]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@lru_cache(maxsize=64)
|
|
133
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
134
|
+
p = json.loads(params_json)
|
|
135
|
+
fk = FONTS[p["font"]]
|
|
136
|
+
tarja = p["style"] == "tarja"
|
|
137
|
+
lines = _lines_of(p["text"], p["upper"], fk)
|
|
138
|
+
max_size = MAX_SIZE[(p["font"], p["size"])]
|
|
139
|
+
avail = box.w - 2 * SIDE
|
|
140
|
+
space_unit = E.font(fk, 100.0).getlength(" ") / 100.0
|
|
141
|
+
# gap between two words and the extra width around a line, as fractions of the size
|
|
142
|
+
gap_unit = (2 * CHIP_X + CHIP_GAP) if tarja else space_unit
|
|
143
|
+
ends_unit = (2 * CHIP_X) if tarja else 0.16 # chips, or the outline and the shake
|
|
144
|
+
|
|
145
|
+
def unit_width(line) -> float:
|
|
146
|
+
return (sum(E.measure(w, fk, 100.0).width for w, _ in line) / 100.0
|
|
147
|
+
+ gap_unit * (len(line) - 1) + ends_unit)
|
|
148
|
+
|
|
149
|
+
def fit(line) -> float:
|
|
150
|
+
return min(max_size, avail / unit_width(line))
|
|
151
|
+
|
|
152
|
+
if len(lines) == 1 and len(lines[0]) >= 2:
|
|
153
|
+
words = lines[0]
|
|
154
|
+
one = fit(words)
|
|
155
|
+
best = None
|
|
156
|
+
if one < 0.80 * max_size:
|
|
157
|
+
for cut in range(1, len(words)):
|
|
158
|
+
s = min(fit(words[:cut]), fit(words[cut:]))
|
|
159
|
+
if best is None or s > best[0]:
|
|
160
|
+
best = (s, [words[:cut], words[cut:]])
|
|
161
|
+
if best is not None and best[0] > one * 1.12:
|
|
162
|
+
lines = best[1]
|
|
163
|
+
n = len(lines)
|
|
164
|
+
cap_unit = E.measure("H", fk, 100.0).cap / 100.0
|
|
165
|
+
line_gap_unit = (2 * CHIP_Y + 0.14) if tarja else 0.20
|
|
166
|
+
pad_y_unit = CHIP_Y if tarja else 0.10
|
|
167
|
+
block_unit = n * cap_unit + (n - 1) * line_gap_unit + 2 * pad_y_unit
|
|
168
|
+
size = min(min(fit(ln) for ln in lines), (box.h - 40.0) / block_unit)
|
|
169
|
+
size = max(40.0, size)
|
|
170
|
+
|
|
171
|
+
def width_at(line, s: float) -> float:
|
|
172
|
+
return (sum(E.measure(w, fk, s).width for w, _ in line) + gap_unit * s * (len(line) - 1)
|
|
173
|
+
+ ends_unit * s)
|
|
174
|
+
|
|
175
|
+
# the widths are checked at the final size (FreeType hinting is not perfectly linear)
|
|
176
|
+
while size > 40.0 and any(width_at(ln, size) > avail + 0.5 for ln in lines):
|
|
177
|
+
size *= 0.985
|
|
178
|
+
cap = E.measure("H", fk, size).cap
|
|
179
|
+
line_gap = line_gap_unit * size
|
|
180
|
+
block_h = n * cap + (n - 1) * line_gap
|
|
181
|
+
top = (box.h - block_h) / 2.0
|
|
182
|
+
gap = gap_unit * size
|
|
183
|
+
escuro = box.theme.dark
|
|
184
|
+
has_accent = any(a for ln in lines for _, a in ln)
|
|
185
|
+
count = sum(len(ln) for ln in lines)
|
|
186
|
+
px, py = CHIP_X * size, CHIP_Y * size
|
|
187
|
+
words = []
|
|
188
|
+
k = 0
|
|
189
|
+
for i, line in enumerate(lines):
|
|
190
|
+
widths = [E.measure(w, fk, size).width for w, _ in line]
|
|
191
|
+
total = sum(widths) + gap * (len(line) - 1)
|
|
192
|
+
x = (box.w - total) / 2.0
|
|
193
|
+
base = top + cap + i * (cap + line_gap)
|
|
194
|
+
for (text, acc), wd in zip(line, widths):
|
|
195
|
+
w = {"text": text, "x": x, "w": wd, "k": k, "base": base, "cx": x + wd / 2.0, "cy": base - cap / 2.0}
|
|
196
|
+
if tarja:
|
|
197
|
+
on_accent = acc if has_accent else (count == 1 or k % 2 == 1)
|
|
198
|
+
w.update(chip=(x - px, base - cap - py, wd + 2 * px, cap + 2 * py),
|
|
199
|
+
chip_fill="accent" if on_accent else "white",
|
|
200
|
+
color="on_accent" if on_accent else "ink", stroke_color=None,
|
|
201
|
+
tilt=TILTS[k % len(TILTS)], ink=(x - px, base - cap - py, x + wd + px, base + py))
|
|
202
|
+
else:
|
|
203
|
+
w.update(color="accent" if acc else ("white" if escuro else "ink"),
|
|
204
|
+
stroke_color="black" if escuro else "white",
|
|
205
|
+
ink=(x, base - cap, x + wd, base))
|
|
206
|
+
words.append(w)
|
|
207
|
+
x += wd + gap
|
|
208
|
+
k += 1
|
|
209
|
+
hits = _hit_times(k, p["stagger"], box.fps)
|
|
210
|
+
# per landing: the area of the words already on screen (impact lines and the local flash go around it)
|
|
211
|
+
landings = []
|
|
212
|
+
for j, h in enumerate(sorted(set(hits))):
|
|
213
|
+
landed = [w for w in words if hits[w["k"]] <= h + 1e-9]
|
|
214
|
+
x0 = min(w["ink"][0] for w in landed)
|
|
215
|
+
y0 = min(w["ink"][1] for w in landed)
|
|
216
|
+
x1 = max(w["ink"][2] for w in landed)
|
|
217
|
+
y1 = max(w["ink"][3] for w in landed)
|
|
218
|
+
r = E.rng("palavra_impacto", "rays", j)
|
|
219
|
+
ccx, ccy = (x0 + x1) / 2.0, (y0 + y1) / 2.0
|
|
220
|
+
rx, ry = (x1 - x0) / 2.0 + 36.0, (y1 - y0) / 2.0 + 28.0
|
|
221
|
+
rays = []
|
|
222
|
+
for m in range(N_RAYS):
|
|
223
|
+
# emphasis lines on the two sides (the placing only moves the item up or down, so lines above and
|
|
224
|
+
# below the words would make it look taller than it is)
|
|
225
|
+
side = 0.0 if m % 2 == 0 else math.pi
|
|
226
|
+
ang = side + RAY_SPREAD * ((m // 2) / (N_RAYS // 2 - 1) * 2.0 - 1.0) + r.uniform(-0.08, 0.08)
|
|
227
|
+
if m % 2:
|
|
228
|
+
ang = side - (ang - side)
|
|
229
|
+
ca, sa = math.cos(ang), math.sin(ang)
|
|
230
|
+
sx, sy = ccx + rx * ca, ccy + ry * sa # on the ellipse, pointing away from the center
|
|
231
|
+
length = r.uniform(0.55, 1.0) * min(95.0, 0.36 * size + 20.0)
|
|
232
|
+
rays.append({"p0": (sx + ca * 6.0, sy + sa * 6.0),
|
|
233
|
+
"p1": (sx + ca * (length + 24.0), sy + sa * (length + 24.0)),
|
|
234
|
+
"width": r.uniform(7.0, 11.0), "delay": r.uniform(0.0, 0.04)})
|
|
235
|
+
landings.append({"hit": h, "first": j == 0, "area": (x0, y0, x1, y1), "rays": rays})
|
|
236
|
+
return {"font": fk, "size": size, "cap": cap, "words": words, "hits": hits, "landings": landings,
|
|
237
|
+
"stroke": size * 0.08, "tarja": tarja,
|
|
238
|
+
"shadow": E.Shadow(blur=max(6.0, size * 0.05), dy=size * 0.045, opacity=0.55),
|
|
239
|
+
"chip_shadow": E.Shadow(blur=22.0, dy=12.0, opacity=0.35), "chip_radius": size * 0.07}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------ motion curves
|
|
243
|
+
|
|
244
|
+
def _scale(tk: float) -> float:
|
|
245
|
+
"""tk = seconds since the landing (negative before it)."""
|
|
246
|
+
if tk < 0:
|
|
247
|
+
u = E.clamp01(1.0 + tk / T_HIT)
|
|
248
|
+
return E.lerp(START_SCALE, 1.0, E.in_quad(u))
|
|
249
|
+
if tk >= T_SETTLE:
|
|
250
|
+
return 1.0
|
|
251
|
+
env = math.exp(-tk * 12.0) * (1.0 - tk / T_SETTLE)
|
|
252
|
+
return 1.0 - SQUASH * env * math.cos(2 * math.pi * 5.0 * tk)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _opacity(tk: float) -> float:
|
|
256
|
+
return E.eased(tk, -T_HIT, T_FADE, "in_quad")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def ink(params: dict, box: E.Box) -> tuple[float, float, float, float]:
|
|
260
|
+
"""What counts as drawn (box-local design px): the words at rest, their outline and shadow, and a little of
|
|
261
|
+
the landing squash. The flash, the glow and the impact lines are effects and stay out."""
|
|
262
|
+
L = _layout(json.dumps(params, sort_keys=True), box)
|
|
263
|
+
pad = L["stroke"] + L["shadow"].dy + 6.0
|
|
264
|
+
x0 = min(w["ink"][0] for w in L["words"]) - pad
|
|
265
|
+
y0 = min(w["ink"][1] for w in L["words"]) - pad
|
|
266
|
+
x1 = max(w["ink"][2] for w in L["words"]) + pad
|
|
267
|
+
y1 = max(w["ink"][3] for w in L["words"]) + pad
|
|
268
|
+
return x0, y0, x1, y1
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _flash(landing: dict, t: float) -> float:
|
|
272
|
+
tk = t - landing["hit"]
|
|
273
|
+
if not 0.0 <= tk < FLASH_T:
|
|
274
|
+
return 0.0
|
|
275
|
+
peak = 0.60 if landing["first"] else 0.38
|
|
276
|
+
return peak * math.exp(-tk / FLASH_TAU) * (1.0 - tk / FLASH_T)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ------------------------------------------------------------------ drawing
|
|
280
|
+
|
|
281
|
+
def _word_outline(cv: E.Canvas, L: dict, w: dict, t: float) -> None:
|
|
282
|
+
tk = t - L["hits"][w["k"]]
|
|
283
|
+
op = _opacity(tk)
|
|
284
|
+
if not cv.visible(op):
|
|
285
|
+
return
|
|
286
|
+
fk, size = L["font"], L["size"]
|
|
287
|
+
pivot = (w["cx"], w["cy"])
|
|
288
|
+
if tk < 0:
|
|
289
|
+
# motion trail: where the word was a moment ago, faint
|
|
290
|
+
sg = _scale(tk - 0.04)
|
|
291
|
+
og = _opacity(tk - 0.04) * 0.16 * op
|
|
292
|
+
if cv.visible(og) and sg > _scale(tk) + 0.01:
|
|
293
|
+
with cv.group(scale=sg, pivot=pivot):
|
|
294
|
+
cv.text(w["x"], w["base"], w["text"], fk, size, color=w["color"], opacity=og)
|
|
295
|
+
with cv.group(scale=_scale(tk), pivot=pivot, opacity=op):
|
|
296
|
+
cv.text(w["x"], w["base"], w["text"], fk, size, color=w["color"], stroke=L["stroke"],
|
|
297
|
+
stroke_color=w["stroke_color"], shadow=L["shadow"])
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
SPRITE_PAD = 110.0 # design px around a chip sprite: the shadow and the tilt
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@lru_cache(maxsize=32)
|
|
304
|
+
def _chip_sprite(params_json: str, box: E.Box, k: int) -> tuple:
|
|
305
|
+
"""Word k on its chip, with the shadow, tilted, drawn ONCE at the render scale.
|
|
306
|
+
|
|
307
|
+
A tilted group costs a full-layer rotation per frame (about 20 ms at 1080x1920); the chip never changes
|
|
308
|
+
shape, so every frame only scales and pastes this sprite. Returns (premultiplied image, absolute render px
|
|
309
|
+
of its top-left, chip center inside it)."""
|
|
310
|
+
L = _layout(params_json, box)
|
|
311
|
+
w = L["words"][k]
|
|
312
|
+
x, y, cw, ch = w["chip"]
|
|
313
|
+
sb = E.with_box(box, x=box.x + x, y=box.y + y, w=cw, h=ch, bleed=SPRITE_PAD)
|
|
314
|
+
sc = E.Canvas(sb)
|
|
315
|
+
with sc.group(rotate=w["tilt"], pivot=(cw / 2.0, ch / 2.0)):
|
|
316
|
+
sc.rect(0.0, 0.0, cw, ch, radius=L["chip_radius"], fill=w["chip_fill"], shadow=L["chip_shadow"])
|
|
317
|
+
sc.text(w["x"] - x, w["base"] - y, w["text"], L["font"], L["size"], color=w["color"])
|
|
318
|
+
cx, cy = sc.px(cw / 2.0, ch / 2.0)
|
|
319
|
+
return sc.image.convert("RGBa"), sc.origin_x, sc.origin_y, cx, cy
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _word_chip(cv: E.Canvas, L: dict, w: dict, t: float, params_json: str) -> None:
|
|
323
|
+
tk = t - L["hits"][w["k"]]
|
|
324
|
+
op = _opacity(tk)
|
|
325
|
+
if not cv.visible(op):
|
|
326
|
+
return
|
|
327
|
+
img, ox, oy, cx, cy = _chip_sprite(params_json, cv.box, w["k"])
|
|
328
|
+
s = _scale(tk)
|
|
329
|
+
left, top = float(ox), float(oy)
|
|
330
|
+
if abs(s - 1.0) > 1e-4:
|
|
331
|
+
size = (max(1, int(round(img.width * s))), max(1, int(round(img.height * s))))
|
|
332
|
+
sx, sy = size[0] / img.width, size[1] / img.height
|
|
333
|
+
img = img.resize(size, Image.BICUBIC)
|
|
334
|
+
left, top = ox + cx - cx * sx, oy + cy - cy * sy # the chip center stays in place
|
|
335
|
+
sprite = img.convert("RGBA")
|
|
336
|
+
k = cv.box.scale
|
|
337
|
+
cv.paste_image(sprite, left / k - cv.box.x, top / k - cv.box.y, w=sprite.width / k, h=sprite.height / k,
|
|
338
|
+
opacity=op)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _rays(cv: E.Canvas, landing: dict, t: float) -> None:
|
|
342
|
+
for ray in landing["rays"]:
|
|
343
|
+
u = E.progress(t, landing["hit"] + ray["delay"], BURST_T)
|
|
344
|
+
if u <= 0 or u >= 1:
|
|
345
|
+
continue
|
|
346
|
+
head = E.out_cubic(min(1.0, u * 1.5))
|
|
347
|
+
tail = E.in_cubic(u)
|
|
348
|
+
if (head - tail) * math.dist(ray["p0"], ray["p1"]) < 10.0: # no leftover dots
|
|
349
|
+
continue
|
|
350
|
+
cv.polyline([ray["p0"], ray["p1"]], ray["width"], color="accent", start=tail, progress=head)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
354
|
+
key = json.dumps(params, sort_keys=True)
|
|
355
|
+
L = _layout(key, box)
|
|
356
|
+
cv = E.Canvas(box, full=params["flash"] == "tela")
|
|
357
|
+
t = (frame_index + 1) / fps # entrances run one frame early: frame 0 already shows the first word
|
|
358
|
+
|
|
359
|
+
if params["flash"] != "nenhum":
|
|
360
|
+
color = "white" if params["flash_color"] == "branco" else "accent"
|
|
361
|
+
for landing in L["landings"]:
|
|
362
|
+
fl = _flash(landing, t)
|
|
363
|
+
if fl <= 0.002:
|
|
364
|
+
continue
|
|
365
|
+
if params["flash"] == "tela":
|
|
366
|
+
cv.fill(color, fl)
|
|
367
|
+
else:
|
|
368
|
+
# a flat glow behind the words
|
|
369
|
+
x0, y0, x1, y1 = landing["area"]
|
|
370
|
+
m = cv.rect_mask(x0 - 60, y0 - 12, x1 - x0 + 120, y1 - y0 + 24, radius=(y1 - y0 + 24) / 2.0)
|
|
371
|
+
if m is not None:
|
|
372
|
+
cv.fill_mask(E.blur_mask(m, cv.len(55), cv.W, cv.H), color, min(1.0, fl * 1.2))
|
|
373
|
+
|
|
374
|
+
dx = dy = 0.0
|
|
375
|
+
if params["shake"]:
|
|
376
|
+
amp = 16.0 if L["size"] < 200 else 20.0
|
|
377
|
+
for j, landing in enumerate(L["landings"]):
|
|
378
|
+
sx, sy = E.shake(t, landing["hit"], SHAKE_T, amplitude=amp * (1.0 if j == 0 else 0.7),
|
|
379
|
+
frequency=20.0, seed=("palavra_impacto", j))
|
|
380
|
+
dx, dy = dx + sx, dy + sy
|
|
381
|
+
|
|
382
|
+
with cv.group(dx=dx, dy=dy):
|
|
383
|
+
if params["burst"]:
|
|
384
|
+
for landing in L["landings"]:
|
|
385
|
+
_rays(cv, landing, t)
|
|
386
|
+
for w in L["words"]:
|
|
387
|
+
if L["tarja"]:
|
|
388
|
+
_word_chip(cv, L, w, t, key)
|
|
389
|
+
else:
|
|
390
|
+
_word_outline(cv, L, w, t)
|
|
391
|
+
return cv
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
395
|
+
name = SOUNDS[params["sound"]]
|
|
396
|
+
hits = sorted(set(_hit_times(_word_count(params), params["stagger"], fps)))
|
|
397
|
+
# the landing is drawn on the frame where (frame + 1) / fps == hit
|
|
398
|
+
return [{"frame": max(0, int(round(h * fps)) - 1), "name": name, "volume": 1.0 if j == 0 else 0.8,
|
|
399
|
+
"why": "palavra batendo"} for j, h in enumerate(hits)]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def suggested_duration(params: dict) -> float:
|
|
403
|
+
"""The last landing and about 1.4 s to read."""
|
|
404
|
+
last = (_word_count(params) - 1) * params["stagger"] + T_HIT
|
|
405
|
+
return round(max(TEMPLATE["default_duration"], last + 1.4), 2)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def settle(params: dict, fps: int) -> float:
|
|
409
|
+
last = max(_hit_times(_word_count(params), params["stagger"], fps))
|
|
410
|
+
return last - 1.0 / fps + max(T_SETTLE, SHAKE_T, BURST_T + 0.04, FLASH_T) + 0.02
|