primocode 9.10.0 → 10.0.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/README.md +30 -6
- package/bin/primocode.js +100 -341
- package/lib/act.js +245 -23
- package/lib/browser.js +17 -0
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +4 -2
- package/lib/estudio/diretor.js +9 -1
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +41 -18
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/index.jsx +11 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +72 -0
- package/lib/estudio/lancamento.js +413 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/remotion.js +87 -43
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/vox.js +385 -0
- package/lib/fala.js +1 -94
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +8 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +79 -20
- package/lib/ui.js +13 -6
- package/lib/video/analise.js +89 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +247 -0
- package/lib/video/ops.js +322 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/render.js +221 -0
- package/lib/video/texto.js +142 -0
- package/lib/video/timeline.js +310 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +3 -4
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/* kit/vox.jsx — os oito dispositivos tipográficos do estilo VOX.
|
|
2
|
+
*
|
|
3
|
+
* "A tipografia da VOX não é legenda: é cenografia." Cada peça é um
|
|
4
|
+
* dispositivo, e cada um tem um comportamento próprio que significa algo:
|
|
5
|
+
*
|
|
6
|
+
* palavra palavra ancorada: carimba no beat, a tese ganha o acento e o sublinhado
|
|
7
|
+
* lista termo — definição; o item novo entra e o anterior apaga para cinza
|
|
8
|
+
* letra highlight na letra: dentro da manchete, uma palavra muda de cor
|
|
9
|
+
* especime o termo em tracking largo, à máquina, com o sub-rótulo etimológico
|
|
10
|
+
* pilula nome e subtítulo pousando na arte, ligados por uma linha que se desenha
|
|
11
|
+
* fonte a fonte do documento em mono, com a barra de acento
|
|
12
|
+
* marcatexto o documento lido em cascata; o marca-texto varre, a caneta circula
|
|
13
|
+
* textura a mesma linha repetida até o documento virar fundo
|
|
14
|
+
* barras o gráfico em tinta: barras que pousam, a maior circulada
|
|
15
|
+
*
|
|
16
|
+
* As coordenadas (x, y, largura) são da cena, em px. O tempo (t, de cada
|
|
17
|
+
* item) é o do elemento, em segundos — o instante em que a palavra é DITA. */
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import {
|
|
20
|
+
carimbo, mola, entre, lerp, acaso, Traco, elipseMao, sublinhadoMao, familia, MONO,
|
|
21
|
+
CURVAS_KIT, mistura, alfa, SOMBRA_CONTATO,
|
|
22
|
+
} from './base';
|
|
23
|
+
|
|
24
|
+
const TINTA = (el) => (el.tinta === false ? undefined : 'url(#pc-tinta)');
|
|
25
|
+
|
|
26
|
+
/* O traço de caneta que cabe em qualquer caixa: SVG esticado (preserveAspectRatio
|
|
27
|
+
none) com traço que não estica (non-scaling-stroke). É assim que o
|
|
28
|
+
sublinhado e o círculo abraçam uma palavra cuja largura só o navegador sabe. */
|
|
29
|
+
function Caneta({ d, p, cor, espessura = 5, inset = '0', opacidade = 1 }) {
|
|
30
|
+
if (p <= 0) return null;
|
|
31
|
+
// O SVG é elemento substituído: "inset" sozinho não o estica. Quem estica é a caixa em volta.
|
|
32
|
+
return (
|
|
33
|
+
<div style={{ position: 'absolute', inset, pointerEvents: 'none' }}>
|
|
34
|
+
<svg viewBox="0 0 100 100" preserveAspectRatio="none" width="100%" height="100%" style={{ overflow: 'visible', display: 'block' }}>
|
|
35
|
+
<path d={d} pathLength={1} fill="none" stroke={cor} strokeWidth={espessura} strokeLinecap="round" strokeLinejoin="round"
|
|
36
|
+
vectorEffect="non-scaling-stroke" strokeDasharray="1 1" strokeDashoffset={1 - Math.min(1, p)} opacity={opacidade} />
|
|
37
|
+
</svg>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const SUBLINHA = sublinhadoMao(0, 50, 100, 2);
|
|
42
|
+
const CIRCULO = elipseMao(50, 50, 50, 50, 4);
|
|
43
|
+
// O traço da mão: 10 a 16 quadros, rápido no começo e arrastando no fim.
|
|
44
|
+
const traco = (t, de, fps, quadros = 13) => CURVAS_KIT.arrasto(entre(t, de, de + quadros / fps));
|
|
45
|
+
|
|
46
|
+
// ── palavra ancorada ─────────────────────────────────────────────────────
|
|
47
|
+
export function Palavra({ el, t, fps }) {
|
|
48
|
+
const ws = el.palavras || [];
|
|
49
|
+
const tam = el.tamanho || 120;
|
|
50
|
+
const centro = el.alinhamento === 'center';
|
|
51
|
+
return (
|
|
52
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, width: el.largura, transform: centro ? 'translateX(-50%)' : undefined,
|
|
53
|
+
display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', justifyContent: centro ? 'center' : 'flex-start',
|
|
54
|
+
columnGap: '0.24em', rowGap: '0.02em', fontFamily: familia(el.fonte), fontSize: tam, lineHeight: el.entrelinha || 1.02,
|
|
55
|
+
color: el.cor || '#141414', letterSpacing: '-0.02em' }}>
|
|
56
|
+
{ws.map((w, i) => {
|
|
57
|
+
const cor = w.acento ? (el.corAcento || '#d8342b') : (w.cor || el.cor || '#141414');
|
|
58
|
+
const pSub = w.acento && el.sublinhar !== false ? traco(t, w.t + 5 / fps, fps, 14) : 0;
|
|
59
|
+
return (
|
|
60
|
+
<React.Fragment key={i}>
|
|
61
|
+
{w.quebra ? <div style={{ flexBasis: '100%', height: 0 }} /> : null}
|
|
62
|
+
<span style={{ display: 'inline-block', position: 'relative', fontWeight: w.peso || (w.acento ? 800 : 600),
|
|
63
|
+
fontSize: w.escala ? w.escala + 'em' : undefined, color: cor, transformOrigin: '50% 80%', filter: TINTA(el),
|
|
64
|
+
...carimbo(t, w.t, fps, i + 3, w.acento ? 1 : 0.7) }}>
|
|
65
|
+
{w.texto}
|
|
66
|
+
<Caneta d={SUBLINHA} p={pSub} cor={el.corAcento || '#d8342b'} espessura={Math.max(4, tam * 0.06)} inset="0.84em -0.04em -0.08em -0.04em" />
|
|
67
|
+
</span>
|
|
68
|
+
</React.Fragment>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── lista com travessão ──────────────────────────────────────────────────
|
|
76
|
+
export function Lista({ el, t, fps }) {
|
|
77
|
+
const itens = el.itens || [];
|
|
78
|
+
const tam = el.tamanho || 72;
|
|
79
|
+
const velha = el.corVelha || '#b9b4aa';
|
|
80
|
+
// O "agora" é o último item que já entrou: a hierarquia é do agora, não do primeiro.
|
|
81
|
+
let agora = -1;
|
|
82
|
+
itens.forEach((it, i) => { if (t >= it.t) agora = i; });
|
|
83
|
+
return (
|
|
84
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, width: el.largura, fontFamily: familia(el.fonte), fontSize: tam,
|
|
85
|
+
lineHeight: 1.08, color: el.cor || '#141414' }}>
|
|
86
|
+
{itens.map((it, i) => {
|
|
87
|
+
// O anterior perde cor em poucos quadros: não sai de cena, é rebaixado.
|
|
88
|
+
const idade = i < agora ? entre(t, itens[i + 1].t, itens[i + 1].t + 6 / fps) : 0;
|
|
89
|
+
const cor = mistura(el.cor || '#141414', velha, idade);
|
|
90
|
+
const corTermo = i === agora && !it.riscar ? (el.corAcento || '#d8342b') : cor;
|
|
91
|
+
const pRisco = it.riscar ? traco(t, it.t + 0.35, fps, 12) : 0;
|
|
92
|
+
return (
|
|
93
|
+
<div key={i} style={{ position: 'relative', display: 'block', marginBottom: '0.34em', transformOrigin: '0% 70%', ...carimbo(t, it.t, fps, i + 11, 0.6) }}>
|
|
94
|
+
<span style={{ position: 'relative', display: 'inline-block', filter: TINTA(el) }}>
|
|
95
|
+
<span style={{ fontWeight: 800, color: corTermo, letterSpacing: '-0.02em' }}>{it.termo}</span>
|
|
96
|
+
{it.def ? <span style={{ fontWeight: 300, color: cor }}>{' — ' + it.def}</span> : null}
|
|
97
|
+
<Caneta d="M 0 55 C 30 50 70 58 100 48" p={pRisco} cor={el.corAcento || '#d8342b'} espessura={Math.max(4, tam * 0.07)} inset="0 -0.1em" />
|
|
98
|
+
</span>
|
|
99
|
+
{it.nota ? <div style={{ fontFamily: MONO(el.fonteMono), fontSize: tam * 0.26, letterSpacing: '0.08em', color: velha, marginTop: '0.3em',
|
|
100
|
+
opacity: t >= it.t + 0.2 ? 1 : 0 }}>{it.nota}</div> : null}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── highlight na letra ───────────────────────────────────────────────────
|
|
109
|
+
export function Letra({ el, t, fps }) {
|
|
110
|
+
const ws = String(el.texto || '').split(/\s+/).filter(Boolean);
|
|
111
|
+
const alvo = String(el.destaque || '').toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
|
112
|
+
const passo = (el.passo || 4) / fps;
|
|
113
|
+
const t0 = el.t || 0;
|
|
114
|
+
const tD = el.tDestaque != null ? el.tDestaque : t0 + ws.length * passo + 0.25;
|
|
115
|
+
const tam = el.tamanho || 96;
|
|
116
|
+
const centro = el.alinhamento === 'center';
|
|
117
|
+
const direita = el.alinhamento === 'right';
|
|
118
|
+
// x é a borda de onde o texto nasce: a esquerda, o centro ou a direita.
|
|
119
|
+
return (
|
|
120
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, width: el.largura, transform: centro ? 'translateX(-50%)' : (direita ? 'translateX(-100%)' : undefined),
|
|
121
|
+
textAlign: centro ? 'center' : (direita ? 'right' : 'left'), fontFamily: familia(el.fonte), fontSize: tam, lineHeight: 1.04, letterSpacing: '-0.025em',
|
|
122
|
+
color: el.cor || '#141414' }}>
|
|
123
|
+
{ws.map((w, i) => {
|
|
124
|
+
const chave = w.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
|
125
|
+
const ehAlvo = alvo && chave === alvo;
|
|
126
|
+
const aceso = ehAlvo && t >= tD;
|
|
127
|
+
const leve = w.length <= 3 && !ehAlvo;
|
|
128
|
+
return (
|
|
129
|
+
<React.Fragment key={i}>
|
|
130
|
+
<span style={{ display: 'inline-block', position: 'relative', fontWeight: ehAlvo ? 800 : (leve ? 300 : 600),
|
|
131
|
+
color: aceso ? (el.corAcento || '#d8342b') : undefined, textTransform: ehAlvo && el.caixaAlta ? 'uppercase' : undefined,
|
|
132
|
+
transformOrigin: '50% 80%', filter: TINTA(el), ...carimbo(t, t0 + i * passo, fps, i + 21, 0.45) }}>
|
|
133
|
+
{w}
|
|
134
|
+
<Caneta d={SUBLINHA} p={ehAlvo ? traco(t, tD + 2 / fps, fps, 14) : 0} cor={el.corAcento || '#d8342b'} espessura={Math.max(4, tam * 0.07)}
|
|
135
|
+
inset="0.84em -0.02em -0.08em -0.02em" />
|
|
136
|
+
</span>
|
|
137
|
+
{i < ws.length - 1 ? ' ' : null}
|
|
138
|
+
</React.Fragment>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── cartão-espécime ──────────────────────────────────────────────────────
|
|
146
|
+
export function Especime({ el, t, fps }) {
|
|
147
|
+
const termo = String(el.termo || '').toUpperCase();
|
|
148
|
+
const t0 = el.t || 0;
|
|
149
|
+
const porLetra = 2 / fps; // a máquina de escrever: uma letra a cada 2 quadros
|
|
150
|
+
const n = Math.max(0, Math.min(termo.length, Math.floor((t - t0) / porLetra) + 1));
|
|
151
|
+
const fimTermo = t0 + termo.length * porLetra;
|
|
152
|
+
const sub = String(el.sub || '');
|
|
153
|
+
const nSub = Math.max(0, Math.min(sub.length, Math.floor((t - fimTermo - 0.25) * fps * 1.5)));
|
|
154
|
+
const cursor = t < fimTermo + 0.6 && Math.floor(t * fps / 8) % 2 === 0;
|
|
155
|
+
const tam = el.tamanho || 150;
|
|
156
|
+
return (
|
|
157
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, transform: 'translate(-50%, -50%)', textAlign: 'center', whiteSpace: 'nowrap' }}>
|
|
158
|
+
<div style={{ fontFamily: familia(el.fonte), fontWeight: 700, fontSize: tam, letterSpacing: '0.42em', marginRight: '-0.42em',
|
|
159
|
+
color: el.cor || '#f2efe8', lineHeight: 1, filter: TINTA(el) }}>
|
|
160
|
+
<span>{termo.slice(0, n)}</span>
|
|
161
|
+
<span style={{ opacity: 0 }}>{termo.slice(n)}</span>
|
|
162
|
+
<span style={{ position: 'absolute', marginLeft: '-0.36em', opacity: cursor ? 1 : 0, fontWeight: 300 }}>|</span>
|
|
163
|
+
</div>
|
|
164
|
+
{sub ? (
|
|
165
|
+
<div style={{ fontFamily: MONO(el.fonteMono), fontSize: Math.max(18, tam * 0.16), letterSpacing: '0.1em', marginTop: tam * 0.42,
|
|
166
|
+
color: el.corSub || alfa(el.cor || '#f2efe8', 0.6) }}>
|
|
167
|
+
{sub.slice(0, nSub)}<span style={{ opacity: 0 }}>{sub.slice(nSub)}</span>
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── pill branca + linha-guia ─────────────────────────────────────────────
|
|
175
|
+
export function Pilula({ el, t, fps }) {
|
|
176
|
+
const t0 = el.t || 0;
|
|
177
|
+
const tam = el.tamanho || 44;
|
|
178
|
+
const alvo = el.alvo || { x: el.x, y: el.y - tam * 4 };
|
|
179
|
+
const pLinha = traco(t, t0, fps, 12);
|
|
180
|
+
const tPill = t0 + 10 / fps;
|
|
181
|
+
// A linha vai do detalhe até a borda de cima da pill: ela pousa no fim da linha.
|
|
182
|
+
const topo = el.y - tam * 0.95;
|
|
183
|
+
return (
|
|
184
|
+
<>
|
|
185
|
+
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', overflow: 'visible', pointerEvents: 'none' }}>
|
|
186
|
+
<Traco d={'M ' + alvo.x + ' ' + alvo.y + ' L ' + el.x + ' ' + topo} p={pLinha} cor={el.corLinha || '#1a1a1a'} espessura={2} />
|
|
187
|
+
{t >= t0 ? <circle cx={alvo.x} cy={alvo.y} r={6 * (0.4 + 0.6 * mola(t, t0, fps))} fill={el.corLinha || '#1a1a1a'} /> : null}
|
|
188
|
+
</svg>
|
|
189
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, transform: 'translate(-50%, -50%)', textAlign: 'center' }}>
|
|
190
|
+
<div style={{ display: 'inline-block', background: el.cor || '#ffffff', color: el.corTexto || '#111', borderRadius: tam * 0.32,
|
|
191
|
+
padding: '0.34em 0.8em', fontFamily: familia(el.fonte), fontWeight: 800, fontSize: tam, letterSpacing: '0.04em',
|
|
192
|
+
textTransform: 'uppercase', boxShadow: SOMBRA_CONTATO, whiteSpace: 'nowrap', transformOrigin: '50% 100%', ...carimbo(t, tPill, fps, 31, 0.6) }}>
|
|
193
|
+
{el.texto}
|
|
194
|
+
</div>
|
|
195
|
+
{el.sub ? (
|
|
196
|
+
<div style={{ marginTop: tam * 0.3, display: 'inline-block', background: alfa('#ffffff', 0.86), borderRadius: 6, padding: '0.3em 0.7em',
|
|
197
|
+
fontFamily: MONO(el.fonteMono), fontSize: tam * 0.42, letterSpacing: '0.06em', color: '#3a3a3a', whiteSpace: 'nowrap',
|
|
198
|
+
opacity: t >= tPill + 5 / fps ? 1 : 0 }}>{el.sub}</div>
|
|
199
|
+
) : null}
|
|
200
|
+
</div>
|
|
201
|
+
</>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── source tag ───────────────────────────────────────────────────────────
|
|
206
|
+
export function Fonte({ el, t, fps }) {
|
|
207
|
+
const t0 = el.t || 0;
|
|
208
|
+
const tam = el.tamanho || 22;
|
|
209
|
+
const txt = String(el.texto || '');
|
|
210
|
+
const pBarra = CURVAS_KIT.editorial(entre(t, t0, t0 + 6 / fps));
|
|
211
|
+
const n = Math.max(0, Math.min(txt.length, Math.floor((t - t0 - 4 / fps) * fps * 1.6)));
|
|
212
|
+
return (
|
|
213
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, display: 'flex', alignItems: 'stretch', gap: tam * 0.6,
|
|
214
|
+
transform: el.alinhamento === 'right' ? 'translateX(-100%)' : undefined }}>
|
|
215
|
+
<div style={{ width: Math.max(3, tam * 0.18), background: el.corAcento || '#d8342b', transform: 'scaleY(' + pBarra + ')', transformOrigin: '50% 0%' }} />
|
|
216
|
+
<div style={{ fontFamily: MONO(el.fonteMono), fontSize: tam, letterSpacing: '0.08em', lineHeight: 1.35, color: el.cor || '#4a4740',
|
|
217
|
+
textTransform: 'uppercase', whiteSpace: 'pre', padding: '0.1em 0' }}>
|
|
218
|
+
{txt.slice(0, n)}<span style={{ opacity: 0 }}>{txt.slice(n)}</span>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── marca-texto e caneta ─────────────────────────────────────────────────
|
|
225
|
+
export function MarcaTexto({ el, t, fps }) {
|
|
226
|
+
const linhas = el.linhas || [];
|
|
227
|
+
const t0 = el.t || 0;
|
|
228
|
+
const passo = 4 / fps;
|
|
229
|
+
const alvo = el.marca == null ? -1 : el.marca;
|
|
230
|
+
const tMarca = el.tMarca != null ? el.tMarca : t0 + linhas.length * passo + 0.3;
|
|
231
|
+
const tam = el.tamanho || 44;
|
|
232
|
+
const pMarca = CURVAS_KIT.arrasto(entre(t, tMarca, tMarca + 12 / fps));
|
|
233
|
+
const pCirculo = traco(t, tMarca + 14 / fps, fps, 15);
|
|
234
|
+
const papel = el.fundo || '#f7f4ee';
|
|
235
|
+
return (
|
|
236
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, width: el.largura, background: papel, boxShadow: SOMBRA_CONTATO,
|
|
237
|
+
padding: tam * 1.1 + 'px ' + tam * 1.2 + 'px', transform: 'rotate(' + (el.rotacao ?? -0.6) + 'deg)', transformOrigin: '50% 50%',
|
|
238
|
+
opacity: t >= t0 - 0.05 ? 1 : 0 }}>
|
|
239
|
+
{el.titulo ? (
|
|
240
|
+
<div style={{ fontFamily: MONO(el.fonteMono), fontSize: tam * 0.42, letterSpacing: '0.16em', color: '#8a8378', textTransform: 'uppercase',
|
|
241
|
+
borderBottom: '1px solid #d6d0c4', paddingBottom: tam * 0.4, marginBottom: tam * 0.6 }}>{el.titulo}</div>
|
|
242
|
+
) : null}
|
|
243
|
+
{linhas.map((ln, i) => {
|
|
244
|
+
const tL = t0 + i * passo;
|
|
245
|
+
// A linha é impressa da esquerda para a direita, em 5 quadros: o documento é LIDO.
|
|
246
|
+
const pL = CURVAS_KIT.editorial(entre(t, tL, tL + 5 / fps));
|
|
247
|
+
const ehAlvo = i === alvo;
|
|
248
|
+
return (
|
|
249
|
+
<div key={i} style={{ position: 'relative', fontFamily: familia(el.fonte, 'Georgia, serif'), fontSize: tam, lineHeight: 1.36,
|
|
250
|
+
color: el.cor || '#23201b' }}>
|
|
251
|
+
<span style={{ position: 'relative', display: 'inline-block' }}>
|
|
252
|
+
{ehAlvo ? <span style={{ position: 'absolute', left: '-0.12em', top: '0.12em', bottom: '0.06em', width: 'calc(100% + 0.24em)',
|
|
253
|
+
background: el.corMarca || alfa(el.corAcento || '#d8342b', 0.32), transform: 'scaleX(' + pMarca + ')', transformOrigin: '0% 50%',
|
|
254
|
+
mixBlendMode: 'multiply', borderRadius: '0.1em' }} /> : null}
|
|
255
|
+
{/* O recorte que imprime a linha fica no TEXTO: no bloco, cortaria o círculo da caneta. */}
|
|
256
|
+
<span style={{ position: 'relative', display: 'inline-block', clipPath: 'inset(-0.2em ' + (100 - pL * 100).toFixed(1) + '% -0.2em 0)', filter: TINTA(el) }}>{ln}</span>
|
|
257
|
+
{ehAlvo ? <Caneta d={CIRCULO} p={pCirculo} cor={el.corAcento || '#d8342b'} espessura={Math.max(3, tam * 0.08)} inset="-0.32em -0.55em -0.28em -0.5em" /> : null}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── texto-textura ────────────────────────────────────────────────────────
|
|
267
|
+
export function Textura({ el, t, fps }) {
|
|
268
|
+
const W = el.largura || 1920, H = el.altura || 1080;
|
|
269
|
+
const tam = el.tamanho || 96;
|
|
270
|
+
const passo = tam * (el.entrelinha || 1.12);
|
|
271
|
+
const linhas = el.linhas || Math.ceil(H / passo) + 2;
|
|
272
|
+
const sep = el.separador ?? ' · ';
|
|
273
|
+
const unidade = String(el.texto || '') + sep;
|
|
274
|
+
const repete = Math.ceil((W * 2.2) / Math.max(1, unidade.length * tam * 0.55)) + 2;
|
|
275
|
+
const meio = el.destaque ?? Math.floor(linhas / 2);
|
|
276
|
+
const cor = el.cor || '#141414';
|
|
277
|
+
const t0 = el.t || 0;
|
|
278
|
+
const contorno = el.modo === 'contorno';
|
|
279
|
+
const tMarca = el.tMarca ?? t0 + 0.5;
|
|
280
|
+
const top0 = (H - linhas * passo) / 2;
|
|
281
|
+
return (
|
|
282
|
+
<div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, overflow: 'hidden' }}>
|
|
283
|
+
{Array.from({ length: linhas }).map((_, i) => {
|
|
284
|
+
const ehMeio = i === meio;
|
|
285
|
+
// As linhas andam em sentidos alternados: o campo respira, não fica chapado.
|
|
286
|
+
const vel = (el.velocidade ?? 40) * (i % 2 ? 1 : -1) * (ehMeio ? 0.5 : 1);
|
|
287
|
+
const x = -((acaso(i + 3) * unidade.length * tam * 0.5) % (unidade.length * tam * 0.55)) + vel * t;
|
|
288
|
+
const aparece = t >= t0 + Math.abs(i - meio) * 2 / fps;
|
|
289
|
+
const estilo = ehMeio
|
|
290
|
+
? { color: el.corDestaque || cor, fontWeight: 900 }
|
|
291
|
+
: contorno
|
|
292
|
+
? { color: 'transparent', WebkitTextStroke: Math.max(1.5, tam * 0.018) + 'px ' + alfa(cor, 0.55), fontWeight: 900 }
|
|
293
|
+
: { color: alfa(cor, 0.16), fontWeight: 600 };
|
|
294
|
+
return (
|
|
295
|
+
<div key={i} style={{ position: 'absolute', left: x, top: top0 + i * passo, whiteSpace: 'nowrap', fontFamily: familia(el.fonte),
|
|
296
|
+
fontSize: tam, lineHeight: 1, letterSpacing: el.espacamento ?? '-0.01em', textTransform: el.caixaAlta === false ? undefined : 'uppercase',
|
|
297
|
+
opacity: aparece ? 1 : 0, ...estilo }}>
|
|
298
|
+
{ehMeio && el.marcar ? (
|
|
299
|
+
<>
|
|
300
|
+
{unidade.repeat(1)}
|
|
301
|
+
<span style={{ position: 'relative', display: 'inline-block' }}>
|
|
302
|
+
<span style={{ position: 'absolute', left: '-0.15em', right: '-0.15em', top: '0.08em', bottom: '0.02em',
|
|
303
|
+
background: alfa(el.corAcento || '#d8342b', 0.35), mixBlendMode: 'multiply',
|
|
304
|
+
transform: 'scaleX(' + CURVAS_KIT.arrasto(entre(t, tMarca, tMarca + 12 / fps)) + ')', transformOrigin: '0% 50%' }} />
|
|
305
|
+
<span style={{ position: 'relative' }}>{el.texto}</span>
|
|
306
|
+
<Caneta d={CIRCULO} p={traco(t, tMarca + 14 / fps, fps, 15)} cor={el.corAcento || '#d8342b'} espessura={Math.max(3, tam * 0.05)} inset="-0.3em -0.5em" />
|
|
307
|
+
</span>
|
|
308
|
+
{sep + unidade.repeat(repete)}
|
|
309
|
+
</>
|
|
310
|
+
) : unidade.repeat(repete)}
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── o gráfico em tinta ───────────────────────────────────────────────────
|
|
319
|
+
export function Barras({ el, t, fps }) {
|
|
320
|
+
const barras = el.barras || [];
|
|
321
|
+
const max = Math.max(1, ...barras.map((b) => Number(b.valor) || 0));
|
|
322
|
+
const iMax = barras.findIndex((b) => Number(b.valor) === max);
|
|
323
|
+
const W = el.largura || 900;
|
|
324
|
+
const tam = el.tamanho || 30;
|
|
325
|
+
const alt = el.alturaBarra || tam * 1.5;
|
|
326
|
+
const rotuloW = el.rotuloLargura || W * 0.28;
|
|
327
|
+
const t0 = el.t || 0;
|
|
328
|
+
const fim = t0 + barras.length * 5 / fps + 0.35;
|
|
329
|
+
return (
|
|
330
|
+
<div style={{ position: 'absolute', left: el.x, top: el.y, width: W }}>
|
|
331
|
+
{barras.map((b, i) => {
|
|
332
|
+
const tb = t0 + i * 5 / fps;
|
|
333
|
+
const m = mola(t, tb, fps);
|
|
334
|
+
const w = (W - rotuloW - tam * 4) * (Number(b.valor) || 0) / max;
|
|
335
|
+
const ehMax = i === iMax;
|
|
336
|
+
return (
|
|
337
|
+
<div key={i} style={{ display: 'flex', alignItems: 'center', height: alt, marginBottom: alt * 0.45 }}>
|
|
338
|
+
<div style={{ width: rotuloW, fontFamily: MONO(el.fonteMono), fontSize: tam * 0.8, letterSpacing: '0.06em', color: '#5a554c',
|
|
339
|
+
textTransform: 'uppercase', opacity: t >= tb ? 1 : 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{b.rotulo}</div>
|
|
340
|
+
<div style={{ height: alt, width: Math.max(0, w * m), background: ehMax ? (el.corAcento || '#d8342b') : (el.cor || '#1a1a1a'),
|
|
341
|
+
boxShadow: '0 1px 1px rgba(0,0,0,0.25)', filter: TINTA(el) }} />
|
|
342
|
+
<div style={{ position: 'relative', marginLeft: tam * 0.6, fontFamily: familia(el.fonte), fontWeight: 800, fontSize: tam * 1.2,
|
|
343
|
+
color: ehMax ? (el.corAcento || '#d8342b') : (el.cor || '#1a1a1a'), ...carimbo(t, tb + 6 / fps, fps, i + 41, 0.5) }}>
|
|
344
|
+
{b.texto || b.valor}
|
|
345
|
+
{ehMax ? <Caneta d={CIRCULO} p={traco(t, fim, fps, 15)} cor={el.corAcento || '#d8342b'} espessura={Math.max(3, tam * 0.12)} inset="-0.35em -0.6em" /> : null}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
})}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export const VOX = {
|
|
355
|
+
palavra: Palavra, lista: Lista, letra: Letra, especime: Especime, pilula: Pilula,
|
|
356
|
+
fonte: Fonte, marcatexto: MarcaTexto, textura: Textura, barras: Barras,
|
|
357
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kit.js — o kit de motion do Estúdio, do lado do PrimoCode.
|
|
3
|
+
*
|
|
4
|
+
* "Motion graphics absurdo de profissional", com as regras do guia MOTION
|
|
5
|
+
* VOX STYLE e o repertório dos vídeos de referência (brand reveal, reel de
|
|
6
|
+
* motion designer, lançamento de SaaS).
|
|
7
|
+
*
|
|
8
|
+
* As peças são React e moram em lib/estudio/kit/*.jsx: arquivos de verdade,
|
|
9
|
+
* que se leem e se editam como código, copiados para src/kit/ do projeto
|
|
10
|
+
* Remotion por remotion.js. No plano, cada peça é um elemento
|
|
11
|
+
* {"tipo": "kit", "peca": "<nome>", …} — o plano continua sendo só dados.
|
|
12
|
+
*
|
|
13
|
+
* Aqui ficam o NOME de cada peça e o que ela precisa para desenhar alguma
|
|
14
|
+
* coisa. É isso que o validador do plano confere: peça que não existe, ou
|
|
15
|
+
* sem o campo que a alimenta, vira problema agora, e não um quadro vazio
|
|
16
|
+
* vinte minutos depois.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
/* precisa: todos estes campos; umDe: pelo menos um destes. listas: campos que
|
|
25
|
+
têm de ser lista não vazia. */
|
|
26
|
+
const PECAS = {
|
|
27
|
+
// os oito dispositivos VOX (e o gráfico em tinta)
|
|
28
|
+
palavra: { precisa: ['palavras'], listas: ['palavras'] },
|
|
29
|
+
lista: { precisa: ['itens'], listas: ['itens'] },
|
|
30
|
+
letra: { precisa: ['texto'] },
|
|
31
|
+
especime: { precisa: ['termo'] },
|
|
32
|
+
pilula: { precisa: ['texto'] },
|
|
33
|
+
fonte: { precisa: ['texto'] },
|
|
34
|
+
marcatexto: { precisa: ['linhas'], listas: ['linhas'] },
|
|
35
|
+
textura: { precisa: ['texto'] },
|
|
36
|
+
barras: { precisa: ['barras'], listas: ['barras'] },
|
|
37
|
+
papel: {},
|
|
38
|
+
// marca e lançamento
|
|
39
|
+
orbe: {},
|
|
40
|
+
vetor: { umDe: ['glifos', 'texto'] },
|
|
41
|
+
manchete: { umDe: ['texto', 'contar'] },
|
|
42
|
+
cromo: {},
|
|
43
|
+
cores: { precisa: ['cores'], listas: ['cores'] },
|
|
44
|
+
padrao: {},
|
|
45
|
+
particulas: {},
|
|
46
|
+
estrela: {},
|
|
47
|
+
cartoes: { precisa: ['cartoes'] },
|
|
48
|
+
painel: {},
|
|
49
|
+
esfera: {},
|
|
50
|
+
grade: {},
|
|
51
|
+
site: {},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'index.jsx'];
|
|
55
|
+
|
|
56
|
+
/** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
|
|
57
|
+
function arquivos() {
|
|
58
|
+
return ARQUIVOS.map((f) => ({ nome: 'kit/' + f, conteudo: fs.readFileSync(path.join(__dirname, 'kit', f), 'utf-8') }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** O que falta nesta peça para ela desenhar alguma coisa. [] = pronta. */
|
|
62
|
+
function conferirPeca(el) {
|
|
63
|
+
const def = PECAS[el && el.peca];
|
|
64
|
+
if (!def) return [`peça "${el && el.peca}" não existe no kit — use ${Object.keys(PECAS).join(', ')}`];
|
|
65
|
+
const falta = [];
|
|
66
|
+
for (const campo of def.precisa || []) if (el[campo] == null || el[campo] === '') falta.push(`a peça "${el.peca}" precisa de "${campo}"`);
|
|
67
|
+
for (const campo of def.listas || []) if (el[campo] != null && (!Array.isArray(el[campo]) || !el[campo].length)) falta.push(`"${campo}" é uma lista com pelo menos um item`);
|
|
68
|
+
if (def.umDe && !def.umDe.some((c) => el[c] != null && el[c] !== '')) falta.push(`a peça "${el.peca}" precisa de ${def.umDe.map((c) => `"${c}"`).join(' ou ')}`);
|
|
69
|
+
return falta;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { PECAS, arquivos, conferirPeca };
|