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.
Files changed (73) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +88 -3
  3. package/lib/act.js +122 -20
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +6 -3
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/config.js +10 -2
  9. package/lib/entrada.js +73 -9
  10. package/lib/estudio/assets.js +12 -6
  11. package/lib/estudio/biblioteca.js +174 -0
  12. package/lib/estudio/cinema.js +63 -25
  13. package/lib/estudio/composicao.js +2 -1
  14. package/lib/estudio/diretor.js +13 -1
  15. package/lib/estudio/edicao.js +2 -1
  16. package/lib/estudio/ende.js +16 -5
  17. package/lib/estudio/estilo.js +41 -10
  18. package/lib/estudio/fluido.js +283 -0
  19. package/lib/estudio/gerativo.js +1471 -0
  20. package/lib/estudio/hibrido.js +169 -0
  21. package/lib/estudio/impacto.js +66 -31
  22. package/lib/estudio/index.js +366 -47
  23. package/lib/estudio/kit/fluido.jsx +493 -0
  24. package/lib/estudio/kit/gerativo.jsx +990 -0
  25. package/lib/estudio/kit/index.jsx +4 -1
  26. package/lib/estudio/kit/tres.jsx +375 -0
  27. package/lib/estudio/kit.js +9 -1
  28. package/lib/estudio/kokoro.js +111 -0
  29. package/lib/estudio/lancamento.js +27 -0
  30. package/lib/estudio/legibilidade.js +20 -0
  31. package/lib/estudio/referencias.js +105 -0
  32. package/lib/estudio/remotion.js +103 -14
  33. package/lib/estudio/ritmo.js +2 -2
  34. package/lib/estudio/sons-leves.js +38 -1
  35. package/lib/estudio/variacao3d.js +46 -0
  36. package/lib/estudio/variedade.js +183 -0
  37. package/lib/estudio/vox.js +37 -8
  38. package/lib/mcp.js +156 -0
  39. package/lib/personas.js +67 -0
  40. package/lib/portao-skill.js +150 -0
  41. package/lib/skills.js +51 -1
  42. package/lib/tools.js +53 -8
  43. package/lib/ui.js +41 -6
  44. package/lib/video/auto.js +329 -0
  45. package/lib/video/camadas.js +207 -0
  46. package/lib/video/cli.js +1 -1
  47. package/lib/video/filters.js +39 -6
  48. package/lib/video/fx3d/Camadas.jsx +64 -0
  49. package/lib/video/fx3d/Objetos.jsx +128 -0
  50. package/lib/video/fx3d/Root.jsx +18 -0
  51. package/lib/video/fx3d/modelos.jsx +304 -0
  52. package/lib/video/fx3d.js +108 -0
  53. package/lib/video/gerados.js +119 -0
  54. package/lib/video/objetos.js +111 -0
  55. package/lib/video/ops-fx.js +453 -0
  56. package/lib/video/ops.js +30 -5
  57. package/lib/video/paralelo.js +50 -0
  58. package/lib/video/pedido.js +191 -0
  59. package/lib/video/rastrear.js +95 -0
  60. package/lib/video/rastro.js +276 -0
  61. package/lib/video/render.js +67 -14
  62. package/lib/video/texto.js +6 -4
  63. package/lib/video/timeline.js +9 -1
  64. package/package.json +2 -2
  65. package/skills/escrever-em-portugues/SKILL.md +3 -0
  66. package/skills/interface-revisada/SKILL.md +7 -1
  67. package/skills/motion-de-primeira/SKILL.md +5 -4
  68. package/skills/projeto-no-github/SKILL.md +6 -0
  69. package/skills/publicar-na-internet/SKILL.md +3 -0
  70. package/skills/site-com-motion/SKILL.md +2 -0
  71. package/studio/narrar.py +10 -7
  72. package/studio/rastrear.py +399 -0
  73. 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;
@@ -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
- const visivel = faixa.type === 'video' && !c.hidden && !faixa.hidden;
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
- linhas.push(`[${k}:v]${f.join(',')}[c${k}]`);
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
- linhas.push(`[${atual}]format=yuv420p[vout]`);
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);