primocode 9.9.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.
Files changed (63) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/ende.js +2 -0
  12. package/lib/estudio/estilo.js +51 -0
  13. package/lib/estudio/glifos.js +215 -0
  14. package/lib/estudio/index.js +63 -11
  15. package/lib/estudio/kit/base.jsx +285 -0
  16. package/lib/estudio/kit/index.jsx +11 -0
  17. package/lib/estudio/kit/marca.jsx +741 -0
  18. package/lib/estudio/kit/vox.jsx +357 -0
  19. package/lib/estudio/kit.js +72 -0
  20. package/lib/estudio/lancamento.js +413 -0
  21. package/lib/estudio/morfar.js +191 -0
  22. package/lib/estudio/motion.js +333 -0
  23. package/lib/estudio/plano.js +54 -7
  24. package/lib/estudio/remotion.js +167 -52
  25. package/lib/estudio/ritmo.js +77 -0
  26. package/lib/estudio/sons-leves.js +62 -0
  27. package/lib/estudio/vox.js +385 -0
  28. package/lib/fala.js +1 -94
  29. package/lib/pipeline.js +55 -56
  30. package/lib/repeticao.js +3 -1
  31. package/lib/skills.js +8 -2
  32. package/lib/snapshot.js +111 -0
  33. package/lib/tools.js +79 -20
  34. package/lib/ui.js +13 -6
  35. package/lib/video/analise.js +89 -0
  36. package/lib/video/cli.js +74 -0
  37. package/lib/video/delta.js +90 -0
  38. package/lib/video/edits.js +379 -0
  39. package/lib/video/filters.js +247 -0
  40. package/lib/video/ops.js +322 -0
  41. package/lib/video/projeto.js +133 -0
  42. package/lib/video/render.js +221 -0
  43. package/lib/video/texto.js +142 -0
  44. package/lib/video/timeline.js +310 -0
  45. package/lib/video/transcript.js +124 -0
  46. package/lib/voz.js +75 -223
  47. package/package.json +2 -3
  48. package/skills/motion-de-primeira/SKILL.md +21 -1
  49. package/studio/texto.py +208 -0
  50. package/voz/primo_voz/__main__.py +96 -236
  51. package/voz/primo_voz/transcrever.py +31 -1
  52. package/app/ansi.js +0 -55
  53. package/app/app.css +0 -500
  54. package/app/app.js +0 -1017
  55. package/app/icone.svg +0 -6
  56. package/app/index.html +0 -220
  57. package/app/primo-marca.js +0 -192
  58. package/lib/app.js +0 -615
  59. package/lib/icone.js +0 -263
  60. package/lib/instalar-app.js +0 -231
  61. package/lib/painel.js +0 -291
  62. package/voz/primo_voz/desperta.py +0 -195
  63. 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 };