primocode 10.0.0 → 10.1.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 +4 -4
- package/bin/primocode.js +88 -3
- package/lib/act.js +122 -20
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +6 -3
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -2
- package/lib/entrada.js +73 -9
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +63 -25
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +41 -10
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +1471 -0
- package/lib/estudio/hibrido.js +169 -0
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +366 -47
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +990 -0
- package/lib/estudio/kit/index.jsx +4 -1
- package/lib/estudio/kit/tres.jsx +375 -0
- package/lib/estudio/kit.js +9 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +27 -0
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +103 -14
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +183 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +51 -1
- package/lib/tools.js +53 -8
- package/lib/ui.js +41 -6
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +1 -1
- package/lib/video/filters.js +39 -6
- 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 +30 -5
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +67 -14
- package/lib/video/texto.js +6 -4
- package/lib/video/timeline.js +9 -1
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +5 -4
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/voz_kokoro.py +98 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* camadas.js — as peças comuns das operações "de efeito" (separar, trocar
|
|
3
|
+
* fundo, objeto na mão, legenda na cabeça): achar a fala, espelhar clipes
|
|
4
|
+
* sobre ela, ordenar as faixas por papel e converter "o estalo", "quando eu
|
|
5
|
+
* falar camadas" ou um quadro em SEGUNDO DA ORIGEM.
|
|
6
|
+
*
|
|
7
|
+
* ── POR QUE TUDO MORA EM SEGUNDO DA ORIGEM ───────────────────────────────
|
|
8
|
+
* O que se rastreia (a mão, a cabeça, o estalo) aconteceu na GRAVAÇÃO. Se a
|
|
9
|
+
* receita de um efeito guardasse quadro da timeline, o primeiro corte de
|
|
10
|
+
* silêncio a deixaria apontando para o lugar errado. Guardando o segundo da
|
|
11
|
+
* origem, o efeito é redesenhado em cima da timeline que existir — e a
|
|
12
|
+
* camada gerada entra "espelhando" os clipes da fala: o corte que pega a
|
|
13
|
+
* fala pega a camada no mesmo quadro.
|
|
14
|
+
*
|
|
15
|
+
* ── AS FAIXAS TÊM PAPEL ──────────────────────────────────────────────────
|
|
16
|
+
* De cima para baixo: textos, objetos 3D, o momento das camadas, a pessoa
|
|
17
|
+
* recortada, o fundo novo e, por baixo de tudo, a gravação (que segue
|
|
18
|
+
* levando o áudio). `arrumar` impõe essa ordem depois de cada operação, em
|
|
19
|
+
* qualquer ordem que o pedido venha.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const T = require('./timeline.js');
|
|
25
|
+
|
|
26
|
+
const ORDEM = { objetos: 1, camadas3d: 2, pessoa: 3, fundo: 4 };
|
|
27
|
+
|
|
28
|
+
/** A gravação: a mídia de vídeo pedida, ou a primeira com áudio que está na timeline. */
|
|
29
|
+
function base(tl, ref) {
|
|
30
|
+
if (ref) {
|
|
31
|
+
const a = T.acharAsset(tl, ref);
|
|
32
|
+
if (a.kind !== 'video' || a.gerado) throw T.erro(`a mídia ${a.id} não é uma gravação de vídeo`);
|
|
33
|
+
return a;
|
|
34
|
+
}
|
|
35
|
+
const usados = new Set(clipesBase(tl).map((c) => c.asset));
|
|
36
|
+
const lista = Object.values(tl.assets).filter((a) => a.kind === 'video' && !a.gerado && usados.has(a.id));
|
|
37
|
+
const achada = lista.find((a) => a.hasAudio) || lista[0];
|
|
38
|
+
if (!achada) throw T.erro('não há gravação de vídeo na timeline: comece com import {"path":"..."}');
|
|
39
|
+
return achada;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Os clipes da gravação "de verdade": faixa de vídeo sem papel. */
|
|
43
|
+
function clipesBase(tl, assetId) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const t of tl.tracks) {
|
|
46
|
+
if (t.type !== 'video' || t.role) continue;
|
|
47
|
+
for (const c of t.clips) if (c.type === 'video' && (!assetId || c.asset === assetId) && !(tl.assets[c.asset] || {}).gerado) out.push(c);
|
|
48
|
+
}
|
|
49
|
+
return out.sort((a, b) => a.startFrame - b.startFrame);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Quadro da timeline → segundo da origem da gravação (ou null fora dela). */
|
|
53
|
+
function quadroParaOrigem(tl, assetId, f) {
|
|
54
|
+
for (const c of clipesBase(tl, assetId)) {
|
|
55
|
+
if (f >= c.startFrame && f < T.fim(c)) return T.segundos(tl, c.trimStartFrame + (f - c.startFrame) * (c.speed || 1));
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Segundo da origem → quadro da timeline (null se o trecho foi cortado). */
|
|
61
|
+
function origemParaQuadro(tl, assetId, seg) {
|
|
62
|
+
for (const c of clipesBase(tl, assetId)) {
|
|
63
|
+
const f = T.origemParaQuadro(tl, c, seg);
|
|
64
|
+
if (f !== null && f < T.fim(c)) return f;
|
|
65
|
+
}
|
|
66
|
+
// O fim exato da gravação (o "até o fim") cai no fim do último clipe.
|
|
67
|
+
const ult = clipesBase(tl, assetId).slice(-1)[0];
|
|
68
|
+
if (ult && Math.abs(T.janelaOrigem(tl, ult).ate - seg) < 0.5 / tl.fps) return T.fim(ult);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** O clipe da gravação que está no quadro f (para saber o encaixe na tela). */
|
|
73
|
+
function clipeNoQuadro(tl, assetId, f) {
|
|
74
|
+
return clipesBase(tl, assetId).find((c) => f >= c.startFrame && f < T.fim(c)) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function arrumar(tl) {
|
|
78
|
+
const rank = (t) => (t.type === 'text' ? 0 : t.type === 'audio' ? 9 : ORDEM[t.role] || 5);
|
|
79
|
+
tl.tracks = tl.tracks.map((t, i) => ({ t, i })).sort((a, b) => rank(a.t) - rank(b.t) || a.i - b.i).map((x) => x.t);
|
|
80
|
+
return tl;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A faixa com aquele papel (cria se não houver). Devolve o índice já na ordem certa. */
|
|
84
|
+
function faixa(tl, papel) {
|
|
85
|
+
let t = tl.tracks.find((x) => x.role === papel);
|
|
86
|
+
if (!t) {
|
|
87
|
+
const i = T.faixaPara(tl, 'video', 'new');
|
|
88
|
+
t = tl.tracks[i];
|
|
89
|
+
t.role = papel;
|
|
90
|
+
}
|
|
91
|
+
arrumar(tl);
|
|
92
|
+
return tl.tracks.indexOf(t);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Tira os clipes de um papel (de uma gravação), e a faixa se esvaziar. */
|
|
96
|
+
function limparPapel(tl, papel, assetId) {
|
|
97
|
+
for (const t of tl.tracks) {
|
|
98
|
+
if (t.role !== papel) continue;
|
|
99
|
+
t.clips = t.clips.filter((c) => assetId && c.base !== assetId);
|
|
100
|
+
}
|
|
101
|
+
tl.tracks = tl.tracks.filter((t) => t.role !== papel || t.clips.length);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Apaga os assets gerados que nenhum clipe usa mais. */
|
|
105
|
+
function limparGerados(tl) {
|
|
106
|
+
const usados = new Set(T.todosOsClipes(tl).map((x) => x.clip.asset));
|
|
107
|
+
for (const [id, a] of Object.entries(tl.assets)) if (a.gerado && !usados.has(id)) delete tl.assets[id];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Cria, na faixa `ti`, um clipe por clipe da gravação que cai em [de, ate)
|
|
112
|
+
* (segundos da origem), no MESMO lugar da timeline. `asset`: a mídia do
|
|
113
|
+
* clipe novo; `origemDe`: o segundo da gravação que corresponde ao 0 dela
|
|
114
|
+
* (a própria gravação: 0; uma camada gerada a partir de `de`: `de`).
|
|
115
|
+
*/
|
|
116
|
+
function espelhar(tl, assetId, ti, { de = 0, ate = Infinity, asset = assetId, origemDe = 0, props = {}, papel } = {}) {
|
|
117
|
+
const criados = [];
|
|
118
|
+
for (const c of clipesBase(tl, assetId)) {
|
|
119
|
+
const j = T.janelaOrigem(tl, c);
|
|
120
|
+
const i0 = Math.max(j.de, de);
|
|
121
|
+
const i1 = Math.min(j.ate, ate);
|
|
122
|
+
if (i1 - i0 < 0.5 / tl.fps) continue;
|
|
123
|
+
const s = T.origemParaQuadro(tl, c, i0);
|
|
124
|
+
const e = Math.min(T.fim(c), T.origemParaQuadro(tl, c, i1));
|
|
125
|
+
if (s === null || e === null || e <= s) continue;
|
|
126
|
+
const o = s - c.startFrame;
|
|
127
|
+
const kf = {};
|
|
128
|
+
for (const [k, keys] of Object.entries(c.keyframes || {})) if (k !== 'volume') kf[k] = keys.map((x) => ({ ...x, frame: x.frame - o }));
|
|
129
|
+
const novo = T.clipe(tl, {
|
|
130
|
+
type: 'video', asset, startFrame: s, durationFrames: e - s,
|
|
131
|
+
trimStartFrame: Math.max(0, Math.round((i0 - origemDe) * tl.fps)), speed: c.speed,
|
|
132
|
+
transform: { ...c.transform }, crop: { ...c.crop }, fit: c.fit, volume: 0, ...props,
|
|
133
|
+
});
|
|
134
|
+
novo.keyframes = kf;
|
|
135
|
+
novo.base = assetId;
|
|
136
|
+
if (papel) novo.papel = papel;
|
|
137
|
+
tl.tracks[ti].clips.push(novo);
|
|
138
|
+
criados.push(novo);
|
|
139
|
+
}
|
|
140
|
+
T.ordenar(tl);
|
|
141
|
+
return criados;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── os momentos ──────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
const sem = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
147
|
+
|
|
148
|
+
/** A primeira palavra da transcrição que casa (sem acento, sem espaço: "chat gpt" casa "chatgpt"). */
|
|
149
|
+
function acharPalavra(palavras, alvo, { desde = 0 } = {}) {
|
|
150
|
+
const a = sem(alvo);
|
|
151
|
+
if (!a) return null;
|
|
152
|
+
for (let i = 0; i < palavras.length; i++) {
|
|
153
|
+
const p = palavras[i];
|
|
154
|
+
if (p.de < desde) continue;
|
|
155
|
+
const um = sem(p.texto);
|
|
156
|
+
const dois = um + sem((palavras[i + 1] || {}).texto);
|
|
157
|
+
if (um.startsWith(a) || (a.length > 4 && um.length >= 4 && a.startsWith(um) && dois.startsWith(a))) return p;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Cor por nome (português ou inglês) → hex. Hex passa direto. */
|
|
163
|
+
const CORES = {
|
|
164
|
+
azul: '#2F80FF', blue: '#2F80FF', 'azul claro': '#5AC8FA', ciano: '#22D3EE', cyan: '#22D3EE',
|
|
165
|
+
vermelho: '#EF4444', vermelha: '#EF4444', red: '#EF4444', verde: '#22C55E', green: '#22C55E',
|
|
166
|
+
amarelo: '#FFD400', amarela: '#FFD400', yellow: '#FFD400', laranja: '#FF8A00', orange: '#FF8A00',
|
|
167
|
+
rosa: '#FF4FA3', pink: '#FF4FA3', roxo: '#8B5CF6', roxa: '#8B5CF6', purple: '#8B5CF6', lilas: '#C084FC',
|
|
168
|
+
branco: '#FFFFFF', branca: '#FFFFFF', white: '#FFFFFF', preto: '#000000', preta: '#000000', black: '#000000',
|
|
169
|
+
dourado: '#F5C542', dourada: '#F5C542', gold: '#F5C542', cinza: '#9CA3AF', gray: '#9CA3AF',
|
|
170
|
+
};
|
|
171
|
+
function cor(nome) {
|
|
172
|
+
if (nome === undefined || nome === null) return undefined;
|
|
173
|
+
const s = String(nome).trim();
|
|
174
|
+
if (/^#?[0-9a-f]{6}$/i.test(s)) return s.startsWith('#') ? s.toUpperCase() : '#' + s.toUpperCase();
|
|
175
|
+
const k = s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
176
|
+
if (CORES[k]) return CORES[k];
|
|
177
|
+
throw T.erro(`cor "${nome}" (use #RRGGBB ou azul/vermelho/verde/amarelo/laranja/rosa/roxo/ciano/branco/preto/dourado/cinza)`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Fundo pedido por palavra → termo de busca que acha foto boa (o acervo é em inglês). */
|
|
181
|
+
const FUNDOS = {
|
|
182
|
+
praia: 'tropical beach turquoise sea', beach: 'tropical beach turquoise sea', mar: 'ocean waves', espaco: 'outer space nebula stars',
|
|
183
|
+
space: 'outer space nebula stars', cidade: 'city skyline night', floresta: 'green forest', montanha: 'mountain landscape',
|
|
184
|
+
escritorio: 'modern office interior', deserto: 'desert dunes', neve: 'snowy mountains', estudio: 'dark studio backdrop',
|
|
185
|
+
paris: 'paris eiffel tower', 'nova york': 'new york skyline', por_do_sol: 'sunset sky', 'por do sol': 'sunset sky',
|
|
186
|
+
};
|
|
187
|
+
function termoDeFundo(pedido) {
|
|
188
|
+
const k = String(pedido || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim();
|
|
189
|
+
return FUNDOS[k] || FUNDOS[k.replace(/^(uma?|o|a|na|no|numa|num)\s+/, '')] || k;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Objeto pedido → modelo do fx3d/modelos.jsx. */
|
|
193
|
+
function modeloDe(nome) {
|
|
194
|
+
const k = sem(nome);
|
|
195
|
+
if (/^(chatgpt|gpt|openai|chat)/.test(k)) return 'chatgpt';
|
|
196
|
+
if (/^(claude|claud|anthropic)/.test(k)) return 'claude';
|
|
197
|
+
if (/^(brain|cerebro)/.test(k)) return 'cerebro';
|
|
198
|
+
if (/^(chrome|google ?chrome|navegador)/.test(k)) return 'chrome';
|
|
199
|
+
if (/^(drive|google ?drive)/.test(k)) return 'drive';
|
|
200
|
+
if (/^(foguete|rocket|nave)/.test(k)) return 'foguete';
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
ORDEM, base, clipesBase, quadroParaOrigem, origemParaQuadro, clipeNoQuadro, arrumar, faixa, limparPapel, limparGerados,
|
|
206
|
+
espelhar, acharPalavra, sem, cor, CORES, termoDeFundo, FUNDOS, modeloDe,
|
|
207
|
+
};
|
package/lib/video/cli.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main(argv, { out = process.stdout, err = process.stderr } = {}) {
|
|
|
52
52
|
try {
|
|
53
53
|
const { op, args } = lerArgs(argv);
|
|
54
54
|
if (!op || op === 'help' || op === '--help' || op === '-h') {
|
|
55
|
-
out.write(ajuda() + '\n');
|
|
55
|
+
out.write(ajuda(argv[1]) + '\n');
|
|
56
56
|
return 0;
|
|
57
57
|
}
|
|
58
58
|
const nome = ALIAS[op] || op;
|
package/lib/video/filters.js
CHANGED
|
@@ -100,6 +100,27 @@ const EFEITOS = {
|
|
|
100
100
|
params: 'color (hex, #00FF00), similarity (0.12), blend (0.08)',
|
|
101
101
|
filtro: (p) => ['format=yuva420p', `chromakey=${String(p.color || '#00FF00').replace('#', '0x')}:${num(p.similarity || 0.12)}:${num(p.blend || 0.08)}`],
|
|
102
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
|
+
},
|
|
103
124
|
};
|
|
104
125
|
|
|
105
126
|
function filtrosDoEfeito(e) {
|
|
@@ -145,7 +166,7 @@ function atempo(speed) {
|
|
|
145
166
|
* Monta tudo. `camadasDeTexto`: {índiceDaFaixa: caminho do .ffconcat}.
|
|
146
167
|
* `escala`: 0.5 para prévia. Devolve {inputs, grafo, saidaV, saidaA, segundos}.
|
|
147
168
|
*/
|
|
148
|
-
function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio = true } = {}) {
|
|
169
|
+
function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio = true, video = true } = {}) {
|
|
149
170
|
const ate = to === undefined ? T.duracao(tl) : Math.min(to, T.duracao(tl));
|
|
150
171
|
if (!(ate > from)) throw T.erro('nada para renderizar nesse trecho');
|
|
151
172
|
const fps = tl.fps;
|
|
@@ -161,7 +182,7 @@ function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio
|
|
|
161
182
|
for (let ti = tl.tracks.length - 1; ti >= 0; ti--) {
|
|
162
183
|
const faixa = tl.tracks[ti];
|
|
163
184
|
if (faixa.type === 'text') {
|
|
164
|
-
const arq = camadasDeTexto[ti];
|
|
185
|
+
const arq = video ? camadasDeTexto[ti] : null;
|
|
165
186
|
if (!arq || faixa.hidden) continue;
|
|
166
187
|
const k = inputs.length;
|
|
167
188
|
inputs.push(['-f', 'concat', '-safe', '0', '-i', arq]);
|
|
@@ -177,13 +198,17 @@ function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio
|
|
|
177
198
|
if (!a) continue;
|
|
178
199
|
const speed = c.speed || 1;
|
|
179
200
|
const temSom = a.kind === 'audio' || (a.kind === 'video' && a.hasAudio);
|
|
180
|
-
|
|
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;
|
|
181
203
|
if (!visivel && !(audio && temSom)) continue;
|
|
182
204
|
const k = inputs.length;
|
|
183
205
|
const startS = j.start / fps;
|
|
184
206
|
const durS = j.dur / fps;
|
|
185
207
|
if (a.kind === 'image') inputs.push(['-loop', '1', '-framerate', String(fps), '-t', num(durS), '-i', a.path]);
|
|
186
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]); }
|
|
187
212
|
const X = `on/${fps}+${num(j.corte / fps)}`; // tempo do clipe, dentro do zoompan
|
|
188
213
|
const Xt = `t+${num(j.corte / fps)}`; // tempo do clipe, nos filtros com t local
|
|
189
214
|
const Xo = `(t-${num(startS)})+${num(j.corte / fps)}`; // tempo do clipe, no overlay (t global)
|
|
@@ -210,7 +235,13 @@ function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio
|
|
|
210
235
|
const rot = expressao(c.keyframes.rotation, Xt, fps) || (c.transform.rotation ? num(c.transform.rotation) : null);
|
|
211
236
|
if (rot) f.push(`rotate=a='(${rot})*PI/180':c=none:ow='hypot(iw,ih)':oh='ow'`);
|
|
212
237
|
f.push(`setpts=PTS+${num(startS)}/TB`);
|
|
213
|
-
|
|
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}]`);
|
|
214
245
|
const cx = expressao(c.keyframes.cx, Xo, fps) || num(c.transform.cx);
|
|
215
246
|
const cy = expressao(c.keyframes.cy, Xo, fps) || num(c.transform.cy);
|
|
216
247
|
const fimS = (j.start + j.dur) / fps;
|
|
@@ -234,14 +265,16 @@ function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio
|
|
|
234
265
|
}
|
|
235
266
|
}
|
|
236
267
|
|
|
237
|
-
|
|
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]`);
|
|
238
271
|
if (!audio) return { inputs, grafo: linhas.join(';\n'), saidaV: '[vout]', saidaA: null, segundos: D, W, H };
|
|
239
272
|
if (audios.length) {
|
|
240
273
|
linhas.push(`${audios.join('')}amix=inputs=${audios.length}:duration=longest:dropout_transition=0:normalize=0,apad,atrim=0:${num(D)}[aout]`);
|
|
241
274
|
} else {
|
|
242
275
|
linhas.push(`anullsrc=r=48000:cl=stereo,atrim=0:${num(D)}[aout]`);
|
|
243
276
|
}
|
|
244
|
-
return { inputs, grafo: linhas.join(';\n'), saidaV: '[vout]', saidaA: '[aout]', segundos: D, W, H };
|
|
277
|
+
return { inputs, grafo: linhas.join(';\n'), saidaV: video ? '[vout]' : null, saidaA: '[aout]', segundos: D, W, H };
|
|
245
278
|
}
|
|
246
279
|
|
|
247
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);
|