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,461 @@
|
|
|
1
|
+
"""etiqueta_preco: a price tag that pops in with a spring and settles slightly tilted, with the cents raised
|
|
2
|
+
like a shop price, an optional old price crossed out above it and a shine that sweeps the tag once
|
|
3
|
+
(SPEC Contract 12). Two looks: "etiqueta" (hang tag with a hole) and "selo" (round scalloped seal).
|
|
4
|
+
|
|
5
|
+
`_add_shine` is a soft diagonal light band on an image and `_transform` scales and rotates the small cached
|
|
6
|
+
tag sprite in one resampling step (a rotated group would redraw and resample its layer on every frame).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from PIL import Image
|
|
17
|
+
|
|
18
|
+
from motion import engine as E
|
|
19
|
+
|
|
20
|
+
TEMPLATE = {
|
|
21
|
+
"id": "etiqueta_preco",
|
|
22
|
+
"label_pt": "Etiqueta de preço",
|
|
23
|
+
"description_pt": "Uma etiqueta de preço na cor do vídeo que entra com um pulo e fica levemente inclinada, "
|
|
24
|
+
"com os centavos pequenos no alto. Opcional: o preço antigo riscado em cima e uma frase "
|
|
25
|
+
"curta embaixo do preço.",
|
|
26
|
+
"use_when_pt": "Quando a fala diz o preço da oferta (\"por apenas 37 reais\") ou compara com o preço antigo. "
|
|
27
|
+
"O preço antigo precisa ser o de verdade.",
|
|
28
|
+
"category": "oferta",
|
|
29
|
+
"version": 1,
|
|
30
|
+
"params": {
|
|
31
|
+
"price": {"type": "number", "default": 37, "min": 0, "max": 1e9,
|
|
32
|
+
"label_pt": "Preço (use ponto para os centavos: 37.90)"},
|
|
33
|
+
"old_price": {"type": "number", "default": None, "nullable": True, "min": 0, "max": 1e9,
|
|
34
|
+
"label_pt": "Preço antigo, riscado em cima (vazio: sem preço antigo)"},
|
|
35
|
+
"prefix": {"type": "text", "default": "R$", "max_len": 4, "label_pt": "Moeda antes do preço, ex.: R$ ou US$"},
|
|
36
|
+
"decimals": {"type": "integer", "default": None, "nullable": True, "min": 0, "max": 2,
|
|
37
|
+
"label_pt": "Casas decimais (vazio: centavos só quando o preço tem centavos)"},
|
|
38
|
+
"label": {"type": "text", "default": "por apenas", "max_len": 20,
|
|
39
|
+
"label_pt": "Texto pequeno em cima do preço (vazio: sem texto)"},
|
|
40
|
+
"note": {"type": "text", "default": "", "max_len": 26,
|
|
41
|
+
"label_pt": "Frase curta embaixo do preço, ex.: \"à vista no PIX\" ou \"12x de R$ 3,70\""},
|
|
42
|
+
"old_label": {"type": "text", "default": "de", "max_len": 8, "label_pt": "Palavra antes do preço antigo"},
|
|
43
|
+
"mode": {"type": "choice", "choices": ["etiqueta", "selo"], "default": "etiqueta",
|
|
44
|
+
"label_pt": "etiqueta (com furinho) ou selo (redondo com borda ondulada)"},
|
|
45
|
+
"tilt": {"type": "number", "default": -4, "min": -12, "max": 12,
|
|
46
|
+
"label_pt": "Inclinação final em graus (negativo sobe para a direita)"},
|
|
47
|
+
"shine": {"type": "bool", "default": True, "label_pt": "Brilho que passa pela etiqueta uma vez"},
|
|
48
|
+
"tag_delay": {"type": "number", "default": 0.62, "min": 0.45, "max": 5,
|
|
49
|
+
"label_pt": "Com preço antigo: segundos entre o preço antigo aparecer e a etiqueta pular "
|
|
50
|
+
"(para bater com a fala \"de 97... por 37\")"},
|
|
51
|
+
},
|
|
52
|
+
"default_duration": 3.0,
|
|
53
|
+
"default_region": "baixo",
|
|
54
|
+
"sfx": [{"at": 0.0, "name": "cash"}],
|
|
55
|
+
"example": {"price": 37.9, "old_price": 97, "note": "à vista no PIX"},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
SPRING = (0.5, 2.4) # damping, frequency of the pop (about 16% overshoot)
|
|
59
|
+
SPRING_T = E.spring_settle(*SPRING) # seconds until it rests
|
|
60
|
+
STRIKE_AT, STRIKE_DUR = 0.30, 0.28 # the old price gets crossed
|
|
61
|
+
SHINE_AT, SHINE_DUR = 0.42, 0.62 # after the pop
|
|
62
|
+
POP_SWING = -9.0 # extra degrees at the start of the pop
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _tag_start(p: dict) -> float:
|
|
66
|
+
"""The tag pops at the item start, or `tag_delay` after the old price (which appears at the start)."""
|
|
67
|
+
return p["tag_delay"] if p["old_price"] is not None else 0.0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _decimals(v: float, fixed) -> int:
|
|
71
|
+
if fixed is not None:
|
|
72
|
+
return int(fixed)
|
|
73
|
+
return 2 if abs(v - round(v)) > 1e-9 else 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _price_parts(p: dict) -> tuple[str, str, str]:
|
|
77
|
+
dec = _decimals(p["price"], p["decimals"])
|
|
78
|
+
s = E.format_number(p["price"], dec)
|
|
79
|
+
whole, _, cents = s.partition(",")
|
|
80
|
+
return p["prefix"].strip(), whole, ("," + cents) if cents else ""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------ layout (once per item)
|
|
84
|
+
|
|
85
|
+
@lru_cache(maxsize=64)
|
|
86
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
87
|
+
p = json.loads(params_json)
|
|
88
|
+
prefix, whole, cents = _price_parts(p)
|
|
89
|
+
label = p["label"].strip()
|
|
90
|
+
note = p["note"].strip()
|
|
91
|
+
selo = p["mode"] == "selo"
|
|
92
|
+
theme = box.theme
|
|
93
|
+
|
|
94
|
+
def price_row(P: float) -> dict:
|
|
95
|
+
pre_s, cen_s = P * 0.30, P * 0.42
|
|
96
|
+
pre_w = E.measure(prefix, "sans_black", pre_s).width if prefix else 0.0
|
|
97
|
+
gap_p = P * 0.08 if prefix else 0.0
|
|
98
|
+
num_w = E.measure(whole, "sans_black", P).width
|
|
99
|
+
gap_c = P * 0.03 if cents else 0.0
|
|
100
|
+
cen_w = E.measure(cents, "sans_black", cen_s).width if cents else 0.0
|
|
101
|
+
return {"P": P, "pre_s": pre_s, "cen_s": cen_s, "pre_w": pre_w, "gap_p": gap_p, "num_w": num_w,
|
|
102
|
+
"gap_c": gap_c, "cen_w": cen_w, "w": pre_w + gap_p + num_w + gap_c + cen_w,
|
|
103
|
+
"cap": E.measure("0", "sans_black", P).cap}
|
|
104
|
+
|
|
105
|
+
# old price pill (above the tag)
|
|
106
|
+
old = None
|
|
107
|
+
if p["old_price"] is not None:
|
|
108
|
+
dec_o = _decimals(p["old_price"], p["decimals"])
|
|
109
|
+
o_label = p["old_label"].strip()
|
|
110
|
+
o_price = (prefix + " " if prefix else "") + E.format_number(p["old_price"], dec_o)
|
|
111
|
+
o_size = E.fit_text(f"{o_label} {o_price}", "text_bold", box.w - 80.0, 44.0, 28.0)
|
|
112
|
+
lab_w = E.measure(o_label, "text_bold", o_size).width if o_label else 0.0
|
|
113
|
+
gap = o_size * 0.28 if o_label else 0.0
|
|
114
|
+
pr_w = E.measure(o_price, "text_bold", o_size).width
|
|
115
|
+
cap = E.measure("H", "text_bold", o_size).cap
|
|
116
|
+
pad_x, pad_y = 30.0, 22.0
|
|
117
|
+
old = {"size": o_size, "label": o_label, "price": o_price, "lab_w": lab_w, "gap": gap, "pr_w": pr_w,
|
|
118
|
+
"cap": cap, "w": lab_w + gap + pr_w + 2 * pad_x, "h": cap + 2 * pad_y, "pad_x": pad_x}
|
|
119
|
+
pill_block = (old["h"] + 30.0) if old else 0.0
|
|
120
|
+
|
|
121
|
+
lab_size, note_size = 32.0, 34.0
|
|
122
|
+
lab_cap = E.measure("H", "text_bold", lab_size).cap if label else 0.0
|
|
123
|
+
note_cap = E.measure("H", "text_semibold", note_size).cap if note else 0.0
|
|
124
|
+
lab_w = E.measure(label.upper(), "text_bold", lab_size, tracking=2.5).width if label else 0.0
|
|
125
|
+
note_w = E.measure(note, "text_semibold", note_size).width if note else 0.0
|
|
126
|
+
gap1 = 20.0 if label else 0.0
|
|
127
|
+
gap2 = 24.0 if note else 0.0
|
|
128
|
+
|
|
129
|
+
avail_h = box.h - pill_block - 12.0
|
|
130
|
+
if selo:
|
|
131
|
+
# round seal: the content sits in the middle 70% of the circle
|
|
132
|
+
P = 150.0
|
|
133
|
+
row = price_row(P)
|
|
134
|
+
for _ in range(3):
|
|
135
|
+
stack_h = lab_cap + gap1 + row["cap"] + gap2 + note_cap
|
|
136
|
+
need = max(row["w"] / 1.28, lab_w / 1.2, note_w / 1.2) # inside the inner ring
|
|
137
|
+
R = max(need, stack_h / 1.25 + 10.0, 150.0)
|
|
138
|
+
R_max = min(box.w * 0.5 - 30.0, avail_h / 2.0)
|
|
139
|
+
if R <= R_max:
|
|
140
|
+
break
|
|
141
|
+
k = R_max / R
|
|
142
|
+
P = max(60.0, P * k)
|
|
143
|
+
row = price_row(P)
|
|
144
|
+
if label:
|
|
145
|
+
lab_size = max(24.0, lab_size * k)
|
|
146
|
+
if note:
|
|
147
|
+
note_size = max(24.0, note_size * k)
|
|
148
|
+
lab_cap = E.measure("H", "text_bold", lab_size).cap if label else 0.0
|
|
149
|
+
note_cap = E.measure("H", "text_semibold", note_size).cap if note else 0.0
|
|
150
|
+
lab_w = E.measure(label.upper(), "text_bold", lab_size, tracking=2.5).width if label else 0.0
|
|
151
|
+
note_w = E.measure(note, "text_semibold", note_size).width if note else 0.0
|
|
152
|
+
R = min(R, R_max)
|
|
153
|
+
tag_w = tag_h = 2 * R
|
|
154
|
+
stack_h = lab_cap + gap1 + row["cap"] + gap2 + note_cap
|
|
155
|
+
block_h = pill_block + tag_h
|
|
156
|
+
top = (box.h - block_h) / 2.0
|
|
157
|
+
tag_x = (box.w - tag_w) / 2.0
|
|
158
|
+
tag_y = top + pill_block
|
|
159
|
+
content_cx = box.w / 2.0
|
|
160
|
+
content_top = tag_y + R - stack_h / 2.0
|
|
161
|
+
shape = {"kind": "selo", "cx": content_cx, "cy": tag_y + R, "R": R}
|
|
162
|
+
else:
|
|
163
|
+
pad_t, pad_b = (34.0, 36.0) if (label or note) else (44.0, 44.0)
|
|
164
|
+
pad_l, pad_r = 36.0, 52.0
|
|
165
|
+
P = 180.0
|
|
166
|
+
row = price_row(P)
|
|
167
|
+
# fit the tilted tag (its rotated bounding box) in the box
|
|
168
|
+
ca, sa = math.cos(math.radians(abs(p["tilt"]))), math.sin(math.radians(abs(p["tilt"])))
|
|
169
|
+
for _ in range(6):
|
|
170
|
+
tag_h = pad_t + lab_cap + gap1 + row["cap"] + gap2 + note_cap + pad_b
|
|
171
|
+
point_w = tag_h * 0.30
|
|
172
|
+
inner = max(row["w"], lab_w, note_w)
|
|
173
|
+
tag_w = max(point_w + pad_l + inner + pad_r, 420.0)
|
|
174
|
+
rot_w, rot_h = tag_w * ca + tag_h * sa, tag_w * sa + tag_h * ca
|
|
175
|
+
k = min((box.w - 24.0) / rot_w, avail_h / rot_h, 1.0)
|
|
176
|
+
if k >= 0.995:
|
|
177
|
+
break
|
|
178
|
+
k = max(k, 0.5) * 0.99
|
|
179
|
+
P = max(60.0, P * k)
|
|
180
|
+
row = price_row(P)
|
|
181
|
+
if label:
|
|
182
|
+
lab_size = max(24.0, lab_size * k)
|
|
183
|
+
lab_cap = E.measure("H", "text_bold", lab_size).cap
|
|
184
|
+
lab_w = E.measure(label.upper(), "text_bold", lab_size, tracking=2.5).width
|
|
185
|
+
if note:
|
|
186
|
+
note_size = max(24.0, note_size * k)
|
|
187
|
+
note_cap = E.measure("H", "text_semibold", note_size).cap
|
|
188
|
+
note_w = E.measure(note, "text_semibold", note_size).width
|
|
189
|
+
inner = tag_w - point_w - pad_l - pad_r
|
|
190
|
+
rot_h = tag_w * sa + tag_h * ca
|
|
191
|
+
block_h = pill_block + rot_h
|
|
192
|
+
top = (box.h - block_h) / 2.0
|
|
193
|
+
tag_x = (box.w - tag_w) / 2.0
|
|
194
|
+
tag_y = top + pill_block + rot_h / 2.0 - tag_h / 2.0
|
|
195
|
+
body_x = tag_x + point_w
|
|
196
|
+
content_cx = body_x + pad_l + inner / 2.0
|
|
197
|
+
content_top = tag_y + pad_t
|
|
198
|
+
r = 16.0
|
|
199
|
+
pts = [(body_x, tag_y), (tag_x + tag_w, tag_y), (tag_x + tag_w, tag_y + tag_h), (body_x, tag_y + tag_h),
|
|
200
|
+
(tag_x, tag_y + tag_h / 2.0)]
|
|
201
|
+
shape = {"kind": "etiqueta", "pts": pts, "radius": r,
|
|
202
|
+
"hole": (tag_x + point_w * 0.62, tag_y + tag_h / 2.0), "hole_r": max(9.0, tag_h * 0.055)}
|
|
203
|
+
|
|
204
|
+
y = content_top
|
|
205
|
+
lab_base = y + lab_cap
|
|
206
|
+
price_top = lab_base + gap1 if label else y
|
|
207
|
+
price_base = price_top + row["cap"]
|
|
208
|
+
note_base = price_base + gap2 + note_cap
|
|
209
|
+
tag_cx, tag_cy = tag_x + tag_w / 2.0, tag_y + tag_h / 2.0
|
|
210
|
+
out = {
|
|
211
|
+
"prefix": prefix, "whole": whole, "cents": cents, "label": label.upper(), "note": note,
|
|
212
|
+
"lab_size": lab_size, "note_size": note_size, "lab_base": lab_base, "note_base": note_base,
|
|
213
|
+
"row": row, "price_x": content_cx - row["w"] / 2.0, "price_top": price_top, "price_base": price_base,
|
|
214
|
+
"content_cx": content_cx, "tag": (tag_x, tag_y, tag_w, tag_h), "pivot": (tag_cx, tag_cy),
|
|
215
|
+
"shape": shape, "old": old, "on_accent": theme.on_accent,
|
|
216
|
+
}
|
|
217
|
+
if old:
|
|
218
|
+
ox = tag_cx - old["w"] / 2.0 if selo else min(max(tag_x + 10.0, tag_cx - old["w"] / 2.0),
|
|
219
|
+
box.w - old["w"])
|
|
220
|
+
oy = top
|
|
221
|
+
out["old_box"] = (ox, oy, old["w"], old["h"])
|
|
222
|
+
tx = ox + old["pad_x"]
|
|
223
|
+
out["old_lab_x"] = tx
|
|
224
|
+
out["old_pr_x"] = tx + old["lab_w"] + old["gap"]
|
|
225
|
+
out["old_base"] = oy + (old["h"] + old["cap"]) / 2.0
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------------ drawing
|
|
230
|
+
#
|
|
231
|
+
# The tag is drawn flat once, on a small canvas of its own (vector, scale 1, no tilt). Each frame of the pop
|
|
232
|
+
# scales and rotates that sprite in one resampling step and casts its shadow; the resting result (scale 1,
|
|
233
|
+
# final tilt, shadow) is cached, so the shine frames only add the light band. The engine's rotated group
|
|
234
|
+
# resamples its whole layer instead (about 40 ms a frame for this item).
|
|
235
|
+
|
|
236
|
+
TAG_SHADOW = E.Shadow(blur=26.0, dy=16.0, opacity=0.40)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _draw_shape(cv: E.Canvas, L: dict) -> None:
|
|
240
|
+
sh = L["shape"]
|
|
241
|
+
ink = L["on_accent"]
|
|
242
|
+
if sh["kind"] == "etiqueta":
|
|
243
|
+
cv.fill_mask(cv.polygon_mask(sh["pts"], radius=sh["radius"]), "accent")
|
|
244
|
+
# a stitched line inside the edge
|
|
245
|
+
tx, ty, tw, th = L["tag"]
|
|
246
|
+
inset = 14.0
|
|
247
|
+
body_x = sh["pts"][0][0]
|
|
248
|
+
ip = [(body_x + inset * 0.45, ty + inset), (tx + tw - inset, ty + inset), (tx + tw - inset, ty + th - inset),
|
|
249
|
+
(body_x + inset * 0.45, ty + th - inset), (tx + inset * 2.1, ty + th / 2.0)]
|
|
250
|
+
ip.append(ip[0])
|
|
251
|
+
total = E.path_length(ip)
|
|
252
|
+
dash, gap = 16.0, 11.0
|
|
253
|
+
k = 0.0
|
|
254
|
+
while k < total - 2.0:
|
|
255
|
+
cv.polyline(E.trim_points(ip, k / total, min(total, k + dash) / total), width=3.0,
|
|
256
|
+
color=(ink[0], ink[1], ink[2], 60))
|
|
257
|
+
k += dash + gap
|
|
258
|
+
hx, hy = sh["hole"]
|
|
259
|
+
cv.circle(hx, hy, sh["hole_r"] + 7.0, fill=(ink[0], ink[1], ink[2], 40))
|
|
260
|
+
else:
|
|
261
|
+
cx, cy, R = sh["cx"], sh["cy"], sh["R"]
|
|
262
|
+
bumps = 20
|
|
263
|
+
parts = [cv.circle_mask(cx, cy, R * 0.90)]
|
|
264
|
+
for i in range(bumps):
|
|
265
|
+
a = 2 * math.pi * i / bumps
|
|
266
|
+
parts.append(cv.circle_mask(cx + math.cos(a) * R * 0.86, cy + math.sin(a) * R * 0.86, R * 0.14))
|
|
267
|
+
cv.fill_mask(E.union_masks(parts), "accent")
|
|
268
|
+
cv.ring(cx, cy, R * 0.74, 3.0, color=(ink[0], ink[1], ink[2], 60))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _draw_text(cv: E.Canvas, L: dict) -> None:
|
|
272
|
+
row = L["row"]
|
|
273
|
+
ink = "on_accent"
|
|
274
|
+
if L["label"]:
|
|
275
|
+
cv.text(L["content_cx"], L["lab_base"], L["label"], "text_bold", L["lab_size"], color=ink, opacity=0.78,
|
|
276
|
+
anchor="ms", tracking=2.5)
|
|
277
|
+
x = L["price_x"]
|
|
278
|
+
top = L["price_top"]
|
|
279
|
+
if L["prefix"]:
|
|
280
|
+
cv.text(x, top, L["prefix"], "sans_black", row["pre_s"], color=ink, anchor="lt")
|
|
281
|
+
x += row["pre_w"] + row["gap_p"]
|
|
282
|
+
cv.text(x, L["price_base"], L["whole"], "sans_black", row["P"], color=ink)
|
|
283
|
+
x += row["num_w"] + row["gap_c"]
|
|
284
|
+
if L["cents"]:
|
|
285
|
+
cv.text(x, top, L["cents"], "sans_black", row["cen_s"], color=ink, anchor="lt")
|
|
286
|
+
if L["note"]:
|
|
287
|
+
cv.text(L["content_cx"], L["note_base"], L["note"], "text_semibold", L["note_size"], color=ink,
|
|
288
|
+
opacity=0.86, anchor="ms")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@lru_cache(maxsize=8)
|
|
292
|
+
def _flat_sprite(params_json: str, box: E.Box) -> tuple[Image.Image, int, int]:
|
|
293
|
+
"""The flat tag (no tilt, no shadow, scale 1): (RGBA image, x, y) in render px of the output canvas."""
|
|
294
|
+
L = _layout(params_json, box)
|
|
295
|
+
tx, ty, tw, th = L["tag"]
|
|
296
|
+
m = 6.0
|
|
297
|
+
sub = E.Canvas(E.with_box(box, x=box.x + tx - m, y=box.y + ty - m, w=tw + 2 * m, h=th + 2 * m, bleed=0.0))
|
|
298
|
+
with sub.group(dx=-(tx - m), dy=-(ty - m)): # main-box coordinates inside the sprite
|
|
299
|
+
_draw_shape(sub, L)
|
|
300
|
+
_draw_text(sub, L)
|
|
301
|
+
if L["shape"]["kind"] == "etiqueta":
|
|
302
|
+
hx, hy = L["shape"]["hole"]
|
|
303
|
+
sub.erase_circle(hx, hy, L["shape"]["hole_r"])
|
|
304
|
+
return sub.image, sub.origin_x, sub.origin_y
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _transform(sprite: Image.Image, ax: int, ay: int, pivot: tuple[float, float], scale: float, angle: float,
|
|
308
|
+
resample) -> tuple[Image.Image, int, int]:
|
|
309
|
+
"""Scale and rotate (clockwise degrees) a sprite around a render-px point of the output canvas.
|
|
310
|
+
Returns the new image and its render-px position."""
|
|
311
|
+
px, py = pivot
|
|
312
|
+
if scale < 0.85: # shrink with a proper filter first (no aliasing on the small first frames)
|
|
313
|
+
w = max(1, int(round(sprite.width * scale)))
|
|
314
|
+
h = max(1, int(round(sprite.height * scale)))
|
|
315
|
+
sx, sy = w / sprite.width, h / sprite.height
|
|
316
|
+
sprite = sprite.convert("RGBa").resize((w, h), Image.LANCZOS).convert("RGBA")
|
|
317
|
+
ax, ay = px + (ax - px) * sx, py + (ay - py) * sy
|
|
318
|
+
scale = scale / ((sx + sy) / 2.0)
|
|
319
|
+
th = math.radians(angle)
|
|
320
|
+
cs, sn = math.cos(th), math.sin(th)
|
|
321
|
+
corners = [(ax, ay), (ax + sprite.width, ay), (ax, ay + sprite.height), (ax + sprite.width, ay + sprite.height)]
|
|
322
|
+
rx = [px + scale * ((x - px) * cs - (y - py) * sn) for x, y in corners]
|
|
323
|
+
ry = [py + scale * ((x - px) * sn + (y - py) * cs) for x, y in corners]
|
|
324
|
+
bx0, by0 = int(math.floor(min(rx))) - 1, int(math.floor(min(ry))) - 1
|
|
325
|
+
bx1, by1 = int(math.ceil(max(rx))) + 1, int(math.ceil(max(ry))) + 1
|
|
326
|
+
if abs(angle) < 1e-6 and abs(scale - 1.0) < 1e-9 and float(ax).is_integer() and float(ay).is_integer():
|
|
327
|
+
return sprite, int(ax), int(ay)
|
|
328
|
+
# output pixel (X, Y) -> sprite pixel: undo the rotation and the scale around the pivot
|
|
329
|
+
k = 1.0 / scale
|
|
330
|
+
coeffs = (cs * k, sn * k, px + (cs * (bx0 - px) + sn * (by0 - py)) * k - ax,
|
|
331
|
+
-sn * k, cs * k, py + (-sn * (bx0 - px) + cs * (by0 - py)) * k - ay)
|
|
332
|
+
out = sprite.convert("RGBa").transform((bx1 - bx0, by1 - by0), Image.AFFINE, coeffs, resample=resample)
|
|
333
|
+
return out.convert("RGBA"), bx0, by0
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _shadow(cv_like: E.Canvas, img: Image.Image, x: int, y: int):
|
|
337
|
+
"""Blurred alpha of a placed image, moved down (a Mask in the canvas' render px)."""
|
|
338
|
+
alpha = np.asarray(img.getchannel("A"), np.float32) / 255.0
|
|
339
|
+
sh = E.blur_mask(E.Mask(alpha, x - cv_like.origin_x, y - cv_like.origin_y), cv_like.len(TAG_SHADOW.blur),
|
|
340
|
+
cv_like.W, cv_like.H)
|
|
341
|
+
sh.y += int(round(cv_like.len(TAG_SHADOW.dy)))
|
|
342
|
+
return sh
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@lru_cache(maxsize=8)
|
|
346
|
+
def _resting(params_json: str, box: E.Box):
|
|
347
|
+
"""The tag at rest (scale 1, final tilt, high quality resampling) and its shadow mask."""
|
|
348
|
+
L = _layout(params_json, box)
|
|
349
|
+
p = json.loads(params_json)
|
|
350
|
+
sprite, ax, ay = _flat_sprite(params_json, box)
|
|
351
|
+
pv = ((box.x + L["pivot"][0]) * box.scale, (box.y + L["pivot"][1]) * box.scale)
|
|
352
|
+
img, x, y = _transform(sprite, ax, ay, pv, 1.0, p["tilt"], Image.BICUBIC)
|
|
353
|
+
return img, x, y, _shadow(E.Canvas(box), img, x, y)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _add_shine(img: Image.Image, progress: float, width_px: float, opacity: float, accent) -> Image.Image:
|
|
357
|
+
"""A soft light band tilted 20 degrees crossing the tag, straight alpha. It lights the accent surface only:
|
|
358
|
+
the printed text and the hole stay as they are."""
|
|
359
|
+
arr = np.asarray(img)
|
|
360
|
+
h, w = arr.shape[:2]
|
|
361
|
+
xs = np.arange(w, dtype=np.float32)[None, :] + 0.5
|
|
362
|
+
ys = np.arange(h, dtype=np.float32)[:, None] + 0.5
|
|
363
|
+
half = max(1.0, width_px / 2.0)
|
|
364
|
+
u = xs + (ys - h / 2.0) * math.tan(math.radians(20))
|
|
365
|
+
c = -half * 2 + (w + half * 4) * E.in_out_sine(progress)
|
|
366
|
+
band = np.clip(1.0 - np.abs(u - c) / half, 0.0, 1.0) ** 1.5
|
|
367
|
+
out = arr.astype(np.float32)
|
|
368
|
+
a_dst = out[..., 3] / 255.0
|
|
369
|
+
dist = np.abs(out[..., :3] - np.array(accent[:3], np.float32)).max(axis=2)
|
|
370
|
+
surface = np.clip(1.0 - dist / 90.0, 0.0, 1.0) # 1 on the tag color, 0 on the text
|
|
371
|
+
a_src = band * opacity * a_dst * surface
|
|
372
|
+
a_out = a_src + a_dst * (1.0 - a_src)
|
|
373
|
+
safe = np.where(a_out > 0, a_out, 1.0)
|
|
374
|
+
for c_i in range(3):
|
|
375
|
+
out[..., c_i] = (255.0 * a_src + out[..., c_i] * a_dst * (1.0 - a_src)) / safe
|
|
376
|
+
out[..., 3] = a_out * 255.0
|
|
377
|
+
return Image.fromarray(np.clip(out + 0.5, 0, 255).astype(np.uint8), "RGBA")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
381
|
+
cv = E.Canvas(box)
|
|
382
|
+
t = frame_index / fps
|
|
383
|
+
key = json.dumps(params, sort_keys=True)
|
|
384
|
+
L = _layout(key, box)
|
|
385
|
+
t_in = t + 1.0 / fps
|
|
386
|
+
d = 0.0
|
|
387
|
+
T = _tag_start(params)
|
|
388
|
+
|
|
389
|
+
# the tag: spring pop and swing into its tilt
|
|
390
|
+
local = t_in - T
|
|
391
|
+
if local > 0:
|
|
392
|
+
sp = 1.0 if local >= SPRING_T else E.spring(local, *SPRING)
|
|
393
|
+
alpha = E.clamp01(local / 0.08)
|
|
394
|
+
scale = 0.5 + 0.5 * sp # peaks near 1.08: a small bounce
|
|
395
|
+
angle = params["tilt"] + POP_SWING * (1.0 - sp)
|
|
396
|
+
if sp == 1.0:
|
|
397
|
+
img, x, y, shadow = _resting(key, box)
|
|
398
|
+
else:
|
|
399
|
+
sprite, ax, ay = _flat_sprite(key, box)
|
|
400
|
+
pv = ((box.x + L["pivot"][0]) * box.scale, (box.y + L["pivot"][1]) * box.scale)
|
|
401
|
+
img, x, y = _transform(sprite, ax, ay, pv, scale, angle, Image.BILINEAR)
|
|
402
|
+
shadow = _shadow(cv, img, x, y)
|
|
403
|
+
shine_p = E.progress(t, T + SHINE_AT, SHINE_DUR)
|
|
404
|
+
if params["shine"] and 0.0 < shine_p < 1.0:
|
|
405
|
+
img = _add_shine(img, shine_p, cv.len(L["tag"][3] * 0.55), 0.42, box.theme.accent)
|
|
406
|
+
cv.fill_mask(shadow, (0, 0, 0), TAG_SHADOW.opacity * alpha)
|
|
407
|
+
if L["shape"]["kind"] == "etiqueta": # the hole shows the video, not the shadow
|
|
408
|
+
hx, hy = L["shape"]["hole"]
|
|
409
|
+
pvx, pvy = L["pivot"]
|
|
410
|
+
ca, sa = math.cos(math.radians(angle)), math.sin(math.radians(angle))
|
|
411
|
+
dx, dy = hx - pvx, hy - pvy
|
|
412
|
+
cv.erase_circle(pvx + scale * (dx * ca - dy * sa), pvy + scale * (dx * sa + dy * ca),
|
|
413
|
+
L["shape"]["hole_r"] * scale)
|
|
414
|
+
s = box.scale
|
|
415
|
+
cv.paste_image(img, x / s - box.x, y / s - box.y, opacity=alpha)
|
|
416
|
+
|
|
417
|
+
# the old price: slides in, gets crossed, then steps back a little
|
|
418
|
+
old = L["old"]
|
|
419
|
+
if old:
|
|
420
|
+
q = t_in - d
|
|
421
|
+
if q > 0:
|
|
422
|
+
a = E.eased(q, 0.0, 0.18, "out_quad") * (1.0 - 0.22 * E.eased(t, T, 0.3))
|
|
423
|
+
s = 0.9 + 0.1 * E.out_back(E.progress(q, 0.0, 0.4), 1.6)
|
|
424
|
+
ox, oy, ow, oh = L["old_box"]
|
|
425
|
+
with cv.group(opacity=a, scale=s, dy=18.0 * (1.0 - E.eased(q, 0.0, 0.35)),
|
|
426
|
+
pivot=(ox + ow / 2.0, oy + oh / 2.0)):
|
|
427
|
+
cv.card(ox, oy, ow, oh, radius=oh / 2.0)
|
|
428
|
+
base = L["old_base"]
|
|
429
|
+
if old["label"]:
|
|
430
|
+
cv.text(L["old_lab_x"], base, old["label"], "text_bold", old["size"], color="muted")
|
|
431
|
+
cv.text(L["old_pr_x"], base, old["price"], "text_bold", old["size"], color="text")
|
|
432
|
+
k = E.eased(t, d + STRIKE_AT, STRIKE_DUR, "in_out_cubic")
|
|
433
|
+
if k > 0:
|
|
434
|
+
x0 = L["old_pr_x"] - 8.0
|
|
435
|
+
x1 = L["old_pr_x"] + old["pr_w"] + 8.0
|
|
436
|
+
mid = base - old["cap"] * 0.42
|
|
437
|
+
cv.line(x0, mid + 7.0, x1, mid - 7.0, width=7.0, color="danger", progress=k)
|
|
438
|
+
return cv
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
442
|
+
d = 0.0
|
|
443
|
+
cues = []
|
|
444
|
+
if params["old_price"] is not None:
|
|
445
|
+
cues.append({"at": d, "name": "pop", "volume": 0.9, "why": "preço antigo"})
|
|
446
|
+
cues.append({"at": d + STRIKE_AT, "name": "tick", "why": "risco no preço antigo"})
|
|
447
|
+
cues.append({"at": _tag_start(params), "name": "cash", "why": "preço novo"})
|
|
448
|
+
return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def suggested_duration(params: dict) -> float:
|
|
452
|
+
"""The pop, the shine and about 1.6 s to read the price."""
|
|
453
|
+
return round(max(TEMPLATE["default_duration"], settle(params, 30) + 1.6), 2)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def settle(params: dict, fps: int) -> float:
|
|
457
|
+
T = _tag_start(params)
|
|
458
|
+
end = T + max(SPRING_T, SHINE_AT + SHINE_DUR)
|
|
459
|
+
if params["old_price"] is not None:
|
|
460
|
+
end = max(end, STRIKE_AT + STRIKE_DUR, T + 0.3)
|
|
461
|
+
return end + 0.05
|