primocode 9.10.0 → 10.0.9
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 +169 -342
- package/lib/act.js +360 -39
- package/lib/browser.js +105 -4
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +77 -11
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +21 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +66 -0
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +284 -53
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +13 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +78 -0
- package/lib/estudio/lancamento.js +441 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +139 -51
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +414 -0
- package/lib/fala.js +1 -94
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +44 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +100 -22
- package/lib/ui.js +50 -8
- package/lib/video/analise.js +89 -0
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -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 +280 -0
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +347 -0
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +274 -0
- package/lib/video/texto.js +144 -0
- package/lib/video/timeline.js +318 -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 +5 -5
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- 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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* filters.js — a linha do tempo vira UM filter_complex do ffmpeg.
|
|
3
|
+
*
|
|
4
|
+
* É uma função pura: entra o json, sai a lista de entradas e o texto do
|
|
5
|
+
* grafo. Quem roda é o render.js. Separado assim, o grafo inteiro é testável
|
|
6
|
+
* como string, sem ffmpeg na máquina de teste.
|
|
7
|
+
*
|
|
8
|
+
* ── COMO O GRAFO É MONTADO ───────────────────────────────────────────────
|
|
9
|
+
* · um fundo preto do tamanho do projeto;
|
|
10
|
+
* · cada clipe visual, da faixa de BAIXO para a de cima, entra como uma
|
|
11
|
+
* entrada própria já cortada na origem (-ss/-t na entrada, que é o corte
|
|
12
|
+
* rápido e exato), passa pela cadeia dele (velocidade, recorte, encaixe,
|
|
13
|
+
* zoom, efeitos, opacidade, fade, giro) e é sobreposto na posição;
|
|
14
|
+
* · cada faixa de TEXTO entra como uma sequência de PNGs transparentes (o
|
|
15
|
+
* texto.js decide os estados, o studio/texto.py desenha) e cobre a tela
|
|
16
|
+
* inteira — uma entrada por faixa, não uma por legenda;
|
|
17
|
+
* · o áudio de cada clipe atrasa até o lugar dele e tudo vira um amix.
|
|
18
|
+
*
|
|
19
|
+
* ── POR QUE O ZOOM É zoompan E NÃO scale ─────────────────────────────────
|
|
20
|
+
* Um scale animado muda o TAMANHO do quadro no meio do fluxo, e o overlay
|
|
21
|
+
* não aceita isso. O zoompan devolve sempre o mesmo tamanho e aproxima por
|
|
22
|
+
* dentro — que é exatamente o "punch-in" de quem edita fala para a câmera.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const T = require('./timeline.js');
|
|
28
|
+
|
|
29
|
+
// ── números e expressões ─────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function num(n) {
|
|
32
|
+
const s = Number(n).toFixed(4).replace(/0+$/, '').replace(/\.$/, '');
|
|
33
|
+
return s === '-0' ? '0' : s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Keyframes → expressão do ffmpeg, em função do tempo `X` (segundos desde
|
|
38
|
+
* o começo do clipe). Antes do primeiro vale o primeiro; depois do último,
|
|
39
|
+
* o último.
|
|
40
|
+
*/
|
|
41
|
+
function expressao(keys, X, fps) {
|
|
42
|
+
if (!keys || !keys.length) return null;
|
|
43
|
+
const ks = keys.map((k) => ({ t: k.frame / fps, v: k.value, i: k.interp || 'ease' }));
|
|
44
|
+
let e = num(ks[ks.length - 1].v);
|
|
45
|
+
for (let j = ks.length - 2; j >= 0; j--) {
|
|
46
|
+
const a = ks[j];
|
|
47
|
+
const b = ks[j + 1];
|
|
48
|
+
const dt = Math.max(1e-6, b.t - a.t);
|
|
49
|
+
const p = `((${X})-${num(a.t)})/${num(dt)}`;
|
|
50
|
+
const curva = a.i === 'hold' ? '0' : a.i === 'linear' ? p : `(${p})*(${p})*(3-2*(${p}))`;
|
|
51
|
+
const seg = a.i === 'hold' ? num(a.v) : `${num(a.v)}+(${num(b.v - a.v)})*${curva}`;
|
|
52
|
+
e = `if(lt(${X},${num(b.t)}),${seg},${e})`;
|
|
53
|
+
}
|
|
54
|
+
return `if(lt(${X},${num(ks[0].t)}),${num(ks[0].v)},${e})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── efeitos ──────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const PRESETS_COR = {
|
|
60
|
+
warm: { temperature: 0.35, saturation: 0.1 },
|
|
61
|
+
cool: { temperature: -0.35, saturation: 0.05 },
|
|
62
|
+
cinematic: { contrast: 0.18, saturation: -0.2, temperature: 0.12, curves: 'medium_contrast' },
|
|
63
|
+
punchy: { contrast: 0.2, saturation: 0.35 },
|
|
64
|
+
bw: { saturation: -1, contrast: 0.15 },
|
|
65
|
+
vintage: { curves: 'vintage', saturation: -0.15 },
|
|
66
|
+
bright: { exposure: 0.25, gamma: 1.1 },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const CURVAS = ['color_negative', 'cross_process', 'darker', 'increase_contrast', 'lighter', 'linear_contrast', 'medium_contrast', 'negative', 'strong_contrast', 'vintage'];
|
|
70
|
+
|
|
71
|
+
const lim = (v, a, b) => Math.max(a, Math.min(b, Number(v)));
|
|
72
|
+
|
|
73
|
+
// Cada efeito: parâmetros com padrão → os filtros do ffmpeg.
|
|
74
|
+
const EFEITOS = {
|
|
75
|
+
'color.adjust': {
|
|
76
|
+
params: 'exposure contrast saturation temperature tint (-1..1), gamma (0.1..3), curves',
|
|
77
|
+
filtro(p) {
|
|
78
|
+
const f = [];
|
|
79
|
+
const eq = [];
|
|
80
|
+
if (p.exposure) eq.push(`brightness=${num(lim(p.exposure, -1, 1) * 0.3)}`);
|
|
81
|
+
if (p.contrast) eq.push(`contrast=${num(1 + lim(p.contrast, -1, 1))}`);
|
|
82
|
+
if (p.saturation) eq.push(`saturation=${num(1 + lim(p.saturation, -1, 2))}`);
|
|
83
|
+
if (p.gamma && Number(p.gamma) !== 1) eq.push(`gamma=${num(lim(p.gamma, 0.1, 3))}`);
|
|
84
|
+
if (eq.length) f.push('eq=' + eq.join(':'));
|
|
85
|
+
const t = lim(p.temperature || 0, -1, 1) * 0.25;
|
|
86
|
+
const g = lim(p.tint || 0, -1, 1) * -0.2;
|
|
87
|
+
if (t || g) f.push(`colorbalance=rs=${num(t)}:gs=${num(g)}:bs=${num(-t)}:rm=${num(t)}:gm=${num(g)}:bm=${num(-t)}`);
|
|
88
|
+
if (p.curves) {
|
|
89
|
+
if (!CURVAS.includes(p.curves)) throw T.erro(`curves "${p.curves}" (use ${CURVAS.join('/')})`);
|
|
90
|
+
f.push(`curves=preset=${p.curves}`);
|
|
91
|
+
}
|
|
92
|
+
return f;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
'blur.gaussian': { params: 'sigma (px, 8)', filtro: (p) => [`gblur=sigma=${num(lim(p.sigma != null ? p.sigma : 8, 0.1, 200))}`] },
|
|
96
|
+
sharpen: { params: 'amount (0..3, 1)', filtro: (p) => [`unsharp=5:5:${num(lim(p.amount != null ? p.amount : 1, 0, 3))}`] },
|
|
97
|
+
'stylize.vignette': { params: 'strength (0..1, 0.5)', filtro: (p) => [`vignette=angle=${num(0.25 + 0.9 * lim(p.strength !== undefined ? p.strength : 0.5, 0, 1))}`] },
|
|
98
|
+
'stylize.grain': { params: 'strength (0..1, 0.3)', filtro: (p) => [`noise=alls=${Math.round(lim(p.strength != null ? p.strength : 0.3, 0, 1) * 40)}:allf=t+u`] },
|
|
99
|
+
'key.chroma': {
|
|
100
|
+
params: 'color (hex, #00FF00), similarity (0.12), blend (0.08)',
|
|
101
|
+
filtro: (p) => ['format=yuva420p', `chromakey=${String(p.color || '#00FF00').replace('#', '0x')}:${num(p.similarity || 0.12)}:${num(p.blend || 0.08)}`],
|
|
102
|
+
},
|
|
103
|
+
/* A pessoa recortada: a máscara (studio/rastrear.py) entra como uma
|
|
104
|
+
segunda entrada, cortada no MESMO trecho do clipe, e vira o alfa
|
|
105
|
+
(alphamerge) antes de qualquer outro filtro — ver montarGrafo. Aqui
|
|
106
|
+
só ficam os números: `choke` encolhe a borda (px), `feather` a suaviza. */
|
|
107
|
+
'key.matte': {
|
|
108
|
+
params: 'matte (arquivo, posto pelo separate_layers), choke (0..4, 1), feather (0..6, 1.2)',
|
|
109
|
+
filtro: () => [],
|
|
110
|
+
cadeiaDaMascara(p) {
|
|
111
|
+
const f = ['format=gray'];
|
|
112
|
+
for (let i = 0; i < Math.round(lim(p.choke ?? 1, 0, 4)); i++) f.push('erosion');
|
|
113
|
+
const fe = lim(p.feather ?? 1.2, 0, 6);
|
|
114
|
+
if (fe > 0) f.push(`gblur=sigma=${num(fe)}`);
|
|
115
|
+
return f;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
/* A pessoa no fundo novo: um empurrão leve da cor dela para a luz do
|
|
119
|
+
fundo (ganho por canal perto de 1) — o "colado" some sem mudar a pele. */
|
|
120
|
+
'color.match': {
|
|
121
|
+
params: 'r g b (ganho por canal, 0.85..1.15), medido pelo replace_background',
|
|
122
|
+
filtro: (p) => [`colorchannelmixer=rr=${num(lim(p.r ?? 1, 0.85, 1.15))}:gg=${num(lim(p.g ?? 1, 0.85, 1.15))}:bb=${num(lim(p.b ?? 1, 0.85, 1.15))}`],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function filtrosDoEfeito(e) {
|
|
127
|
+
const def = EFEITOS[e.type];
|
|
128
|
+
if (!def) throw T.erro(`efeito "${e.type}" não existe (use ${Object.keys(EFEITOS).join('/')})`);
|
|
129
|
+
return def.filtro(e.params || {});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** apply_color → o efeito color.adjust (preset + ajustes por cima). */
|
|
133
|
+
function efeitoDeCor(args) {
|
|
134
|
+
const base = args.preset ? PRESETS_COR[args.preset] : {};
|
|
135
|
+
if (args.preset && !base) throw T.erro(`preset "${args.preset}" (use ${Object.keys(PRESETS_COR).join('/')})`);
|
|
136
|
+
const params = { ...base };
|
|
137
|
+
for (const k of ['exposure', 'contrast', 'saturation', 'temperature', 'tint', 'gamma', 'curves']) {
|
|
138
|
+
if (args[k] !== undefined) params[k] = args[k];
|
|
139
|
+
}
|
|
140
|
+
filtrosDoEfeito({ type: 'color.adjust', params }); // valida já
|
|
141
|
+
return { type: 'color.adjust', params };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── o grafo ──────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/** O pedaço do clipe que cai na janela [from, to) — ou null. */
|
|
147
|
+
function naJanela(c, from, to) {
|
|
148
|
+
const s = Math.max(c.startFrame, from);
|
|
149
|
+
const e = Math.min(T.fim(c), to);
|
|
150
|
+
if (e <= s) return null;
|
|
151
|
+
const corte = s - c.startFrame;
|
|
152
|
+
return { start: s - from, dur: e - s, corte, trim: c.trimStartFrame + Math.round(corte * (c.speed || 1)) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function atempo(speed) {
|
|
156
|
+
// atempo aceita 0.5..2 por filtro: acima disso, em cadeia.
|
|
157
|
+
const f = [];
|
|
158
|
+
let s = speed;
|
|
159
|
+
while (s > 2) { f.push('atempo=2'); s /= 2; }
|
|
160
|
+
while (s < 0.5) { f.push('atempo=0.5'); s /= 0.5; }
|
|
161
|
+
if (Math.abs(s - 1) > 1e-6) f.push(`atempo=${num(s)}`);
|
|
162
|
+
return f;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Monta tudo. `camadasDeTexto`: {índiceDaFaixa: caminho do .ffconcat}.
|
|
167
|
+
* `escala`: 0.5 para prévia. Devolve {inputs, grafo, saidaV, saidaA, segundos}.
|
|
168
|
+
*/
|
|
169
|
+
function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio = true, video = true } = {}) {
|
|
170
|
+
const ate = to === undefined ? T.duracao(tl) : Math.min(to, T.duracao(tl));
|
|
171
|
+
if (!(ate > from)) throw T.erro('nada para renderizar nesse trecho');
|
|
172
|
+
const fps = tl.fps;
|
|
173
|
+
const W = T.par(tl.width * escala);
|
|
174
|
+
const H = T.par(tl.height * escala);
|
|
175
|
+
const D = (ate - from) / fps;
|
|
176
|
+
const inputs = [];
|
|
177
|
+
const linhas = [`color=c=black:s=${W}x${H}:r=${fps}:d=${num(D)}[base]`];
|
|
178
|
+
const audios = [];
|
|
179
|
+
let atual = 'base';
|
|
180
|
+
let n = 0;
|
|
181
|
+
|
|
182
|
+
for (let ti = tl.tracks.length - 1; ti >= 0; ti--) {
|
|
183
|
+
const faixa = tl.tracks[ti];
|
|
184
|
+
if (faixa.type === 'text') {
|
|
185
|
+
const arq = video ? camadasDeTexto[ti] : null;
|
|
186
|
+
if (!arq || faixa.hidden) continue;
|
|
187
|
+
const k = inputs.length;
|
|
188
|
+
inputs.push(['-f', 'concat', '-safe', '0', '-i', arq]);
|
|
189
|
+
linhas.push(`[${k}:v]fps=${fps},format=rgba,setpts=PTS-STARTPTS[tx${k}]`);
|
|
190
|
+
linhas.push(`[${atual}][tx${k}]overlay=0:0:eof_action=pass[v${++n}]`);
|
|
191
|
+
atual = `v${n}`;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const c of faixa.clips) {
|
|
195
|
+
const j = naJanela(c, from, ate);
|
|
196
|
+
if (!j) continue;
|
|
197
|
+
const a = tl.assets[c.asset];
|
|
198
|
+
if (!a) continue;
|
|
199
|
+
const speed = c.speed || 1;
|
|
200
|
+
const temSom = a.kind === 'audio' || (a.kind === 'video' && a.hasAudio);
|
|
201
|
+
// video:false — só o áudio (o export em partes faz o som numa passada à parte).
|
|
202
|
+
const visivel = video && faixa.type === 'video' && !c.hidden && !faixa.hidden;
|
|
203
|
+
if (!visivel && !(audio && temSom)) continue;
|
|
204
|
+
const k = inputs.length;
|
|
205
|
+
const startS = j.start / fps;
|
|
206
|
+
const durS = j.dur / fps;
|
|
207
|
+
if (a.kind === 'image') inputs.push(['-loop', '1', '-framerate', String(fps), '-t', num(durS), '-i', a.path]);
|
|
208
|
+
else inputs.push(['-ss', num(j.trim / fps), '-t', num(durS * speed + 0.05), '-i', a.path]);
|
|
209
|
+
const matte = visivel && c.effects.find((e) => e.type === 'key.matte' && e.params && e.params.matte);
|
|
210
|
+
let km = null;
|
|
211
|
+
if (matte) { km = inputs.length; inputs.push(['-ss', num(j.trim / fps), '-t', num(durS * speed + 0.05), '-i', matte.params.matte]); }
|
|
212
|
+
const X = `on/${fps}+${num(j.corte / fps)}`; // tempo do clipe, dentro do zoompan
|
|
213
|
+
const Xt = `t+${num(j.corte / fps)}`; // tempo do clipe, nos filtros com t local
|
|
214
|
+
const Xo = `(t-${num(startS)})+${num(j.corte / fps)}`; // tempo do clipe, no overlay (t global)
|
|
215
|
+
|
|
216
|
+
if (visivel) {
|
|
217
|
+
const bw = T.par(c.transform.w * W);
|
|
218
|
+
const bh = T.par(c.transform.h * H);
|
|
219
|
+
const f = [speed !== 1 ? `setpts=(PTS-STARTPTS)/${num(speed)}` : 'setpts=PTS-STARTPTS', `fps=${fps}`];
|
|
220
|
+
const cr = c.crop;
|
|
221
|
+
if (cr.l || cr.t || cr.r || cr.b) f.push(`crop=iw*${num(1 - cr.l - cr.r)}:ih*${num(1 - cr.t - cr.b)}:iw*${num(cr.l)}:ih*${num(cr.t)}`);
|
|
222
|
+
if (c.fit === 'contain') f.push(`scale=${bw}:${bh}:force_original_aspect_ratio=decrease`, 'format=yuva420p', `pad=${bw}:${bh}:(ow-iw)/2:(oh-ih)/2:color=black@0`);
|
|
223
|
+
else if (c.fit === 'stretch') f.push(`scale=${bw}:${bh}`);
|
|
224
|
+
else f.push(`scale=${bw}:${bh}:force_original_aspect_ratio=increase`, `crop=${bw}:${bh}`);
|
|
225
|
+
f.push('setsar=1');
|
|
226
|
+
const zoom = expressao(c.keyframes.zoom, X, fps);
|
|
227
|
+
if (zoom) f.push(`zoompan=z='max(1,${zoom})':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s=${bw}x${bh}:fps=${fps}`);
|
|
228
|
+
for (const e of c.effects) f.push(...filtrosDoEfeito(e));
|
|
229
|
+
f.push('format=yuva420p');
|
|
230
|
+
const op = expressao(c.keyframes.opacity, Xt, fps);
|
|
231
|
+
if (op) f.push('format=rgba', `geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='alpha(X,Y)*max(0,min(1,${op}))'`);
|
|
232
|
+
else if (c.opacity < 1) f.push(`colorchannelmixer=aa=${num(Math.max(0, c.opacity))}`);
|
|
233
|
+
if (c.fadeIn && j.corte === 0) f.push(`fade=t=in:st=0:d=${num(c.fadeIn / fps)}:alpha=1`);
|
|
234
|
+
if (c.fadeOut && j.start + j.dur + from === T.fim(c)) f.push(`fade=t=out:st=${num(Math.max(0, durS - c.fadeOut / fps))}:d=${num(c.fadeOut / fps)}:alpha=1`);
|
|
235
|
+
const rot = expressao(c.keyframes.rotation, Xt, fps) || (c.transform.rotation ? num(c.transform.rotation) : null);
|
|
236
|
+
if (rot) f.push(`rotate=a='(${rot})*PI/180':c=none:ow='hypot(iw,ih)':oh='ow'`);
|
|
237
|
+
f.push(`setpts=PTS+${num(startS)}/TB`);
|
|
238
|
+
if (km !== null) {
|
|
239
|
+
// O alfa vem da máscara, no mesmo tempo e no mesmo tamanho da origem.
|
|
240
|
+
const pre = f.splice(0, 2);
|
|
241
|
+
linhas.push(`[${k}:v]${pre.join(',')}[pre${k}]`);
|
|
242
|
+
linhas.push(`[${km}:v]${[...EFEITOS['key.matte'].cadeiaDaMascara(matte.params), ...pre].join(',')}[mt${k}]`);
|
|
243
|
+
linhas.push(`[pre${k}][mt${k}]alphamerge,${f.join(',')}[c${k}]`);
|
|
244
|
+
} else linhas.push(`[${k}:v]${f.join(',')}[c${k}]`);
|
|
245
|
+
const cx = expressao(c.keyframes.cx, Xo, fps) || num(c.transform.cx);
|
|
246
|
+
const cy = expressao(c.keyframes.cy, Xo, fps) || num(c.transform.cy);
|
|
247
|
+
const fimS = (j.start + j.dur) / fps;
|
|
248
|
+
linhas.push(`[${atual}][c${k}]overlay=x='(${cx})*W-w/2':y='(${cy})*H-h/2':eof_action=pass:enable='between(t,${num(startS)},${num(fimS - 0.0001)})'[v${++n}]`);
|
|
249
|
+
atual = `v${n}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (audio && temSom && !faixa.muted && !(c.volume === 0 && !c.keyframes.volume)) {
|
|
253
|
+
const f = ['asetpts=PTS-STARTPTS', ...atempo(speed), 'aformat=sample_rates=48000:channel_layouts=stereo'];
|
|
254
|
+
const vol = expressao(c.keyframes.volume, Xt, fps);
|
|
255
|
+
if (vol) f.push(`volume='max(0,${vol})':eval=frame`);
|
|
256
|
+
else if (c.volume !== 1) f.push(`volume=${num(c.volume)}`);
|
|
257
|
+
if (c.fadeIn && j.corte === 0) f.push(`afade=t=in:st=0:d=${num(c.fadeIn / fps)}`);
|
|
258
|
+
if (c.fadeOut && j.start + j.dur + from === T.fim(c)) f.push(`afade=t=out:st=${num(Math.max(0, durS - c.fadeOut / fps))}:d=${num(c.fadeOut / fps)}`);
|
|
259
|
+
f.push(`atrim=0:${num(durS)}`);
|
|
260
|
+
const ms = Math.round(startS * 1000);
|
|
261
|
+
if (ms > 0) f.push(`adelay=${ms}:all=1`);
|
|
262
|
+
linhas.push(`[${k}:a]${f.join(',')}[a${k}]`);
|
|
263
|
+
audios.push(`[a${k}]`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Só o áudio: o vídeo (uma cor preta) termina num ralo — saída solta derruba o ffmpeg.
|
|
269
|
+
if (!video) linhas.push(`[${atual}]nullsink`);
|
|
270
|
+
else linhas.push(`[${atual}]format=yuv420p[vout]`);
|
|
271
|
+
if (!audio) return { inputs, grafo: linhas.join(';\n'), saidaV: '[vout]', saidaA: null, segundos: D, W, H };
|
|
272
|
+
if (audios.length) {
|
|
273
|
+
linhas.push(`${audios.join('')}amix=inputs=${audios.length}:duration=longest:dropout_transition=0:normalize=0,apad,atrim=0:${num(D)}[aout]`);
|
|
274
|
+
} else {
|
|
275
|
+
linhas.push(`anullsrc=r=48000:cl=stereo,atrim=0:${num(D)}[aout]`);
|
|
276
|
+
}
|
|
277
|
+
return { inputs, grafo: linhas.join(';\n'), saidaV: video ? '[vout]' : null, saidaA: '[aout]', segundos: D, W, H };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = { num, expressao, EFEITOS, PRESETS_COR, CURVAS, filtrosDoEfeito, efeitoDeCor, naJanela, atempo, montarGrafo };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* fx3d/Camadas.jsx — o momento "separa em camadas": o fundo e a pessoa
|
|
2
|
+
* viram duas placas no espaço, se afastam em profundidade, a câmera gira um
|
|
3
|
+
* pouco para mostrar que são duas, e tudo volta a encostar.
|
|
4
|
+
*
|
|
5
|
+
* POR QUE CSS 3D E NÃO three.js AQUI: as camadas são PLANOS. A perspectiva
|
|
6
|
+
* do CSS (perspective + translateZ + rotateY) projeta plano exatamente como
|
|
7
|
+
* uma câmera, e a imagem de cada quadro entra como <Img> — o Remotion espera
|
|
8
|
+
* cada uma carregar antes de fotografar o quadro. Com three seria preciso
|
|
9
|
+
* subir uma textura por quadro para a GPU e esperar por ela à mão.
|
|
10
|
+
*
|
|
11
|
+
* O primeiro e o último quadro são a composição plana (k = 0): o corte para
|
|
12
|
+
* o vídeo normal, antes e depois, não aparece.
|
|
13
|
+
*
|
|
14
|
+
* props: {n, fps, w, h, dir, fundo, rotulos:{fundo, pessoa}} */
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { AbsoluteFill, Img, staticFile, useCurrentFrame } from 'remotion';
|
|
17
|
+
|
|
18
|
+
const clamp = (x) => Math.max(0, Math.min(1, x));
|
|
19
|
+
const suave = (x) => { const t = clamp(x); return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; };
|
|
20
|
+
|
|
21
|
+
function Rotulo({ texto, k, cor, lado = 'left' }) {
|
|
22
|
+
if (!texto) return null;
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ position: 'absolute', [lado]: '6%', top: '4%', padding: '14px 26px', borderRadius: 999, background: 'rgba(10,14,22,0.72)',
|
|
25
|
+
border: `3px solid ${cor}`, color: '#fff', font: '800 44px/1 system-ui, -apple-system, Helvetica, Arial', letterSpacing: 3,
|
|
26
|
+
opacity: clamp((k - 0.5) * 2.5), transform: `translateZ(2px)` }}>{texto}</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Camadas = (p) => {
|
|
31
|
+
const f = useCurrentFrame();
|
|
32
|
+
const { w, h, n, fps } = p;
|
|
33
|
+
const ent = Math.round(fps * 0.75);
|
|
34
|
+
const sai = Math.round(fps * 0.75);
|
|
35
|
+
const k = f < ent ? suave(f / ent) : f > n - 1 - sai ? suave((n - 1 - f) / sai) : 1;
|
|
36
|
+
const t = f / fps;
|
|
37
|
+
const rotY = -34 * k + Math.sin(t * 1.3) * 7 * k;
|
|
38
|
+
const rotX = 10 * k;
|
|
39
|
+
const escala = 1 - 0.34 * k;
|
|
40
|
+
const pessoa = staticFile(`${p.dir}/p${String(f + 1).padStart(4, '0')}.png`);
|
|
41
|
+
const placa = { position: 'absolute', left: 0, top: 0, width: w, height: h, transformStyle: 'preserve-3d' };
|
|
42
|
+
const borda = (cor) => ({ position: 'absolute', inset: 0, border: `${Math.round(6 * k)}px solid ${cor}`, borderRadius: 18 * k, opacity: k, boxShadow: `0 40px 120px rgba(0,0,0,${0.55 * k})` });
|
|
43
|
+
return (
|
|
44
|
+
<AbsoluteFill style={{ background: 'radial-gradient(ellipse at 50% 40%, #1d2433 0%, #07090f 75%)', perspective: 2600, perspectiveOrigin: '50% 45%', overflow: 'hidden' }}>
|
|
45
|
+
{/* o chão de grade, que dá a noção de espaço */}
|
|
46
|
+
<div style={{ position: 'absolute', left: -w, top: h * 0.62, width: w * 3, height: h, opacity: 0.45 * k,
|
|
47
|
+
transform: 'rotateX(78deg)', transformOrigin: '50% 0%',
|
|
48
|
+
backgroundImage: 'linear-gradient(rgba(120,200,255,0.35) 2px, transparent 2px), linear-gradient(90deg, rgba(120,200,255,0.35) 2px, transparent 2px)',
|
|
49
|
+
backgroundSize: '120px 120px' }} />
|
|
50
|
+
<div style={{ ...placa, transform: `scale(${escala}) rotateX(${rotX}deg) rotateY(${rotY}deg)` }}>
|
|
51
|
+
<div style={{ ...placa, transform: `translateZ(${-560 * k}px)` }}>
|
|
52
|
+
<Img src={staticFile(p.fundo)} style={{ width: w, height: h, borderRadius: 18 * k, filter: `brightness(${1 - 0.18 * k})` }} />
|
|
53
|
+
<div style={borda('rgba(255,190,90,0.95)')} />
|
|
54
|
+
<Rotulo texto={(p.rotulos || {}).fundo} k={k} cor="rgba(255,190,90,0.95)" lado="right" />
|
|
55
|
+
</div>
|
|
56
|
+
<div style={{ ...placa, transform: `translateZ(${300 * k}px)` }}>
|
|
57
|
+
<Img src={pessoa} style={{ width: w, height: h, filter: `drop-shadow(0 0 ${Math.round(22 * k)}px rgba(90,210,255,${0.85 * k}))` }} />
|
|
58
|
+
<div style={{ ...borda('rgba(90,210,255,0.9)'), background: 'transparent' }} />
|
|
59
|
+
<Rotulo texto={(p.rotulos || {}).pessoa} k={k} cor="rgba(90,210,255,0.9)" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</AbsoluteFill>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* fx3d/Objetos.jsx — a camada TRANSPARENTE dos objetos que seguem as mãos.
|
|
2
|
+
*
|
|
3
|
+
* Tudo o que é decisão (onde fica cada objeto em cada quadro, quando entra,
|
|
4
|
+
* quando funde) já vem calculado do lib/video/camadas.js nas props: aqui só
|
|
5
|
+
* se desenha. Assim a conta é testada no node, e o 3D só gira, brilha e voa.
|
|
6
|
+
*
|
|
7
|
+
* props: {n, fps, w, h, objetos:[{modelo, cor?, texto?, f0, f1, trilha:[[x,y,s]|0], funde?}],
|
|
8
|
+
* fusao?: {fj, fm, trilha:[[x,y,s]|0]}}
|
|
9
|
+
* x,y = centro do objeto em frações da tela; s = diâmetro em fração da altura. */
|
|
10
|
+
import React, { useMemo } from 'react';
|
|
11
|
+
import { AbsoluteFill, useCurrentFrame } from 'remotion';
|
|
12
|
+
import { ThreeCanvas } from '@remotion/three';
|
|
13
|
+
import * as THREE from 'three';
|
|
14
|
+
import { useThree } from '@react-three/fiber';
|
|
15
|
+
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
|
16
|
+
import { Modelo } from './modelos';
|
|
17
|
+
|
|
18
|
+
const FOV = 30;
|
|
19
|
+
const DIST = 10;
|
|
20
|
+
const ALTO = 2 * DIST * Math.tan((FOV * Math.PI) / 360); // altura do mundo visível no plano z=0
|
|
21
|
+
|
|
22
|
+
const clamp = (x) => Math.max(0, Math.min(1, x));
|
|
23
|
+
const saida = (x) => 1 - Math.pow(1 - clamp(x), 3);
|
|
24
|
+
const volta = (x) => { const c = 1.9, t = clamp(x) - 1; return 1 + (c + 1) * t * t * t + c * t * t; }; // easeOutBack
|
|
25
|
+
const lerp = (a, b, k) => a + (b - a) * k;
|
|
26
|
+
|
|
27
|
+
function Ambiente() {
|
|
28
|
+
const { gl, scene } = useThree();
|
|
29
|
+
useMemo(() => {
|
|
30
|
+
const pm = new THREE.PMREMGenerator(gl);
|
|
31
|
+
scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
32
|
+
scene.environmentIntensity = 0.9;
|
|
33
|
+
pm.dispose();
|
|
34
|
+
}, [gl, scene]);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Frações da tela → mundo (no plano z=0 da câmera em perspectiva).
|
|
39
|
+
const mundo = (x, y, asp) => [(x - 0.5) * ALTO * asp, (0.5 - y) * ALTO, 0];
|
|
40
|
+
|
|
41
|
+
function Objeto({ o, f, fps, asp, fusao }) {
|
|
42
|
+
const p = o.trilha[f - o.f0] || o.trilha[Math.min(o.trilha.length - 1, Math.max(0, f - o.f0))];
|
|
43
|
+
if (!p) return null;
|
|
44
|
+
let [x, y, s] = p;
|
|
45
|
+
const local = (f - o.f0) / fps;
|
|
46
|
+
let escala = volta(local / 0.5); // entra com mola em 0,5 s
|
|
47
|
+
let giro = -Math.PI * 1.2 * (1 - saida(local / 0.7)) + Math.sin(local * 1.1) * 0.35;
|
|
48
|
+
if (!o.funde && o.f1 - f < fps * 0.3) escala *= saida((o.f1 - f) / (fps * 0.3)); // sai encolhendo
|
|
49
|
+
if (o.funde && fusao && f >= fusao.fj) {
|
|
50
|
+
const m = fusao.trilha[Math.max(0, Math.min(fusao.trilha.length - 1, f - fusao.fj))];
|
|
51
|
+
const k = saida((f - fusao.fj) / Math.max(1, fusao.fm - fusao.fj));
|
|
52
|
+
if (m) { x = lerp(x, m[0], k); y = lerp(y, m[1], k); }
|
|
53
|
+
escala *= 1 - 0.55 * k;
|
|
54
|
+
giro += k * Math.PI * 3;
|
|
55
|
+
if (f >= fusao.fm) return null;
|
|
56
|
+
}
|
|
57
|
+
const flutua = Math.sin(local * 2.4 + o.f0) * 0.012;
|
|
58
|
+
const [wx, wy] = mundo(x, y + flutua, asp);
|
|
59
|
+
const tam = s * ALTO * Math.max(0, escala);
|
|
60
|
+
return (
|
|
61
|
+
<group position={[wx, wy, 0]} rotation={[0.12 + Math.sin(local * 0.9) * 0.08, giro, Math.sin(local * 0.7) * 0.05]} scale={[tam, tam, tam]}>
|
|
62
|
+
<Modelo modelo={o.modelo} cor={o.cor} texto={o.texto} />
|
|
63
|
+
<pointLight position={[0, -0.6, 0.6]} intensity={0.6} distance={3} color={o.modelo === 'cerebro' ? '#ff9fb8' : '#ffffff'} />
|
|
64
|
+
</group>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* O clarão da fusão: uma esfera que abre e some, um anel que corre e
|
|
69
|
+
faíscas que saem do centro (semente fixa: o mesmo quadro sai igual). */
|
|
70
|
+
function Clarao({ fusao, f, fps, asp }) {
|
|
71
|
+
const faiscas = useMemo(() => Array.from({ length: 46 }, (_, i) => {
|
|
72
|
+
const a = (i * 2.399963) % (Math.PI * 2);
|
|
73
|
+
const z = ((i * 0.618) % 1) * 2 - 1;
|
|
74
|
+
const r = Math.sqrt(1 - z * z);
|
|
75
|
+
return { d: [Math.cos(a) * r, Math.sin(a) * r, z], v: 0.7 + ((i * 0.37) % 1) * 0.9, c: i % 3 === 0 ? '#ffd27a' : i % 3 === 1 ? '#9fe8ff' : '#ffffff' };
|
|
76
|
+
}), []);
|
|
77
|
+
if (!fusao || f < fusao.fm) return null;
|
|
78
|
+
const t = (f - fusao.fm) / fps;
|
|
79
|
+
if (t > 0.9) return null;
|
|
80
|
+
const m = fusao.trilha[Math.max(0, Math.min(fusao.trilha.length - 1, f - fusao.fj))];
|
|
81
|
+
if (!m) return null;
|
|
82
|
+
const [wx, wy] = mundo(m[0], m[1], asp);
|
|
83
|
+
const R = m[2] * ALTO;
|
|
84
|
+
const k = saida(t / 0.35);
|
|
85
|
+
return (
|
|
86
|
+
<group position={[wx, wy, 0.5]}>
|
|
87
|
+
<mesh scale={[R * (0.3 + 1.4 * k), R * (0.3 + 1.4 * k), R * (0.3 + 1.4 * k)]}>
|
|
88
|
+
<sphereGeometry args={[0.5, 32, 32]} />
|
|
89
|
+
<meshBasicMaterial color="#fff6e0" transparent opacity={Math.max(0, 0.9 * (1 - t / 0.35))} blending={THREE.AdditiveBlending} depthWrite={false} />
|
|
90
|
+
</mesh>
|
|
91
|
+
<mesh scale={[R * (0.4 + 2.2 * k), R * (0.4 + 2.2 * k), 1]}>
|
|
92
|
+
<ringGeometry args={[0.46, 0.5, 96]} />
|
|
93
|
+
<meshBasicMaterial color="#bfefff" transparent opacity={Math.max(0, 0.8 * (1 - t / 0.5))} blending={THREE.AdditiveBlending} depthWrite={false} side={THREE.DoubleSide} />
|
|
94
|
+
</mesh>
|
|
95
|
+
{faiscas.map((p, i) => {
|
|
96
|
+
const dd = R * p.v * (0.2 + 1.8 * saida(t / 0.8));
|
|
97
|
+
const a = Math.max(0, 1 - t / 0.85);
|
|
98
|
+
return (
|
|
99
|
+
<mesh key={i} position={[p.d[0] * dd, p.d[1] * dd, p.d[2] * dd]} scale={[R * 0.035 * a + 0.001, R * 0.035 * a + 0.001, R * 0.035 * a + 0.001]}>
|
|
100
|
+
<sphereGeometry args={[1, 10, 10]} />
|
|
101
|
+
<meshBasicMaterial color={p.c} transparent opacity={a} blending={THREE.AdditiveBlending} depthWrite={false} />
|
|
102
|
+
</mesh>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</group>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const Objetos = (props) => {
|
|
110
|
+
const f = useCurrentFrame();
|
|
111
|
+
const { w, h, fps } = props;
|
|
112
|
+
const asp = w / h;
|
|
113
|
+
const vivos = (props.objetos || []).filter((o) => f >= o.f0 && f < o.f1);
|
|
114
|
+
return (
|
|
115
|
+
<AbsoluteFill>
|
|
116
|
+
<ThreeCanvas width={w} height={h} camera={{ position: [0, 0, DIST], fov: FOV, near: 0.1, far: 100 }}
|
|
117
|
+
gl={{ antialias: true, alpha: true, preserveDrawingBuffer: true, premultipliedAlpha: false }}>
|
|
118
|
+
<Ambiente />
|
|
119
|
+
<ambientLight intensity={0.35} />
|
|
120
|
+
<directionalLight position={[3, 5, 6]} intensity={2.6} />
|
|
121
|
+
<directionalLight position={[-4, 2, 3]} intensity={1.0} color="#cfe6ff" />
|
|
122
|
+
<directionalLight position={[0, -3, -4]} intensity={1.6} color="#ffd9b0" />
|
|
123
|
+
{vivos.map((o, i) => <Objeto key={i} o={o} f={f} fps={fps} asp={asp} fusao={props.fusao} />)}
|
|
124
|
+
<Clarao fusao={props.fusao} f={f} fps={fps} asp={asp} />
|
|
125
|
+
</ThreeCanvas>
|
|
126
|
+
</AbsoluteFill>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* fx3d/Root.jsx — as duas composições do editor. O tamanho, o fps e a
|
|
2
|
+
* duração vêm das props de cada render (calculateMetadata), não daqui. */
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Composition, registerRoot } from 'remotion';
|
|
5
|
+
import { Camadas } from './Camadas';
|
|
6
|
+
import { Objetos } from './Objetos';
|
|
7
|
+
|
|
8
|
+
const meta = ({ props }) => ({ durationInFrames: Math.max(1, props.n || 1), fps: props.fps || 30, width: props.w || 1080, height: props.h || 1920 });
|
|
9
|
+
const padrao = { n: 30, fps: 30, w: 1080, h: 1920 };
|
|
10
|
+
|
|
11
|
+
const Root = () => (
|
|
12
|
+
<>
|
|
13
|
+
<Composition id="Camadas" component={Camadas} durationInFrames={30} fps={30} width={1080} height={1920} defaultProps={padrao} calculateMetadata={meta} />
|
|
14
|
+
<Composition id="Objetos" component={Objetos} durationInFrames={30} fps={30} width={1080} height={1920} defaultProps={padrao} calculateMetadata={meta} />
|
|
15
|
+
</>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
registerRoot(Root);
|