primocode 10.0.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 +2 -2
- package/bin/primocode.js +69 -1
- package/lib/act.js +118 -19
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +1 -1
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/entrada.js +73 -9
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +23 -8
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +244 -36
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +3 -1
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit.js +7 -1
- package/lib/estudio/lancamento.js +28 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +53 -9
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/skills.js +36 -0
- package/lib/tools.js +22 -3
- 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/motion-de-primeira/SKILL.md +5 -4
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
|
@@ -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);
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/* fx3d/modelos.jsx — os objetos 3D que flutuam na mão, feitos de geometria.
|
|
2
|
+
*
|
|
3
|
+
* Nenhum arquivo de marca é baixado: as duas "logos" são DESENHADAS aqui,
|
|
4
|
+
* como formas genéricas que lembram o tema — um nó hexagonal de seis elos
|
|
5
|
+
* vazados (o "assistente verde") e um sol de raios irregulares (o
|
|
6
|
+
* "assistente coral") — extrudadas com chanfro sobre uma placa arredondada,
|
|
7
|
+
* como ícone de app. O cérebro é procedural: dois hemisférios de esfera
|
|
8
|
+
* deformada, com os sulcos feitos de ruído.
|
|
9
|
+
*
|
|
10
|
+
* Todo modelo cabe numa caixa de ~1 unidade de lado, centrado na origem. */
|
|
11
|
+
import React, { useMemo } from 'react';
|
|
12
|
+
import * as THREE from 'three';
|
|
13
|
+
|
|
14
|
+
function placa(lado = 1.15, raio = 0.28) {
|
|
15
|
+
const s = new THREE.Shape();
|
|
16
|
+
const m = lado / 2;
|
|
17
|
+
s.moveTo(-m + raio, -m);
|
|
18
|
+
s.lineTo(m - raio, -m); s.quadraticCurveTo(m, -m, m, -m + raio);
|
|
19
|
+
s.lineTo(m, m - raio); s.quadraticCurveTo(m, m, m - raio, m);
|
|
20
|
+
s.lineTo(-m + raio, m); s.quadraticCurveTo(-m, m, -m, m - raio);
|
|
21
|
+
s.lineTo(-m, -m + raio); s.quadraticCurveTo(-m, -m, -m + raio, -m);
|
|
22
|
+
return s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EXT = (prof, chanfro = 0.035) => ({ depth: prof, bevelEnabled: true, bevelThickness: chanfro, bevelSize: chanfro, bevelSegments: 6, curveSegments: 36 });
|
|
26
|
+
|
|
27
|
+
function Placa({ cor }) {
|
|
28
|
+
const g = useMemo(() => { const x = new THREE.ExtrudeGeometry(placa(), EXT(0.16, 0.05)); x.translate(0, 0, -0.2); return x; }, []);
|
|
29
|
+
// Fosca o bastante para a cor aparecer: muito verniz e o reflexo da sala lava a placa para branco.
|
|
30
|
+
return (
|
|
31
|
+
<mesh geometry={g}>
|
|
32
|
+
<meshPhysicalMaterial color={cor} roughness={0.5} metalness={0} clearcoat={0.6} clearcoatRoughness={0.35} />
|
|
33
|
+
</mesh>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Um elo: a cápsula VAZADA (contorno grosso). Seis delas, giradas de 60° em
|
|
38
|
+
volta do centro e inclinadas, formam o nó hexagonal. */
|
|
39
|
+
function elo(comp = 0.62, larg = 0.24, esp = 0.065) {
|
|
40
|
+
const capsula = (c, l) => {
|
|
41
|
+
const s = new THREE.Path();
|
|
42
|
+
const r = l / 2, m = c / 2 - r;
|
|
43
|
+
s.moveTo(-m, -r); s.lineTo(m, -r);
|
|
44
|
+
s.absarc(m, 0, r, -Math.PI / 2, Math.PI / 2, false);
|
|
45
|
+
s.lineTo(-m, r);
|
|
46
|
+
s.absarc(-m, 0, r, Math.PI / 2, (3 * Math.PI) / 2, false);
|
|
47
|
+
return s;
|
|
48
|
+
};
|
|
49
|
+
const fora = new THREE.Shape(capsula(comp, larg).getPoints(48));
|
|
50
|
+
fora.holes.push(new THREE.Path(capsula(comp - 2 * esp, larg - 2 * esp).getPoints(48).reverse()));
|
|
51
|
+
return fora;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function NoHexagonal() {
|
|
55
|
+
const g = useMemo(() => new THREE.ExtrudeGeometry(elo(), EXT(0.07, 0.018)), []);
|
|
56
|
+
return (
|
|
57
|
+
<group scale={[0.92, 0.92, 0.92]}>
|
|
58
|
+
{Array.from({ length: 6 }, (_, k) => {
|
|
59
|
+
const a = (k * Math.PI) / 3;
|
|
60
|
+
return (
|
|
61
|
+
<group key={k} rotation={[0, 0, a]}>
|
|
62
|
+
<mesh geometry={g} position={[0.04, 0.2, 0.02 + (k % 2) * 0.012]} rotation={[0, 0, Math.PI / 3.2]}>
|
|
63
|
+
<meshPhysicalMaterial color="#ffffff" roughness={0.18} metalness={0.1} clearcoat={1} clearcoatRoughness={0.08} />
|
|
64
|
+
</mesh>
|
|
65
|
+
</group>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</group>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* O sol de raios: onze raios de comprimento e largura um pouco diferentes
|
|
73
|
+
(a irregularidade é o que faz parecer desenhado à mão, não engrenagem). */
|
|
74
|
+
function raio(comp, base, ponta) {
|
|
75
|
+
const s = new THREE.Shape();
|
|
76
|
+
s.moveTo(-base / 2, 0);
|
|
77
|
+
s.lineTo(-ponta / 2, comp - ponta / 2);
|
|
78
|
+
s.absarc(0, comp - ponta / 2, ponta / 2, Math.PI, 0, true);
|
|
79
|
+
s.lineTo(base / 2, 0);
|
|
80
|
+
s.lineTo(-base / 2, 0);
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function Sol({ cor }) {
|
|
85
|
+
const raios = useMemo(() => {
|
|
86
|
+
const comps = [0.47, 0.42, 0.5, 0.44, 0.48, 0.4, 0.49, 0.43, 0.46, 0.41, 0.5];
|
|
87
|
+
return comps.map((c, k) => ({ g: new THREE.ExtrudeGeometry(raio(c, 0.07, 0.1 + (k % 3) * 0.012), EXT(0.08, 0.02)), a: (k / comps.length) * Math.PI * 2 + 0.12 * Math.sin(k * 2.3) }));
|
|
88
|
+
}, []);
|
|
89
|
+
return (
|
|
90
|
+
<group position={[0, 0, 0.02]}>
|
|
91
|
+
{raios.map((r, k) => (
|
|
92
|
+
<mesh key={k} geometry={r.g} rotation={[0, 0, r.a]}>
|
|
93
|
+
<meshPhysicalMaterial color={cor} roughness={0.35} metalness={0.05} clearcoat={0.8} clearcoatRoughness={0.2} />
|
|
94
|
+
</mesh>
|
|
95
|
+
))}
|
|
96
|
+
<mesh position={[0, 0, 0.05]}><cylinderGeometry args={[0.09, 0.09, 0.1, 48]} /><meshPhysicalMaterial color={cor} roughness={0.35} clearcoat={0.8} /></mesh>
|
|
97
|
+
</group>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ── o cérebro ────────────────────────────────────────────────────────────
|
|
102
|
+
* Esfera → elipsoide → giros. O giro é RUÍDO DE CRISTA (1 − |simplex|, com o
|
|
103
|
+
* espaço torcido por outro ruído): dá as linhas sinuosas, contínuas e de
|
|
104
|
+
* largura parecida que o córtex tem — seno sobre seno dava papel amassado.
|
|
105
|
+
* O lado de dentro de cada hemisfério é achatado e a fenda fica no meio. */
|
|
106
|
+
const GRAD = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1]];
|
|
107
|
+
const PERM = (() => {
|
|
108
|
+
// Permutação fixa (semente constante): o cérebro sai igual em todo render.
|
|
109
|
+
const p = Array.from({ length: 256 }, (_, i) => i);
|
|
110
|
+
let x = 1234567;
|
|
111
|
+
for (let i = 255; i > 0; i--) { x = (x * 1103515245 + 12345) & 0x7fffffff; const j = x % (i + 1); [p[i], p[j]] = [p[j], p[i]]; }
|
|
112
|
+
return [...p, ...p];
|
|
113
|
+
})();
|
|
114
|
+
function simplex(xin, yin, zin) {
|
|
115
|
+
const F3 = 1 / 3, G3 = 1 / 6;
|
|
116
|
+
const s = (xin + yin + zin) * F3;
|
|
117
|
+
const i = Math.floor(xin + s), j = Math.floor(yin + s), k = Math.floor(zin + s);
|
|
118
|
+
const t = (i + j + k) * G3;
|
|
119
|
+
const x0 = xin - (i - t), y0 = yin - (j - t), z0 = zin - (k - t);
|
|
120
|
+
let i1, j1, k1, i2, j2, k2;
|
|
121
|
+
if (x0 >= y0) {
|
|
122
|
+
if (y0 >= z0) { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0; } else if (x0 >= z0) { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1; } else { i1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1; }
|
|
123
|
+
} else if (y0 < z0) { i1 = 0; j1 = 0; k1 = 1; i2 = 0; j2 = 1; k2 = 1; } else if (x0 < z0) { i1 = 0; j1 = 1; k1 = 0; i2 = 0; j2 = 1; k2 = 1; } else { i1 = 0; j1 = 1; k1 = 0; i2 = 1; j2 = 1; k2 = 0; }
|
|
124
|
+
const cantos = [[x0, y0, z0, 0, 0, 0], [x0 - i1 + G3, y0 - j1 + G3, z0 - k1 + G3, i1, j1, k1],
|
|
125
|
+
[x0 - i2 + 2 * G3, y0 - j2 + 2 * G3, z0 - k2 + 2 * G3, i2, j2, k2], [x0 - 1 + 3 * G3, y0 - 1 + 3 * G3, z0 - 1 + 3 * G3, 1, 1, 1]];
|
|
126
|
+
let n = 0;
|
|
127
|
+
const ii = i & 255, jj = j & 255, kk = k & 255;
|
|
128
|
+
for (const [x, y, z, a, b, c] of cantos) {
|
|
129
|
+
let tt = 0.6 - x * x - y * y - z * z;
|
|
130
|
+
if (tt < 0) continue;
|
|
131
|
+
const g = GRAD[PERM[ii + a + PERM[jj + b + PERM[kk + c]]] % 12];
|
|
132
|
+
tt *= tt;
|
|
133
|
+
n += tt * tt * (g[0] * x + g[1] * y + g[2] * z);
|
|
134
|
+
}
|
|
135
|
+
return 32 * n;
|
|
136
|
+
}
|
|
137
|
+
/* 0 no fundo do sulco, 1 no alto do giro. O sulco é onde o ruído cruza o
|
|
138
|
+
zero — curvas fechadas e sinuosas; o giro é o resto, largo e redondo. */
|
|
139
|
+
const liso = (a, b, x) => { const t = Math.max(0, Math.min(1, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
|
|
140
|
+
function sulcos(x, y, z) {
|
|
141
|
+
const w = 0.25 * simplex(x * 1.3 + 7, y * 1.3, z * 1.3);
|
|
142
|
+
const n1 = simplex(x * 2.6 + w, y * 2.6 - w, z * 2.6 + w);
|
|
143
|
+
const n2 = simplex(x * 5.2 - w + 3, y * 5.2 + w, z * 5.2);
|
|
144
|
+
return Math.min(liso(0.0, 0.3, Math.abs(n1)), 0.35 + 0.65 * liso(0.0, 0.22, Math.abs(n2)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Cor de vértice é linear no three: os valores parecem escuros, e saem rosados na tela.
|
|
148
|
+
const ROSA_FUNDO = new THREE.Color('#3a0816');
|
|
149
|
+
const ROSA_TOPO = new THREE.Color('#d65a7c');
|
|
150
|
+
|
|
151
|
+
/* Cor por vértice: o fundo do sulco escurece (a "sombra" que a luz sozinha
|
|
152
|
+
não faz numa malha tão fina) e a crista do giro clareia. */
|
|
153
|
+
function pintar(g, alturas) {
|
|
154
|
+
const cores = new Float32Array(alturas.length * 3);
|
|
155
|
+
const c = new THREE.Color();
|
|
156
|
+
for (let i = 0; i < alturas.length; i++) {
|
|
157
|
+
c.copy(ROSA_FUNDO).lerp(ROSA_TOPO, 0.25 + 0.75 * alturas[i]);
|
|
158
|
+
cores.set([c.r, c.g, c.b], i * 3);
|
|
159
|
+
}
|
|
160
|
+
g.setAttribute('color', new THREE.BufferAttribute(cores, 3));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function hemisferio(lado) {
|
|
164
|
+
const g = new THREE.SphereGeometry(1, 220, 170);
|
|
165
|
+
const p = g.attributes.position;
|
|
166
|
+
const v = new THREE.Vector3();
|
|
167
|
+
const alturas = new Float32Array(p.count);
|
|
168
|
+
for (let i = 0; i < p.count; i++) {
|
|
169
|
+
v.fromBufferAttribute(p, i);
|
|
170
|
+
const s = sulcos(v.x + lado * 0.3, v.y, v.z);
|
|
171
|
+
alturas[i] = s;
|
|
172
|
+
v.multiplyScalar(0.93 + 0.07 * s);
|
|
173
|
+
v.set(v.x * 0.46, v.y * 0.44, v.z * 0.74);
|
|
174
|
+
if (v.x * lado < 0) v.x *= 0.3; // achata o lado de dentro (contínuo: sem degrau)
|
|
175
|
+
if (v.y < 0) v.y *= 0.72; // a base mais plana
|
|
176
|
+
p.setXYZ(i, v.x + lado * 0.16, v.y, v.z);
|
|
177
|
+
}
|
|
178
|
+
g.computeVertexNormals();
|
|
179
|
+
pintar(g, alturas);
|
|
180
|
+
return g;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function Cerebro({ cor }) {
|
|
184
|
+
const [e, d] = useMemo(() => [hemisferio(-1), hemisferio(1)], []);
|
|
185
|
+
const mat = <meshPhysicalMaterial vertexColors color={cor || '#ffffff'} roughness={0.5} metalness={0} clearcoat={0.35} clearcoatRoughness={0.4} sheen={0.3} sheenColor="#ffc2d2" />;
|
|
186
|
+
/* Visto de cima e de frente (o topo inclinado para a câmera): é assim que
|
|
187
|
+
se reconhece um cérebro — os dois hemisférios e a fenda no meio. */
|
|
188
|
+
return (
|
|
189
|
+
<group rotation={[0.95, 0, 0]} scale={[1.05, 1.05, 1.05]}>
|
|
190
|
+
<mesh geometry={e}>{mat}</mesh>
|
|
191
|
+
<mesh geometry={d}>{mat}</mesh>
|
|
192
|
+
</group>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Texto curto numa placa: desenhado num canvas (sem fonte 3D para baixar). */
|
|
197
|
+
function PlacaTexto({ texto = '', cor = '#111827' }) {
|
|
198
|
+
const tex = useMemo(() => {
|
|
199
|
+
const cv = document.createElement('canvas');
|
|
200
|
+
cv.width = 512; cv.height = 512;
|
|
201
|
+
const ctx = cv.getContext('2d');
|
|
202
|
+
ctx.fillStyle = '#ffffff';
|
|
203
|
+
const t = String(texto).slice(0, 14).toUpperCase();
|
|
204
|
+
let tam = 200;
|
|
205
|
+
ctx.font = `900 ${tam}px system-ui, -apple-system, Helvetica, Arial`;
|
|
206
|
+
while (ctx.measureText(t).width > 440 && tam > 40) { tam -= 8; ctx.font = `900 ${tam}px system-ui, -apple-system, Helvetica, Arial`; }
|
|
207
|
+
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
208
|
+
ctx.fillText(t, 256, 270);
|
|
209
|
+
const x = new THREE.CanvasTexture(cv);
|
|
210
|
+
x.colorSpace = THREE.SRGBColorSpace;
|
|
211
|
+
return x;
|
|
212
|
+
}, [texto]);
|
|
213
|
+
return (
|
|
214
|
+
<group>
|
|
215
|
+
<Placa cor={cor} />
|
|
216
|
+
<mesh position={[0, 0, 0.05]}><planeGeometry args={[1.0, 1.0]} /><meshBasicMaterial map={tex} transparent toneMapped={false} /></mesh>
|
|
217
|
+
</group>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* O navegador: o anel de três arcos (vermelho, amarelo, verde) em volta da
|
|
222
|
+
pupila azul, extrudado — lembra o tema sem copiar o arquivo da marca. */
|
|
223
|
+
function Navegador() {
|
|
224
|
+
const arcos = useMemo(() => [0, 1, 2].map((k) => {
|
|
225
|
+
const s = new THREE.Shape();
|
|
226
|
+
const a0 = (k * 2 * Math.PI) / 3 + Math.PI / 2;
|
|
227
|
+
const a1 = a0 + (2 * Math.PI) / 3 - 0.06;
|
|
228
|
+
s.absarc(0, 0, 0.44, a0, a1, false);
|
|
229
|
+
s.absarc(0, 0, 0.2, a1, a0, true);
|
|
230
|
+
return new THREE.ExtrudeGeometry(s, EXT(0.09, 0.02));
|
|
231
|
+
}), []);
|
|
232
|
+
const cores = ['#db4437', '#f4b400', '#0f9d58'];
|
|
233
|
+
return (
|
|
234
|
+
<group position={[0, 0, -0.02]}>
|
|
235
|
+
{arcos.map((g, k) => (<mesh key={k} geometry={g}><meshPhysicalMaterial color={cores[k]} roughness={0.45} clearcoat={0.5} /></mesh>))}
|
|
236
|
+
<mesh position={[0, 0, 0.06]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.15, 0.15, 0.12, 48]} /><meshPhysicalMaterial color="#ffffff" roughness={0.4} /></mesh>
|
|
237
|
+
<mesh position={[0, 0, 0.1]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.11, 0.11, 0.12, 48]} /><meshPhysicalMaterial color="#4285f4" roughness={0.35} clearcoat={0.6} /></mesh>
|
|
238
|
+
</group>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* A pasta na nuvem: o triângulo de três faixas (verde, azul, amarelo). */
|
|
243
|
+
function Triangulo() {
|
|
244
|
+
const faixas = useMemo(() => {
|
|
245
|
+
const L = 0.5, h = L * Math.sqrt(3);
|
|
246
|
+
const v = [[0, h * 0.62], [-L, -h * 0.38], [L, -h * 0.38]];
|
|
247
|
+
return [0, 1, 2].map((k) => {
|
|
248
|
+
const a = v[k], b = v[(k + 1) % 3];
|
|
249
|
+
const nx = (b[1] - a[1]), ny = -(b[0] - a[0]);
|
|
250
|
+
const n = Math.hypot(nx, ny);
|
|
251
|
+
const e = 0.2;
|
|
252
|
+
const s = new THREE.Shape();
|
|
253
|
+
s.moveTo(a[0], a[1]); s.lineTo(b[0], b[1]);
|
|
254
|
+
s.lineTo(b[0] - (nx / n) * e * 0.9 + (a[0] - b[0]) * 0.18, b[1] - (ny / n) * e * 0.9 + (a[1] - b[1]) * 0.18);
|
|
255
|
+
s.lineTo(a[0] - (nx / n) * e * 0.9 + (b[0] - a[0]) * 0.18, a[1] - (ny / n) * e * 0.9 + (b[1] - a[1]) * 0.18);
|
|
256
|
+
s.closePath();
|
|
257
|
+
return new THREE.ExtrudeGeometry(s, EXT(0.09, 0.02));
|
|
258
|
+
});
|
|
259
|
+
}, []);
|
|
260
|
+
const cores = ['#0f9d58', '#4285f4', '#f4b400'];
|
|
261
|
+
return (
|
|
262
|
+
<group position={[0, 0.02, -0.02]} scale={[0.9, 0.9, 0.9]}>
|
|
263
|
+
{faixas.map((g, k) => (<mesh key={k} geometry={g}><meshPhysicalMaterial color={cores[k]} roughness={0.45} clearcoat={0.5} side={THREE.DoubleSide} /></mesh>))}
|
|
264
|
+
</group>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* O foguete: corpo em revolução (o perfil girado), janela, três aletas e a
|
|
269
|
+
chama que pulsa — de geometria, como o resto. */
|
|
270
|
+
function Foguete() {
|
|
271
|
+
const corpo = useMemo(() => {
|
|
272
|
+
const p = [[0, -0.42], [0.15, -0.4], [0.17, -0.1], [0.16, 0.12], [0.12, 0.28], [0.06, 0.4], [0, 0.46]].map(([x, y]) => new THREE.Vector2(x, y));
|
|
273
|
+
return new THREE.LatheGeometry(p, 48);
|
|
274
|
+
}, []);
|
|
275
|
+
const aleta = useMemo(() => {
|
|
276
|
+
const s = new THREE.Shape();
|
|
277
|
+
s.moveTo(0, 0); s.lineTo(0.16, -0.12); s.lineTo(0.16, -0.26); s.lineTo(0, -0.18); s.closePath();
|
|
278
|
+
return new THREE.ExtrudeGeometry(s, EXT(0.03, 0.01));
|
|
279
|
+
}, []);
|
|
280
|
+
return (
|
|
281
|
+
<group rotation={[0, 0, -0.35]} scale={[1.1, 1.1, 1.1]}>
|
|
282
|
+
<mesh geometry={corpo}><meshPhysicalMaterial color="#f3f4f6" roughness={0.3} metalness={0.2} clearcoat={0.8} /></mesh>
|
|
283
|
+
<mesh position={[0, 0.34, 0]}><sphereGeometry args={[0.075, 32, 32]} /><meshPhysicalMaterial color="#e5484d" roughness={0.35} clearcoat={0.6} /></mesh>
|
|
284
|
+
<mesh position={[0, 0.12, 0.15]}><sphereGeometry args={[0.055, 32, 32]} /><meshPhysicalMaterial color="#3b82f6" roughness={0.1} metalness={0.3} clearcoat={1} /></mesh>
|
|
285
|
+
{[0, 1, 2].map((k) => (
|
|
286
|
+
<group key={k} rotation={[0, (k * 2 * Math.PI) / 3, 0]}>
|
|
287
|
+
<mesh geometry={aleta} position={[0.14, -0.14, -0.015]}><meshPhysicalMaterial color="#e5484d" roughness={0.4} clearcoat={0.5} /></mesh>
|
|
288
|
+
</group>
|
|
289
|
+
))}
|
|
290
|
+
<mesh position={[0, -0.52, 0]} rotation={[Math.PI, 0, 0]}><coneGeometry args={[0.1, 0.24, 32]} /><meshBasicMaterial color="#ffb020" toneMapped={false} /></mesh>
|
|
291
|
+
<mesh position={[0, -0.49, 0]} rotation={[Math.PI, 0, 0]}><coneGeometry args={[0.06, 0.14, 32]} /><meshBasicMaterial color="#fff3c4" toneMapped={false} /></mesh>
|
|
292
|
+
</group>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function Modelo({ modelo, cor, texto }) {
|
|
297
|
+
if (modelo === 'foguete') return <Foguete />;
|
|
298
|
+
if (modelo === 'chrome') return (<group><Placa cor={cor || '#ffffff'} /><Navegador /></group>);
|
|
299
|
+
if (modelo === 'drive') return (<group><Placa cor={cor || '#ffffff'} /><Triangulo /></group>);
|
|
300
|
+
if (modelo === 'chatgpt') return (<group><Placa cor={cor || '#0b8a6a'} /><NoHexagonal /></group>);
|
|
301
|
+
if (modelo === 'claude') return (<group><Placa cor={cor || '#f4efe6'} /><Sol cor="#cf5f3c" /></group>);
|
|
302
|
+
if (modelo === 'cerebro') return <Cerebro cor={cor} />;
|
|
303
|
+
return <PlacaTexto texto={texto || modelo} cor={cor || '#111827'} />;
|
|
304
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fx3d.js — as mídias GERADAS do editor: o momento 3D das camadas e a
|
|
3
|
+
* camada dos objetos que seguem as mãos. Cada uma é um asset da timeline
|
|
4
|
+
* (`asset.gerado`), renderizado no Remotion na primeira vez que o export ou
|
|
5
|
+
* o inspect precisa dele, e guardado pelo hash da receita.
|
|
6
|
+
*
|
|
7
|
+
* ── POR QUE O 3D SAI COMO UMA CAMADA, E O RESTO CONTINUA NO ffmpeg ───────
|
|
8
|
+
* O editor inteiro é um grafo de ffmpeg (filters.js): corte, ripple, cor,
|
|
9
|
+
* legenda. Levar tudo para o Remotion obrigaria a reescrever o editor e
|
|
10
|
+
* deixaria cada export lento como um render de navegador. O contrário é
|
|
11
|
+
* o robusto: só o que PRECISA de GPU (o three.js, a perspectiva das
|
|
12
|
+
* camadas) passa pelo Remotion, numa camada com canal alfa (ProRes 4444 —
|
|
13
|
+
* o ffmpeg lê o alfa dele sem decodificador especial, ao contrário do VP9),
|
|
14
|
+
* e entra no grafo como um clipe qualquer. O clipe "espelha" a fala na
|
|
15
|
+
* timeline, então um corte de silêncio corta a camada no mesmo quadro.
|
|
16
|
+
*
|
|
17
|
+
* A receita fica na timeline (pequena); as trilhas por quadro saem do
|
|
18
|
+
* rastro na hora do render — mudar a receita muda o hash e refaz só ela.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
const { spawn } = require('child_process');
|
|
28
|
+
const RM = require('../estudio/remotion.js');
|
|
29
|
+
|
|
30
|
+
const FONTES = path.join(__dirname, 'fx3d');
|
|
31
|
+
const ARQUIVOS = ['Root.jsx', 'Camadas.jsx', 'Objetos.jsx', 'modelos.jsx'];
|
|
32
|
+
|
|
33
|
+
const CONFIG = `import { Config } from '@remotion/cli/config';
|
|
34
|
+
// O three.js precisa de WebGL de verdade no Chromium sem tela.
|
|
35
|
+
Config.setChromiumOpenGlRenderer('angle');
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
function trabalhadores() { return Math.max(1, (os.cpus() || []).length - 1); }
|
|
39
|
+
|
|
40
|
+
/** A versão das fontes JSX: mudou o desenho, muda o hash, e o render refaz. */
|
|
41
|
+
let _versao = null;
|
|
42
|
+
function versao() {
|
|
43
|
+
if (_versao) return _versao;
|
|
44
|
+
return (_versao = hash(ARQUIVOS.map((a) => fs.readFileSync(path.join(FONTES, a), 'utf8')).join('\n')));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hash(obj) { return crypto.createHash('sha1').update(JSON.stringify(obj)).digest('hex').slice(0, 12); }
|
|
48
|
+
|
|
49
|
+
function escrever(arq, txt) {
|
|
50
|
+
fs.mkdirSync(path.dirname(arq), { recursive: true });
|
|
51
|
+
if (!fs.existsSync(arq) || fs.readFileSync(arq, 'utf8') !== txt) fs.writeFileSync(arq, txt);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rodar(cmd, args, { cwd, minutos = 30 } = {}) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const p = spawn(cmd, args, { cwd, shell: process.platform === 'win32' });
|
|
57
|
+
let erro = '';
|
|
58
|
+
p.stderr.on('data', (d) => { erro = (erro + d.toString()).slice(-3000); });
|
|
59
|
+
p.stdout.on('data', (d) => { erro = (erro + d.toString()).slice(-3000); });
|
|
60
|
+
const rel = setTimeout(() => { try { p.kill(); } catch {} }, minutos * 60000);
|
|
61
|
+
p.on('error', (e) => { clearTimeout(rel); resolve({ ok: false, error: e.message }); });
|
|
62
|
+
p.on('close', (code) => { clearTimeout(rel); resolve(code === 0 ? { ok: true } : { ok: false, error: `${cmd} saiu com ${code}: ${erro.slice(-700)}` }); });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** O projeto Remotion do editor, em <projeto>/cache/fx3d (um por projeto de vídeo). */
|
|
67
|
+
async function projeto(pasta) {
|
|
68
|
+
const raiz = path.join(pasta, 'cache', 'fx3d');
|
|
69
|
+
escrever(path.join(raiz, 'package.json'), RM.pkgJson('fx3d'));
|
|
70
|
+
escrever(path.join(raiz, 'remotion.config.ts'), CONFIG);
|
|
71
|
+
escrever(path.join(raiz, 'tsconfig.json'), RM.TSCONFIG);
|
|
72
|
+
for (const a of ARQUIVOS) escrever(path.join(raiz, 'src', a), fs.readFileSync(path.join(FONTES, a), 'utf8'));
|
|
73
|
+
fs.mkdirSync(path.join(raiz, 'public'), { recursive: true });
|
|
74
|
+
if (fs.existsSync(path.join(raiz, 'node_modules'))) return { ok: true, raiz };
|
|
75
|
+
const comp = RM.baseCompartilhada(raiz);
|
|
76
|
+
if (comp && !comp.pronta) {
|
|
77
|
+
// A mesma base do Estúdio (~/.primocode/studio/remotion-base): instala uma vez.
|
|
78
|
+
fs.mkdirSync(comp.base, { recursive: true });
|
|
79
|
+
fs.copyFileSync(path.join(raiz, 'package.json'), path.join(comp.base, 'package.json'));
|
|
80
|
+
const r = await rodar('npm', ['install', '--no-audit', '--no-fund'], { cwd: comp.base, minutos: 20 });
|
|
81
|
+
if (!r.ok) return r;
|
|
82
|
+
fs.writeFileSync(path.join(comp.base, 'deps.json'), comp.deps);
|
|
83
|
+
}
|
|
84
|
+
if (comp) {
|
|
85
|
+
try { fs.symlinkSync(path.join(comp.base, 'node_modules'), path.join(raiz, 'node_modules'), 'dir'); return { ok: true, raiz }; } catch { /* instala aqui */ }
|
|
86
|
+
}
|
|
87
|
+
const r = await rodar('npm', ['install', '--no-audit', '--no-fund'], { cwd: raiz, minutos: 20 });
|
|
88
|
+
return r.ok ? { ok: true, raiz } : r;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Renderiza uma composição. `alfa`: ProRes 4444 com transparência. */
|
|
92
|
+
async function renderizar(raiz, comp, props, saida, { alfa = false } = {}) {
|
|
93
|
+
const arqProps = path.join(raiz, `props-${hash(props)}.json`);
|
|
94
|
+
fs.writeFileSync(arqProps, JSON.stringify(props));
|
|
95
|
+
const tmp = saida.replace(/(\.\w+)$/, `.tmp$1`);
|
|
96
|
+
fs.mkdirSync(path.dirname(saida), { recursive: true });
|
|
97
|
+
const formato = alfa
|
|
98
|
+
? ['--codec=prores', '--prores-profile=4444', '--pixel-format=yuva444p10le', '--image-format=png']
|
|
99
|
+
: ['--codec=h264', '--crf=14', '--image-format=jpeg', '--jpeg-quality=95'];
|
|
100
|
+
const r = await rodar('npx', ['remotion', 'render', 'src/Root.jsx', comp, tmp, `--props=${arqProps}`,
|
|
101
|
+
`--concurrency=${trabalhadores()}`, ...formato, '--log=error', '--timeout=120000'], { cwd: raiz, minutos: 40 });
|
|
102
|
+
try { fs.unlinkSync(arqProps); } catch {}
|
|
103
|
+
if (!r.ok) return r;
|
|
104
|
+
fs.renameSync(tmp, saida);
|
|
105
|
+
return { ok: true, arquivo: saida };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { projeto, renderizar, hash, versao, trabalhadores, rodar, ARQUIVOS };
|