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,462 @@
|
|
|
1
|
+
"""grafico_linha: a line that draws itself from left to right through the values, with a soft area under it,
|
|
2
|
+
a dot that pops on the last point, and the value at the tip counting in the header (SPEC Contract 12).
|
|
3
|
+
|
|
4
|
+
The curve is a monotone cubic (Fritsch-Carlson): smooth, and it never goes above or below the data between
|
|
5
|
+
two points. The area under the line is filled column by column (faster than a polygon with many sides).
|
|
6
|
+
`_area_mask` is the area under a curve up to a baseline. The card, grid and names are drawn once
|
|
7
|
+
(`E.static_sprite`) and pasted after the entrance.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from motion import engine as E
|
|
19
|
+
|
|
20
|
+
TEMPLATE = {
|
|
21
|
+
"id": "grafico_linha",
|
|
22
|
+
"label_pt": "Gráfico de linha",
|
|
23
|
+
"description_pt": "Uma linha que se desenha da esquerda para a direita passando pelos valores, com uma área "
|
|
24
|
+
"suave embaixo e uma bolinha que pulsa no último ponto. O número no topo acompanha a ponta "
|
|
25
|
+
"da linha até o valor final.",
|
|
26
|
+
"use_when_pt": "Para mostrar evolução no tempo: crescimento de seguidores, alunos, visitas, faturamento, "
|
|
27
|
+
"semanas de prática. Só com números que existem no material da pessoa, sem inventar.",
|
|
28
|
+
"category": "números",
|
|
29
|
+
"version": 1,
|
|
30
|
+
"params": {
|
|
31
|
+
"values": {"type": "list", "item": {"type": "number", "min": -1e13, "max": 1e13},
|
|
32
|
+
"min_items": 2, "max_items": 24, "default": [12, 15, 14, 21, 26, 24, 33, 41],
|
|
33
|
+
"label_pt": "Valores em ordem, de 2 a 24 (use ponto para decimais)"},
|
|
34
|
+
"labels": {"type": "list", "item": {"type": "text", "max_len": 10}, "max_items": 24, "default": [],
|
|
35
|
+
"label_pt": "Nomes embaixo do gráfico: vazio, 2 (primeiro e último) ou um por valor"},
|
|
36
|
+
"title": {"type": "text", "default": "", "max_len": 34, "label_pt": "Título em cima (opcional)"},
|
|
37
|
+
"prefix": {"type": "text", "default": "", "max_len": 4, "label_pt": "Antes do número, ex.: R$ ou +"},
|
|
38
|
+
"suffix": {"type": "text", "default": "", "max_len": 10, "label_pt": "Depois do número, ex.: %, mil ou alunos"},
|
|
39
|
+
"decimals": {"type": "integer", "default": None, "nullable": True, "min": 0, "max": 2,
|
|
40
|
+
"label_pt": "Casas decimais (vazio: automático)"},
|
|
41
|
+
"label": {"type": "text", "default": "", "max_len": 24, "label_pt": "Texto curto ao lado do número, ex.: em 8 semanas"},
|
|
42
|
+
"badge": {"type": "text", "default": "", "max_len": 12,
|
|
43
|
+
"label_pt": "Selo que aparece no fim, ex.: +240% (opcional)"},
|
|
44
|
+
"zero": {"type": "bool", "default": True,
|
|
45
|
+
"label_pt": "O gráfico começa no zero (desligue para mostrar variações pequenas)"},
|
|
46
|
+
"dots": {"type": "bool", "default": True, "label_pt": "Bolinhas em cada ponto (até 12 valores)"},
|
|
47
|
+
"area": {"type": "bool", "default": True, "label_pt": "Área colorida embaixo da linha"},
|
|
48
|
+
"duration": {"type": "number", "default": 1.6, "min": 0.3, "max": 8,
|
|
49
|
+
"label_pt": "Segundos que a linha leva para se desenhar"},
|
|
50
|
+
"delay": {"type": "number", "default": 0.2, "min": 0, "max": 5,
|
|
51
|
+
"label_pt": "Segundos antes de a linha começar"},
|
|
52
|
+
"card": {"type": "bool", "default": True, "label_pt": "Cartão atrás do gráfico"},
|
|
53
|
+
},
|
|
54
|
+
"default_duration": 4.0,
|
|
55
|
+
"default_region": "baixo",
|
|
56
|
+
"sfx": [{"at": 0.0, "name": "pop"}, {"at": 0.2, "name": "count", "volume": 1.3}, {"at": 1.8, "name": "pop"}],
|
|
57
|
+
"example": {"title": "Seguidores do perfil", "values": [800, 950, 1400, 1300, 2600, 3900, 5200],
|
|
58
|
+
"labels": ["Jan", "Jul"], "label": "em 6 meses", "badge": "+550%"},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
EASE = "in_out_cubic"
|
|
62
|
+
DOT_POP = 0.3
|
|
63
|
+
HALO = 0.7
|
|
64
|
+
FIT = 0.97 # text fitted to a full width keeps 3% spare: the half-size preview rounds glyphs wider
|
|
65
|
+
ROOM = 40.0 # design px of the region left free (the entrance rise and the card shadow)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------ helpers
|
|
69
|
+
|
|
70
|
+
def _decimals(p: dict) -> int:
|
|
71
|
+
if p["decimals"] is not None:
|
|
72
|
+
return int(p["decimals"])
|
|
73
|
+
d = max(E.auto_decimals(v) for v in p["values"])
|
|
74
|
+
money = E.looks_like_money(p["prefix"], p["suffix"])
|
|
75
|
+
return 2 if money and d else d
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _affix_gap(affix: str, size: float) -> float:
|
|
79
|
+
"""Space between the number and a prefix/suffix: none for one symbol (%, x, +), a word space otherwise."""
|
|
80
|
+
return size * (0.02 if len(affix) <= 1 else 0.14) if affix else 0.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _slopes(ys: list[float]) -> list[float]:
|
|
84
|
+
"""Fritsch-Carlson tangents for x spacing 1: the curve keeps the shape of the data (no overshoot)."""
|
|
85
|
+
n = len(ys)
|
|
86
|
+
d = [ys[i + 1] - ys[i] for i in range(n - 1)]
|
|
87
|
+
m = [0.0] * n
|
|
88
|
+
m[0], m[-1] = d[0], d[-1]
|
|
89
|
+
for i in range(1, n - 1):
|
|
90
|
+
m[i] = 0.0 if d[i - 1] * d[i] <= 0 else (d[i - 1] + d[i]) / 2.0
|
|
91
|
+
for i in range(n - 1):
|
|
92
|
+
if d[i] == 0:
|
|
93
|
+
m[i] = m[i + 1] = 0.0
|
|
94
|
+
continue
|
|
95
|
+
a, b = m[i] / d[i], m[i + 1] / d[i]
|
|
96
|
+
if a < 0:
|
|
97
|
+
m[i], a = 0.0, 0.0
|
|
98
|
+
if b < 0:
|
|
99
|
+
m[i + 1], b = 0.0, 0.0
|
|
100
|
+
s = a * a + b * b
|
|
101
|
+
if s > 9.0:
|
|
102
|
+
tau = 3.0 / math.sqrt(s)
|
|
103
|
+
m[i], m[i + 1] = tau * a * d[i], tau * b * d[i]
|
|
104
|
+
return m
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _hermite(ys: list[float], m: list[float], u: float) -> float:
|
|
108
|
+
"""Value of the curve at u (0 .. n-1, in points)."""
|
|
109
|
+
n = len(ys)
|
|
110
|
+
u = min(max(u, 0.0), n - 1.0)
|
|
111
|
+
i = min(int(u), n - 2)
|
|
112
|
+
t = u - i
|
|
113
|
+
t2, t3 = t * t, t * t * t
|
|
114
|
+
return ((2 * t3 - 3 * t2 + 1) * ys[i] + (t3 - 2 * t2 + t) * m[i] + (-2 * t3 + 3 * t2) * ys[i + 1]
|
|
115
|
+
+ (t3 - t2) * m[i + 1])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _area_mask(cv: E.Canvas, pts: list[tuple[float, float]], base: float):
|
|
119
|
+
"""Coverage of the region between a curve (x increasing, design px) and the horizontal line y = base."""
|
|
120
|
+
if len(pts) < 2:
|
|
121
|
+
return None
|
|
122
|
+
P = [cv.px(x, y) for x, y in pts]
|
|
123
|
+
X = np.array([p[0] for p in P], np.float64)
|
|
124
|
+
Y = np.array([p[1] for p in P], np.float64)
|
|
125
|
+
B = cv.px(0.0, base)[1]
|
|
126
|
+
x0, x1 = float(X[0]), float(X[-1])
|
|
127
|
+
if x1 - x0 < 0.05:
|
|
128
|
+
return None
|
|
129
|
+
ix0, ix1 = max(0, int(math.floor(x0))), min(cv.W, int(math.ceil(x1)))
|
|
130
|
+
iy0, iy1 = max(0, int(math.floor(Y.min()))), min(cv.H, int(math.ceil(B)))
|
|
131
|
+
if ix1 <= ix0 or iy1 <= iy0:
|
|
132
|
+
return None
|
|
133
|
+
cols = np.arange(ix0, ix1, dtype=np.float64)
|
|
134
|
+
ycurve = np.interp(cols + 0.5, X, Y)
|
|
135
|
+
rows = np.arange(iy0, iy1, dtype=np.float64)
|
|
136
|
+
a = np.clip(rows[:, None] + 0.5 - ycurve[None, :] + 0.5, 0.0, 1.0) # under the curve
|
|
137
|
+
a *= np.clip(B - rows, 0.0, 1.0)[:, None] # above the base line
|
|
138
|
+
a *= (np.clip(cols + 1.0 - x0, 0.0, 1.0) * np.clip(x1 - cols, 0.0, 1.0))[None, :]
|
|
139
|
+
return E.Mask(a.astype(np.float32), ix0, iy0)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _time_at(progress: float, delay: float, duration: float) -> float:
|
|
143
|
+
"""Seconds when the eased line progress reaches `progress` (bisection, deterministic)."""
|
|
144
|
+
lo, hi = 0.0, 1.0
|
|
145
|
+
for _ in range(40):
|
|
146
|
+
mid = (lo + hi) / 2.0
|
|
147
|
+
if E.ease(EASE, mid) < progress:
|
|
148
|
+
lo = mid
|
|
149
|
+
else:
|
|
150
|
+
hi = mid
|
|
151
|
+
return delay + duration * hi
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------------ layout (once per item)
|
|
155
|
+
|
|
156
|
+
@lru_cache(maxsize=64)
|
|
157
|
+
def _layout(params_json: str, box: E.Box) -> dict:
|
|
158
|
+
p = json.loads(params_json)
|
|
159
|
+
vals = [float(v) for v in p["values"]]
|
|
160
|
+
n = len(vals)
|
|
161
|
+
labels = [s.strip() for s in p["labels"]]
|
|
162
|
+
if labels and len(labels) not in (2, n):
|
|
163
|
+
raise E.MotionError(f"grafico_linha.params.labels tem {len(labels)} nomes e values tem {n} números: "
|
|
164
|
+
f"use 2 nomes (primeiro e último), um nome por número, ou deixe vazio")
|
|
165
|
+
dec = _decimals(p)
|
|
166
|
+
prefix, suffix = p["prefix"].strip(), p["suffix"].strip()
|
|
167
|
+
card = p["card"]
|
|
168
|
+
theme = box.theme
|
|
169
|
+
pad = 46.0 if card else 12.0
|
|
170
|
+
card_w = box.w
|
|
171
|
+
inner = card_w - 2 * pad
|
|
172
|
+
x_card = (box.w - card_w) / 2.0
|
|
173
|
+
|
|
174
|
+
# header: title / big value + label ... badge
|
|
175
|
+
title = p["title"].strip()
|
|
176
|
+
t_size = E.fit_text(title, "text_bold", inner * FIT, 38.0, 28.0) if title else 0.0
|
|
177
|
+
t_cap = E.measure("H", "text_bold", t_size).cap if title else 0.0
|
|
178
|
+
t_gap = 26.0 if title else 0.0
|
|
179
|
+
widest = max((E.format_number(v, dec) for v in (min(vals), max(vals), vals[0], vals[-1])),
|
|
180
|
+
key=lambda s: E.measure(s, "sans_black", 100, tabular=True).width)
|
|
181
|
+
pre, suf = prefix, suffix
|
|
182
|
+
badge = p["badge"].strip()
|
|
183
|
+
b_size = 36.0
|
|
184
|
+
b_w = (E.measure(badge, "sans_black", b_size).width + 2 * 22.0) if badge else 0.0
|
|
185
|
+
b_h = 64.0
|
|
186
|
+
label = p["label"].strip()
|
|
187
|
+
|
|
188
|
+
def value_w(size: float) -> float:
|
|
189
|
+
aff = size * 0.52
|
|
190
|
+
return (E.measure(pre, "sans_black", aff).width + _affix_gap(pre, size)
|
|
191
|
+
+ E.measure(widest, "sans_black", size, tabular=True).width
|
|
192
|
+
+ _affix_gap(suf, size) + E.measure(suf, "sans_black", aff).width)
|
|
193
|
+
|
|
194
|
+
v_size = 92.0
|
|
195
|
+
room = inner - (b_w + 24.0 if badge else 0.0)
|
|
196
|
+
if value_w(v_size) > room * 0.72:
|
|
197
|
+
v_size = max(48.0, v_size * room * 0.72 / value_w(v_size))
|
|
198
|
+
v_cap = E.measure("0", "sans_black", v_size).cap
|
|
199
|
+
l_size = 36.0
|
|
200
|
+
label_below = False
|
|
201
|
+
if label:
|
|
202
|
+
left = room - value_w(v_size) - 22.0
|
|
203
|
+
l_size = E.fit_text(label, "text_semibold", max(10.0, left * FIT), 36.0, 10.0)
|
|
204
|
+
if l_size < 28.0:
|
|
205
|
+
label_below = True
|
|
206
|
+
l_size = E.fit_text(label, "text_semibold", room * FIT, 34.0, 26.0)
|
|
207
|
+
l_cap = E.measure("H", "text_semibold", l_size).cap if label else 0.0
|
|
208
|
+
head_h = t_cap + t_gap + v_cap + ((18.0 + l_cap) if label_below else 0.0)
|
|
209
|
+
|
|
210
|
+
# x labels
|
|
211
|
+
xl_size = 32.0
|
|
212
|
+
xl_cap = E.measure("H", "text_semibold", xl_size).cap if labels else 0.0
|
|
213
|
+
xl_block = (20.0 + xl_cap) if labels else 0.0
|
|
214
|
+
|
|
215
|
+
plot_gap = 40.0
|
|
216
|
+
fixed = 2 * pad + head_h + plot_gap + xl_block + 16.0
|
|
217
|
+
plot_h = max(110.0, min(360.0, box.h - ROOM - fixed))
|
|
218
|
+
card_h = fixed + plot_h
|
|
219
|
+
top = (box.h - card_h) / 2.0
|
|
220
|
+
plot_top = top + pad + head_h + plot_gap + 16.0 # 16: room for the end dot above the highest point
|
|
221
|
+
base = plot_top + plot_h
|
|
222
|
+
x0 = x_card + pad + 14.0
|
|
223
|
+
x1 = x_card + card_w - pad - 18.0
|
|
224
|
+
|
|
225
|
+
lo, hi = min(vals), max(vals)
|
|
226
|
+
if p["zero"]:
|
|
227
|
+
lo = min(0.0, lo)
|
|
228
|
+
hi = max(0.0, hi)
|
|
229
|
+
else:
|
|
230
|
+
span = hi - lo
|
|
231
|
+
lo -= span * 0.12 if span > 0 else 1.0
|
|
232
|
+
if hi - lo <= 1e-12:
|
|
233
|
+
lo, hi = lo - 1.0, hi + 1.0
|
|
234
|
+
|
|
235
|
+
def ymap(v: float) -> float:
|
|
236
|
+
return base - (v - lo) / (hi - lo) * plot_h
|
|
237
|
+
|
|
238
|
+
slopes = _slopes(vals)
|
|
239
|
+
xs = [x0 + (x1 - x0) * i / (n - 1) for i in range(n)]
|
|
240
|
+
per = max(3, min(48, int((x1 - x0) / (n - 1) / 9.0))) # a curve sample about every 9 px
|
|
241
|
+
dense = []
|
|
242
|
+
for k in range((n - 1) * per + 1):
|
|
243
|
+
u = k / per
|
|
244
|
+
dense.append((x0 + (x1 - x0) * u / (n - 1), ymap(_hermite(vals, slopes, u))))
|
|
245
|
+
dots = p["dots"] and n <= 12
|
|
246
|
+
dot_times = [_time_at(i / (n - 1), p["delay"], p["duration"]) for i in range(n)]
|
|
247
|
+
|
|
248
|
+
# x labels: 2 = first and last; n = every point (thinned when they would touch)
|
|
249
|
+
xlabels = []
|
|
250
|
+
if labels:
|
|
251
|
+
idx = [0, n - 1] if len(labels) == 2 else list(range(n))
|
|
252
|
+
names = labels if len(labels) == n else [labels[0]] + [""] * (n - 2) + [labels[1]]
|
|
253
|
+
if len(idx) > 2:
|
|
254
|
+
widest_l = max(E.measure(s, "text_semibold", xl_size).width for s in names) + 18.0
|
|
255
|
+
step = max(1, math.ceil(widest_l / ((x1 - x0) / (n - 1))))
|
|
256
|
+
idx = [i for i in idx if i % step == 0 or i == n - 1]
|
|
257
|
+
if len(idx) >= 2 and idx[-1] - idx[-2] < step:
|
|
258
|
+
idx.pop(-2)
|
|
259
|
+
for i in idx:
|
|
260
|
+
if not names[i]:
|
|
261
|
+
continue
|
|
262
|
+
anchor = "ls" if i == 0 else "rs" if i == n - 1 else "ms"
|
|
263
|
+
x = x0 - 14.0 if i == 0 else x1 + 18.0 if i == n - 1 else xs[i]
|
|
264
|
+
xlabels.append((x, names[i], anchor))
|
|
265
|
+
|
|
266
|
+
accent_ok = not card or E.contrast(theme.accent, theme.card) >= 2.4
|
|
267
|
+
head_base = top + pad + t_cap + t_gap + v_cap
|
|
268
|
+
return {
|
|
269
|
+
"n": n, "vals": vals, "slopes": slopes, "dec": dec, "pre": pre, "suf": suf, "card": card,
|
|
270
|
+
"card_box": (x_card, top, card_w, card_h), "radius": min(44.0, card_h * 0.12),
|
|
271
|
+
"title": title, "t_size": t_size, "title_base": top + pad + t_cap, "x_left": x_card + pad,
|
|
272
|
+
"v_size": v_size, "value_w": value_w(v_size), "head_base": head_base, "value_color": "accent" if accent_ok else "text",
|
|
273
|
+
"label": label, "l_size": l_size, "label_below": label_below,
|
|
274
|
+
"label_base": head_base + 18.0 + l_cap if label_below else head_base,
|
|
275
|
+
"badge": badge, "b_size": b_size, "b_w": b_w, "b_h": b_h,
|
|
276
|
+
"badge_box": (x_card + card_w - pad - b_w, head_base - v_cap / 2.0 - b_h / 2.0, b_w, b_h),
|
|
277
|
+
"plot_top": plot_top, "base": base, "plot_h": plot_h, "lo": lo, "hi": hi, "x0": x0, "x1": x1, "xs": xs,
|
|
278
|
+
"ys": [ymap(v) for v in vals], "dense": dense, "dots": dots, "dot_times": dot_times,
|
|
279
|
+
"xlabels": xlabels, "xl_size": xl_size, "xl_base": base + 20.0 + xl_cap,
|
|
280
|
+
"grid": [plot_top + plot_h * k / 3.0 for k in range(4)], "zero_line": ymap(0.0) if lo < 0 < hi else None,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------ drawing
|
|
285
|
+
|
|
286
|
+
def _t_end(params: dict) -> float:
|
|
287
|
+
return params["delay"] + params["duration"]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
STATIC_AFTER = 0.5 # seconds: from here on the card, grid, names, title and label never change
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _draw_static(cv: E.Canvas, L: dict, ui_in: float) -> None:
|
|
294
|
+
"""Everything that does not move once the card is in."""
|
|
295
|
+
card = L["card"]
|
|
296
|
+
stroke = {} if card else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
|
|
297
|
+
soft = {} if card else {"stroke": 4.0, "stroke_color": "black"}
|
|
298
|
+
x0, x1 = L["x0"], L["x1"]
|
|
299
|
+
if card:
|
|
300
|
+
cx0, cy0, cw, ch = L["card_box"]
|
|
301
|
+
cv.card(cx0, cy0, cw, ch, radius=L["radius"])
|
|
302
|
+
grid_c = (255, 255, 255, 26) if (cv.theme.dark or not card) else (0, 0, 0, 20)
|
|
303
|
+
strong_c = "track" if card else (255, 255, 255, 110)
|
|
304
|
+
for k, gy in enumerate(L["grid"]):
|
|
305
|
+
strong = k == len(L["grid"]) - 1
|
|
306
|
+
cv.line(x0 - 14.0, gy, x1 + 18.0, gy, width=2.5 if strong else 2.0, color=strong_c if strong else grid_c,
|
|
307
|
+
opacity=ui_in)
|
|
308
|
+
if L["zero_line"] is not None:
|
|
309
|
+
cv.line(x0 - 14.0, L["zero_line"], x1 + 18.0, L["zero_line"], width=2.5, color=strong_c, opacity=ui_in)
|
|
310
|
+
for x, name, anchor in L["xlabels"]:
|
|
311
|
+
cv.text(x, L["xl_base"], name, "text_semibold", L["xl_size"], color="muted" if card else "text",
|
|
312
|
+
opacity=ui_in, anchor=anchor, **soft)
|
|
313
|
+
if L["title"]:
|
|
314
|
+
cv.text(L["x_left"], L["title_base"], L["title"], "text_bold", L["t_size"], color="text", **stroke)
|
|
315
|
+
if L["label"]:
|
|
316
|
+
lx = L["x_left"] if L["label_below"] else L["x_left"] + L["value_w"] + 22.0
|
|
317
|
+
cv.text(lx, L["label_base"], L["label"], "text_semibold", L["l_size"],
|
|
318
|
+
color="muted" if card else "text", opacity=ui_in, **soft)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _static_sprite(params_json: str, box: E.Box):
|
|
322
|
+
"""The static part drawn once (card, grid, names, title and label)."""
|
|
323
|
+
return E.static_sprite(("grafico_linha", params_json), box,
|
|
324
|
+
lambda c: _draw_static(c, _layout(params_json, box), 1.0))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
|
|
328
|
+
cv = E.Canvas(box)
|
|
329
|
+
t = frame_index / fps
|
|
330
|
+
key = json.dumps(params, sort_keys=True)
|
|
331
|
+
L = _layout(key, box)
|
|
332
|
+
cx0, cy0, cw, ch = L["card_box"]
|
|
333
|
+
theme = box.theme
|
|
334
|
+
t_end = _t_end(params)
|
|
335
|
+
|
|
336
|
+
t_in = t + 1.0 / fps
|
|
337
|
+
alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
|
|
338
|
+
scale = 0.92 + 0.08 * E.out_back(E.progress(t_in, 0.0, 0.5), 1.4)
|
|
339
|
+
rise = 30.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
|
|
340
|
+
card = L["card"]
|
|
341
|
+
stroke = {} if card else {"stroke": 5.0, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
|
|
342
|
+
ui_in = E.eased(t, 0.05, 0.3)
|
|
343
|
+
p = E.eased(t, params["delay"], params["duration"], EASE)
|
|
344
|
+
x0, x1 = L["x0"], L["x1"]
|
|
345
|
+
tip_x = x0 + (x1 - x0) * p
|
|
346
|
+
u_tip = p * (L["n"] - 1)
|
|
347
|
+
if p >= 1.0:
|
|
348
|
+
tip_v = L["vals"][-1]
|
|
349
|
+
elif p <= 0.0:
|
|
350
|
+
tip_v = L["vals"][0]
|
|
351
|
+
else:
|
|
352
|
+
tip_v = _hermite(L["vals"], L["slopes"], u_tip)
|
|
353
|
+
base = L["base"]
|
|
354
|
+
|
|
355
|
+
with cv.group(opacity=alpha, scale=scale, dy=rise, pivot=(cx0 + cw / 2.0, cy0 + ch / 2.0)):
|
|
356
|
+
if t_in >= STATIC_AFTER: # the entrance is over: the group is the identity
|
|
357
|
+
cv.paste_sprite(_static_sprite(key, box))
|
|
358
|
+
else:
|
|
359
|
+
_draw_static(cv, L, ui_in)
|
|
360
|
+
|
|
361
|
+
# header (the title and the label are in the static part)
|
|
362
|
+
size = L["v_size"]
|
|
363
|
+
aff = size * 0.52
|
|
364
|
+
num = E.format_number(tip_v, L["dec"])
|
|
365
|
+
x = L["x_left"]
|
|
366
|
+
hb = L["head_base"]
|
|
367
|
+
if L["pre"]:
|
|
368
|
+
x = cv.text(x, hb, L["pre"], "sans_black", aff, color="text", opacity=0.92, **stroke)[2]
|
|
369
|
+
x += _affix_gap(L["pre"], size)
|
|
370
|
+
x = cv.text(x, hb, num, "sans_black", size, color=L["value_color"], tabular=True, **stroke)[2]
|
|
371
|
+
if L["suf"]:
|
|
372
|
+
x += _affix_gap(L["suf"], size)
|
|
373
|
+
cv.text(x, hb, L["suf"], "sans_black", aff, color="text", opacity=0.92, **stroke)
|
|
374
|
+
|
|
375
|
+
# area and line
|
|
376
|
+
if p > 0:
|
|
377
|
+
if p >= 1.0:
|
|
378
|
+
pts = list(L["dense"])
|
|
379
|
+
tip = pts[-1]
|
|
380
|
+
else:
|
|
381
|
+
pts = [q for q in L["dense"] if q[0] < tip_x]
|
|
382
|
+
tip = (tip_x, _y_of(L, tip_v))
|
|
383
|
+
pts.append(tip)
|
|
384
|
+
if len(pts) == 1:
|
|
385
|
+
pts = [pts[0], pts[0]]
|
|
386
|
+
if params["area"]:
|
|
387
|
+
acc = theme.accent
|
|
388
|
+
grad = E.Gradient((acc[0], acc[1], acc[2], 105 if card else 90), (acc[0], acc[1], acc[2], 0),
|
|
389
|
+
start=(0.0, L["plot_top"]), end=(0.0, base))
|
|
390
|
+
cv.fill_mask(_area_mask(cv, pts, base), grad)
|
|
391
|
+
# on the card the area already separates the line; on the video a shadow does
|
|
392
|
+
cv.polyline(pts, width=9.0, color="accent",
|
|
393
|
+
shadow=None if card else E.Shadow(blur=10.0, dy=5.0, opacity=0.55))
|
|
394
|
+
|
|
395
|
+
# point markers
|
|
396
|
+
if L["dots"]:
|
|
397
|
+
for i in range(L["n"] - 1):
|
|
398
|
+
q = E.progress(t, L["dot_times"][i], DOT_POP)
|
|
399
|
+
if q <= 0:
|
|
400
|
+
continue
|
|
401
|
+
s = E.out_back(q, 2.2)
|
|
402
|
+
cv.circle(L["xs"][i], L["ys"][i], 11.0 * s, fill="accent")
|
|
403
|
+
cv.circle(L["xs"][i], L["ys"][i], 5.5 * s, fill="card" if card else (20, 20, 22, 255))
|
|
404
|
+
|
|
405
|
+
if p < 1.0:
|
|
406
|
+
cv.circle(tip[0], tip[1], 11.0, fill="accent")
|
|
407
|
+
cv.circle(tip[0], tip[1], 5.0, fill="white")
|
|
408
|
+
|
|
409
|
+
# end: dot, halo, badge
|
|
410
|
+
q_end = E.progress(t, t_end - 0.02, 0.35)
|
|
411
|
+
if q_end > 0:
|
|
412
|
+
ex, ey = L["xs"][-1], L["ys"][-1]
|
|
413
|
+
h = E.progress(t, t_end, HALO)
|
|
414
|
+
if 0 < h < 1:
|
|
415
|
+
cv.ring(ex, ey, 16.0 + 34.0 * E.out_cubic(h), 5.0 * (1.0 - h) + 1.0, color="accent",
|
|
416
|
+
opacity=0.8 * (1.0 - h))
|
|
417
|
+
s = E.out_back(q_end, 2.4)
|
|
418
|
+
|
|
419
|
+
def end_dot() -> None:
|
|
420
|
+
cv.circle(ex, ey, 17.0 * s, fill="accent")
|
|
421
|
+
cv.circle(ex, ey, 7.0 * s, fill="white")
|
|
422
|
+
|
|
423
|
+
glow = E.pulse(t, t_end - 0.02, 0.5)
|
|
424
|
+
if glow > 0.02:
|
|
425
|
+
cv.glow(end_dot, radius=18.0, opacity=0.7 * glow)
|
|
426
|
+
else:
|
|
427
|
+
end_dot()
|
|
428
|
+
|
|
429
|
+
if L["badge"]:
|
|
430
|
+
qb = E.progress(t, t_end + 0.1, 0.35)
|
|
431
|
+
if qb > 0:
|
|
432
|
+
bx, by, bw, bh = L["badge_box"]
|
|
433
|
+
with cv.group(scale=0.4 + 0.6 * E.out_back(qb, 2.0), opacity=E.clamp01(qb * 3.0),
|
|
434
|
+
pivot=(bx + bw / 2.0, by + bh / 2.0)):
|
|
435
|
+
cv.pill(bx, by, bw, bh, fill="accent", shadow=E.Shadow(blur=14.0, dy=6.0, opacity=0.35))
|
|
436
|
+
cv.text(bx + bw / 2.0, by + bh / 2.0, L["badge"], "sans_black", L["b_size"],
|
|
437
|
+
color="on_accent", anchor="mc")
|
|
438
|
+
return cv
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _y_of(L: dict, v: float) -> float:
|
|
442
|
+
"""Design y of a value on the plot scale."""
|
|
443
|
+
return L["base"] - (v - L["lo"]) / (L["hi"] - L["lo"]) * L["plot_h"]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def sfx(params: dict, fps: int, frames: int) -> list[dict]:
|
|
447
|
+
start, dur = params["delay"], params["duration"]
|
|
448
|
+
cues = [{"at": 0.0, "name": "pop", "why": "entrada"}]
|
|
449
|
+
if params["values"][0] != params["values"][-1]:
|
|
450
|
+
for k in range(max(1, int(dur / 0.6 + 0.25))):
|
|
451
|
+
cues.append({"at": start + k * 0.6, "name": "count", "volume": 1.3, "why": "número subindo"})
|
|
452
|
+
cues.append({"at": start + dur, "name": "pop", "volume": 0.9, "why": "ponto final"})
|
|
453
|
+
return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def suggested_duration(params: dict) -> float:
|
|
457
|
+
"""The line, the dot and badge, and about 1.8 s to read."""
|
|
458
|
+
return round(max(TEMPLATE["default_duration"], _t_end(params) + HALO + 1.8), 2)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def settle(params: dict, fps: int) -> float:
|
|
462
|
+
return _t_end(params) + HALO + 0.05
|