primocode 8.41.0 → 8.42.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 CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v8.41.0
1
+ # PrimoCode v8.42.0
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -11,7 +11,7 @@ do Claude ou do GPT escolhe isso na abertura, com as setinhas, e passa a usar a
11
11
  assinatura dele.
12
12
 
13
13
  ```
14
- ▐▛███▜▌ Primo Code v8.41.0
14
+ ▐▛███▜▌ Primo Code v8.42.0
15
15
  ▝▜█████▛▘ Bem-vindo de volta, Joel
16
16
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
17
17
  ```
@@ -50,6 +50,12 @@ crédito; a identidade de uma empresa — logo, cor, paleta, fontes e frase —
50
50
  arquivo, e os créditos saem prontos para a cena final. Recorte de fundo com
51
51
  `rembg`, quando instalado.
52
52
 
53
+ O quadro também se move: cada cena aceita uma **câmera** (`aproximar`,
54
+ `empurrar`, `pan-direita`, `balanco`…) e cada elemento uma **profundidade**, e
55
+ aí as camadas andam em velocidades diferentes — é o que separa motion de slide
56
+ animado. Junto com as chaves em 3D, as curvas com nome e o som por elemento,
57
+ dá para montar no navegador o que antes pedia After Effects.
58
+
53
59
  O motor não é gosto: vídeo curto sai pelo estúdio, em segundos, sem instalar
54
60
  nada; vídeo longo, canal alfa, ProRes ou fps alto viram projeto **Remotion** na
55
61
  sua pasta — e a resposta sempre diz por que escolheu.
package/lib/act.js CHANGED
@@ -460,6 +460,31 @@ async function runAct(userPrompt, opts) {
460
460
  if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
461
461
  }
462
462
  }
463
+ /* ── O GATILHO DO DIRETOR ────────────────────────────────────────────
464
+ *
465
+ * "A IA identifica que precisa criar vídeo, ativa o agente, esse agente
466
+ * faz a estrutura e manda pra IA, e depois ela só faz o vídeo."
467
+ *
468
+ * Deixar isso só escrito no catálogo não basta: com um pedido curto
469
+ * ("faz um vídeo do produto") o modelo pula direto para o studio_criar,
470
+ * porque criar é o que ele sabe fazer. Quem reconhece o pedido é uma
471
+ * regra (diretor.precisaDeProducao), e ela entra como ORDEM na conversa —
472
+ * não como conselho no meio de cinquenta ferramentas.
473
+ *
474
+ * Num subagente não entra: ele já está fazendo uma parte de um plano, e
475
+ * um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
476
+ if (!subagent && profundidade === 0) {
477
+ const producao = require('./diretor.js').precisaDeProducao(userPrompt);
478
+ if (producao.producao) {
479
+ messages.push({ role: 'system', content:
480
+ 'ESTE PEDIDO É DE PRODUÇÃO AUDIOVISUAL. Antes de criar qualquer peça, chame '
481
+ + `planejar_producao com o pedido do usuário (formato "${producao.formato}"). `
482
+ + 'Ele devolve a pauta — roteiro, narração falada, voz, câmera e a mídia a buscar — '
483
+ + 'e é ela que você produz, cena a cena. Não comece pelo studio_criar: o roteiro '
484
+ + 'escrito no meio do JSON da cena vira legenda de slide, que é o que se quer evitar.' });
485
+ }
486
+ }
487
+
463
488
  messages.push({ role: 'user', content: userPrompt });
464
489
 
465
490
  let finalText = '';
package/lib/diretor.js CHANGED
@@ -116,7 +116,7 @@ function ritmoPara(segundos) {
116
116
  // ── 3. o plano ───────────────────────────────────────────────────────────
117
117
 
118
118
  const CAMPOS_CENA = ['titulo', 'narracao', 'duracao', 'layout', 'fundo', 'transicao',
119
- 'elementos', 'midia', 'chaves', 'notas'];
119
+ 'elementos', 'midia', 'camera', 'chaves', 'notas'];
120
120
 
121
121
  /**
122
122
  * Normaliza o que o diretor devolveu e aponta o que ficou faltando.
@@ -206,6 +206,7 @@ Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
206
206
  "fundo": "malha | aurora | raios | ondas | nevoa | vinheta, uma cor, ou {\\"imagem\\":\\"<da lista midia>\\"}",
207
207
  "transicao": "corte | fade | deslizar | subir | zoom | limpar | persiana | flash",
208
208
  "midia": [ { "busca": "o que procurar", "tipo": "imagem | logo", "papel": "fundo | destaque | selo", "semFundo": false } ],
209
+ "camera": "aproximar | afastar | pan-direita | pan-esquerda | subir | descer | empurrar | balanco | nenhuma",
209
210
  "chaves": "descreva em uma frase o movimento principal desta cena (o que entra, o que atravessa, o que cresce)",
210
211
  "notas": "o que o produtor precisa saber e não está acima"
211
212
  }
@@ -219,7 +220,8 @@ REGRAS QUE SEPARAM UM VÍDEO BOM DE UM GENÉRICO:
219
220
  4. NÚMERO É CONCRETO. Nada de "muitos clientes": ou tem o número, ou a frase muda.
220
221
  5. RITMO VARIADO. Cenas de durações diferentes e transições diferentes. Tudo em fade parece slide.
221
222
  6. MÍDIA COM PROPÓSITO. Só peça imagem ou logo quando ela PROVA algo. Diga o termo de busca de verdade ("painel solar em telhado residencial", não "imagem legal").
222
- 7. MOVIMENTO. Em cada cena diga em "chaves" o movimento principal — é ele que tira a cara de apresentação parada.`;
223
+ 7. MOVIMENTO. Em cada cena diga em "chaves" o movimento principal — é ele que tira a cara de apresentação parada.
224
+ 8. CÂMERA COM PARCIMÔNIA. Movimento de quadro em UMA cena a cada duas ou três ("camera": "nenhuma" nas outras): o que dá ritmo é alternar cena parada e cena que se move. Em cena com foto de fundo, prefira "aproximar" ou "empurrar".`;
223
225
  };
224
226
 
225
227
  const REVISAO = (defeitos) => `O plano voltou com defeitos de FORMA. Corrija SÓ o que está listado e devolva o JSON inteiro de novo, no mesmo formato:
@@ -288,6 +290,7 @@ function pauta(plano) {
288
290
  linhas.push(`CENA ${i + 1} — ${c.duracao}s · layout ${c.layout || 'titulo'} · entra em ${c.transicao}`);
289
291
  if (c.titulo) linhas.push(` na tela: ${c.titulo}`);
290
292
  linhas.push(` narração: ${c.narracao}`);
293
+ if (c.camera && c.camera !== 'nenhuma') linhas.push(` câmera: ${c.camera}`);
291
294
  if (c.chaves) linhas.push(` movimento: ${c.chaves}`);
292
295
  (c.midia || []).forEach((m) => linhas.push(
293
296
  ` mídia: ${m.tipo || 'imagem'} "${m.busca}" como ${m.papel || 'destaque'}${m.semFundo ? ' (sem fundo)' : ''}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.41.0",
3
+ "version": "8.42.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/fala-repetida.test.js && node test/trilha.test.js && node test/anuncio.test.js && node test/pipeline.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/pareamento.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/icone.test.js && node test/exportar.test.js && node test/ate-o-fim.test.js && node test/motion.test.js && node test/audio-marca.test.js && node test/diretor.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/fala-repetida.test.js && node test/trilha.test.js && node test/anuncio.test.js && node test/pipeline.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/pareamento.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/icone.test.js && node test/exportar.test.js && node test/ate-o-fim.test.js && node test/motion.test.js && node test/audio-marca.test.js && node test/diretor.test.js && node test/camera.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -0,0 +1,57 @@
1
+ {
2
+ "id": "corte-teste-de-camera-fb823",
3
+ "ferramenta": "corte",
4
+ "titulo": "Teste de câmera",
5
+ "doc": {
6
+ "tema": "meia-noite",
7
+ "largura": 1920,
8
+ "altura": 1080,
9
+ "acabamento": "grao",
10
+ "cenas": [
11
+ {
12
+ "layout": "capa",
13
+ "duracao": 4,
14
+ "fundo": "aurora",
15
+ "transicao": "fade",
16
+ "camera": "empurrar",
17
+ "elementos": [
18
+ {
19
+ "tipo": "texto",
20
+ "papel": "chapeu",
21
+ "texto": "Câmera",
22
+ "paralaxe": 1.3
23
+ },
24
+ {
25
+ "tipo": "texto",
26
+ "papel": "titulo",
27
+ "texto": "O quadro se move"
28
+ },
29
+ {
30
+ "tipo": "texto",
31
+ "papel": "apoio",
32
+ "texto": "e as camadas andam em velocidades diferentes.",
33
+ "paralaxe": 0.5
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ "layout": "numeros",
39
+ "duracao": 3.5,
40
+ "fundo": "malha",
41
+ "transicao": "zoom",
42
+ "camera": "aproximar",
43
+ "elementos": [
44
+ {
45
+ "tipo": "numeros",
46
+ "itens": [
47
+ "+38% | em 12 meses"
48
+ ],
49
+ "contar": true,
50
+ "som": "ding"
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+ },
56
+ "atualizado": "2026-09-11T18:39:36+00:00"
57
+ }
@@ -796,6 +796,48 @@ Duas chaves já bastam. Quem tem `chaves` ignora `anim` — as duas disputariam
796
796
  mesmo movimento, e a chave é mais específica. Use em **um ou dois** elementos
797
797
  por vídeo: tudo se mexendo ao mesmo tempo cansa e esconde a mensagem.
798
798
 
799
+ ## Câmera da cena — o quadro também se move
800
+
801
+ `chaves` move um elemento. `camera` move o **quadro inteiro**: é o que separa
802
+ motion de slide animado. Na cena:
803
+
804
+ ```json
805
+ { "layout": "capa", "duracao": 4, "camera": "aproximar" }
806
+ ```
807
+
808
+ Movimentos com nome: `aproximar` · `afastar` · `pan-direita` · `pan-esquerda` ·
809
+ `subir` · `descer` · `empurrar` (zoom com um grau de giro — a mão humana) ·
810
+ `balanco`.
811
+
812
+ Para um movimento seu, as mesmas chaves dos elementos:
813
+
814
+ ```json
815
+ "camera": { "chaves": [ { "t": 0, "escala": 1.15, "x": -40 },
816
+ { "t": 1, "escala": 1, "x": 0 } ], "suavizar": "saida" }
817
+ ```
818
+
819
+ O **fundo não anda junto** — é assim de propósito: o quadro passa sobre o
820
+ fundo, e é daí que vem a sensação de profundidade sem custo nenhum.
821
+
822
+ ### Paralaxe — as camadas andam em velocidades diferentes
823
+
824
+ No elemento, `paralaxe` diz o quanto ele acompanha a câmera: `1` é o padrão
825
+ (anda com o quadro), acima de 1 vem à frente e anda **mais**, abaixo de 1 fica
826
+ ao fundo e anda **menos**.
827
+
828
+ ```json
829
+ { "tipo": "imagem", "src": "/midia/cidade.jpg", "paralaxe": 0.4 }
830
+ { "tipo": "texto", "papel": "titulo", "texto": "São Paulo", "paralaxe": 1.25 }
831
+ ```
832
+
833
+ Duas ou três camadas bastam: fundo em 0,3–0,5, conteúdo em 1, o que está na
834
+ frente em 1,2–1,4. Um elemento que já tem `chaves` próprias e está no fluxo
835
+ ignora a paralaxe — a animação dele é mais específica, e embrulhar mudaria o
836
+ espaçamento da cena.
837
+
838
+ **Um movimento de câmera por cena, e não em todas.** Câmera em tudo embrulha o
839
+ olho; o que dá ritmo é alternar cena parada e cena com movimento.
840
+
799
841
  ## Acabamento
800
842
 
801
843
  Textura por cima da cena inteira — é o que tira a cara de template:
@@ -0,0 +1,57 @@
1
+ [studio] 2026-09-11T18:38:32+00:00 — iniciando na porta 7777
2
+
3
+ ██ PrimoCode Studio
4
+ ────────────────────────────────────────────────────────
5
+ Estúdio http://localhost:7777
6
+ Manual da IA http://localhost:7777/api/manual
7
+ Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
8
+ ────────────────────────────────────────────────────────
9
+ Ctrl+C para parar
10
+
11
+ GET /api/ferramentas
12
+ GET /api/ferramentas
13
+ GET /api/arquivos
14
+ GET /api/ferramentas
15
+ GET /api/arquivos
16
+ GET /api/ferramentas
17
+ GET /api/arquivos
18
+ POST /api/arquivos
19
+ GET /api/arquivos/corte-teste-de-camera-fb823
20
+ GET /api/voz/status
21
+ GET /api/voz/status
22
+ GET /api/arquivos/corte-teste-de-camera-fb823
23
+ GET /api/voz/status
24
+ GET /api/arquivos/corte-teste-de-camera-fb823
25
+ GET /api/voz/status
26
+ [studio] 2026-09-11T18:40:34+00:00 — iniciando na porta 7777
27
+
28
+ ██ PrimoCode Studio
29
+ ────────────────────────────────────────────────────────
30
+ Estúdio http://localhost:7777
31
+ Manual da IA http://localhost:7777/api/manual
32
+ Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
33
+ ────────────────────────────────────────────────────────
34
+ Ctrl+C para parar
35
+
36
+ GET /api/arquivos/corte-teste-de-camera-fb823
37
+ GET /api/voz/status
38
+ GET /api/voz/status
39
+ GET /api/voz/status
40
+ GET /api/voz/status
41
+ GET /api/arquivos/corte-teste-de-camera-fb823
42
+ GET /api/voz/status
43
+ GET /api/voz/status
44
+ GET /api/arquivos/corte-teste-de-camera-fb823
45
+ GET /api/voz/status
46
+ GET /api/voz/status
47
+ GET /api/arquivos/corte-teste-de-camera-fb823
48
+ GET /api/voz/status
49
+ GET /api/voz/status
50
+ GET /api/arquivos/corte-teste-de-camera-fb823
51
+ GET /api/voz/status
52
+ GET /api/voz/status
53
+ GET /api/arquivos/corte-teste-de-camera-fb823
54
+ GET /api/voz/status
55
+ GET /api/ferramentas
56
+ GET /api/versao
57
+ POST /api/sair
@@ -987,3 +987,181 @@ body {
987
987
  font: 400 11.5px/1.2 var(--sans);
988
988
  color: var(--fraco);
989
989
  }
990
+
991
+ /* ==========================================================================
992
+ REDESENHO DA CAPA — o estúdio se apresentando
993
+ --------------------------------------------------------------------------
994
+ O que estava aqui era uma página de lista: título, cartões, lista de
995
+ arquivos. Funcionava e não dizia nada — quem abria pela primeira vez não
996
+ descobria que o vídeo sai com narração, câmera e keyframes, nem que dá para
997
+ pedir tudo por texto ao agente.
998
+
999
+ Este bloco vem DEPOIS do núcleo de propósito: ele acrescenta a capa, os
1000
+ poderes e o acabamento dos cartões sem reescrever a primitiva (.btn, .campo,
1001
+ .topo) que o editor usa — mudar aquilo mexeria em cinco telas de uma vez.
1002
+ ========================================================================== */
1003
+
1004
+ .home { max-width: 1140px; padding-top: 34px; }
1005
+
1006
+ .busca-topo { width: 220px; background: var(--sup2); }
1007
+ .busca-topo::placeholder { color: var(--fraco2); }
1008
+ @media (max-width: 720px) { .busca-topo { display: none; } }
1009
+
1010
+ /* ------------------------------------------------------------------ capa -- */
1011
+
1012
+ .capa {
1013
+ position: relative;
1014
+ display: grid;
1015
+ grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
1016
+ gap: 30px;
1017
+ align-items: center;
1018
+ padding: 46px 40px;
1019
+ margin-bottom: 54px;
1020
+ border: 1px solid var(--linha);
1021
+ border-radius: 22px;
1022
+ background: linear-gradient(180deg, var(--sup) 0%, #101014 100%);
1023
+ overflow: hidden;
1024
+ }
1025
+ /* A luz é a única coisa que se move na capa, e devagar: movimento na página
1026
+ inteira briga com o vídeo que a pessoa veio fazer. */
1027
+ .capa-luz {
1028
+ position: absolute;
1029
+ inset: -40% -10% auto -20%;
1030
+ height: 520px;
1031
+ background:
1032
+ radial-gradient(420px 240px at 20% 40%, rgba(255, 90, 54, .22), transparent 70%),
1033
+ radial-gradient(360px 220px at 70% 30%, rgba(139, 92, 246, .18), transparent 72%);
1034
+ filter: blur(6px);
1035
+ animation: capaLuz 18s ease-in-out infinite alternate;
1036
+ pointer-events: none;
1037
+ }
1038
+ @keyframes capaLuz { from { transform: translate3d(-3%, 0, 0); } to { transform: translate3d(6%, 3%, 0); } }
1039
+
1040
+ .capa-texto { position: relative; z-index: 1; }
1041
+ .capa h1 {
1042
+ font-size: 40px;
1043
+ line-height: 1.12;
1044
+ font-weight: 680;
1045
+ letter-spacing: -.04em;
1046
+ margin: 14px 0 12px;
1047
+ }
1048
+ .capa h1 .grad {
1049
+ background: linear-gradient(92deg, var(--primo), #ff9f7a 55%, #8b5cf6);
1050
+ -webkit-background-clip: text;
1051
+ background-clip: text;
1052
+ color: transparent;
1053
+ }
1054
+ .capa p { color: var(--fraco); font-size: 14.5px; line-height: 1.7; margin: 0 0 22px; max-width: 560px; }
1055
+ .capa-acoes { display: flex; gap: 10px; flex-wrap: wrap; }
1056
+ .btn.grande { height: 40px; padding: 0 18px; font-size: 13.5px; border-radius: 12px; }
1057
+
1058
+ .selo-vivo { color: var(--fraco); }
1059
+ .selo-vivo i {
1060
+ width: 7px; height: 7px; border-radius: 999px; background: #4ade80;
1061
+ box-shadow: 0 0 0 3px rgba(74, 222, 128, .16);
1062
+ animation: pulso 2.4s ease-in-out infinite;
1063
+ }
1064
+ @keyframes pulso { 50% { box-shadow: 0 0 0 6px rgba(74, 222, 128, 0); } }
1065
+
1066
+ /* A arte da direita é o produto em miniatura: três quadros de cena, com a
1067
+ mesma linguagem do render. Nada de screenshot — captura envelhece e mente. */
1068
+ .capa-arte { position: relative; height: 250px; }
1069
+ .capa-arte .quadro {
1070
+ position: absolute;
1071
+ border: 1px solid var(--linha2);
1072
+ border-radius: 14px;
1073
+ background: linear-gradient(160deg, #16161d, #0c0c11);
1074
+ box-shadow: 0 24px 60px rgba(0, 0, 0, .45);
1075
+ animation: flutua 9s ease-in-out infinite alternate;
1076
+ }
1077
+ .capa-arte .q1 {
1078
+ left: 0; top: 16px; width: 62%; height: 132px; padding: 16px;
1079
+ display: flex; flex-direction: column; gap: 9px; justify-content: center;
1080
+ }
1081
+ .capa-arte .q1 span { height: 9px; border-radius: 5px; background: rgba(255, 255, 255, .16); }
1082
+ .capa-arte .q1 span:first-child { width: 42%; background: var(--primo); height: 11px; }
1083
+ .capa-arte .q1 span:last-child { width: 68%; }
1084
+ .capa-arte .q2 {
1085
+ right: 6px; top: 0; width: 42%; height: 104px;
1086
+ display: grid; place-content: center; text-align: center;
1087
+ animation-delay: -3s;
1088
+ }
1089
+ .capa-arte .q2 b { font-size: 26px; font-weight: 680; letter-spacing: -.03em; display: block; }
1090
+ .capa-arte .q2 i { font-size: 11px; color: var(--fraco2); font-style: normal; }
1091
+ .capa-arte .q3 {
1092
+ right: 22px; bottom: 4px; width: 54%; height: 96px; padding: 14px;
1093
+ display: flex; align-items: flex-end; gap: 8px;
1094
+ animation-delay: -6s;
1095
+ }
1096
+ .capa-arte .q3 u { flex: 1; border-radius: 4px 4px 0 0; background: linear-gradient(180deg, var(--primo), rgba(255, 90, 54, .25)); }
1097
+ .capa-arte .q3 u:nth-child(1) { height: 38%; }
1098
+ .capa-arte .q3 u:nth-child(2) { height: 72%; }
1099
+ .capa-arte .q3 u:nth-child(3) { height: 54%; }
1100
+ @keyframes flutua { from { transform: translateY(-5px); } to { transform: translateY(7px); } }
1101
+
1102
+ @media (max-width: 880px) {
1103
+ .capa { grid-template-columns: 1fr; padding: 34px 24px; }
1104
+ .capa-arte { display: none; }
1105
+ .capa h1 { font-size: 31px; }
1106
+ }
1107
+
1108
+ /* ------------------------------------------------------- cartões e listas -- */
1109
+
1110
+ /* Seis ferramentas em três colunas fecham duas fileiras cheias; em quatro
1111
+ colunas sobra uma fileira pela metade, e a página parece interrompida. */
1112
+ .grade-ferr {
1113
+ gap: 16px;
1114
+ margin-bottom: 52px;
1115
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1116
+ }
1117
+
1118
+ .cartao-ferr { padding: 22px; background: linear-gradient(180deg, var(--sup), #101015); }
1119
+ .cartao-ferr .brilho {
1120
+ position: absolute; inset: -1px -1px auto -1px; height: 2px;
1121
+ background: linear-gradient(90deg, transparent, var(--cor, var(--primo)), transparent);
1122
+ opacity: 0; transition: opacity .18s;
1123
+ }
1124
+ .cartao-ferr:hover .brilho, .cartao-ferr:focus-visible .brilho { opacity: .9; }
1125
+ .cartao-ferr:focus-visible { outline: 2px solid var(--cor, var(--primo)); outline-offset: 2px; }
1126
+ .cartao-ferr .icone {
1127
+ width: 38px; height: 38px; border-radius: 11px;
1128
+ background: color-mix(in srgb, var(--cor, var(--primo)) 16%, transparent);
1129
+ color: var(--cor, var(--primo));
1130
+ border: 1px solid color-mix(in srgb, var(--cor, var(--primo)) 28%, transparent);
1131
+ }
1132
+ .cartao-ferr .icone svg { width: 19px; height: 19px; }
1133
+ .cartao-ferr:hover { transform: translateY(-3px); box-shadow: 0 18px 40px rgba(0, 0, 0, .35); }
1134
+
1135
+ .item-arq { border-radius: 12px; }
1136
+ .item-arq .tag {
1137
+ background: color-mix(in srgb, var(--cor, var(--primo)) 16%, transparent);
1138
+ color: var(--cor, var(--primo));
1139
+ }
1140
+ .item-arq:hover { transform: translateX(2px); }
1141
+
1142
+ /* ------------------------------------------------------------- poderes --- */
1143
+
1144
+ .poderes { margin-bottom: 50px; }
1145
+ .grade-poder {
1146
+ display: grid;
1147
+ grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
1148
+ gap: 12px;
1149
+ }
1150
+ .grade-poder article {
1151
+ border: 1px solid var(--linha);
1152
+ border-radius: var(--r2);
1153
+ background: var(--sup);
1154
+ padding: 16px 18px;
1155
+ }
1156
+ .grade-poder b { font-size: 13.5px; font-weight: 620; display: block; margin-bottom: 5px; }
1157
+ .grade-poder p { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--fraco); }
1158
+
1159
+ .copiado { font-size: 12px; color: #4ade80; opacity: 0; transition: opacity .2s; }
1160
+ .copiado.on { opacity: 1; }
1161
+
1162
+ /* Quem pediu menos movimento recebe menos movimento — a capa continua
1163
+ inteira, só para de respirar. */
1164
+ @media (prefers-reduced-motion: reduce) {
1165
+ .capa-luz, .capa-arte .quadro, .selo-vivo i { animation: none; }
1166
+ .cartao-ferr:hover, .item-arq:hover { transform: none; }
1167
+ }
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>PrimoCode Studio</title>
7
7
  <link rel="stylesheet" href="/core.css">
8
+ <link rel="stylesheet" href="/fontes.css">
8
9
  </head>
9
10
  <body>
10
11
 
@@ -27,22 +28,48 @@
27
28
  <header class="topo">
28
29
  <a class="marca" href="/"><span class="cubo">P</span>PrimoCode <em>Studio</em></a>
29
30
  <div class="cresce"></div>
31
+ <input class="campo busca-topo" id="busca" placeholder="Buscar no estúdio /" autocomplete="off">
30
32
  <button class="btn vazio" id="btn-manual">Manual da IA</button>
31
33
  <a class="btn vazio" href="/api/manual" target="_blank">API</a>
32
34
  </header>
33
35
 
34
36
  <div class="home">
35
37
 
36
- <section class="hero">
37
- <h1>O estúdio que a IA opera</h1>
38
- <p>Seis ferramentas nativas com um modelo de documento em comum. A IA escreve o
39
- documento, o estúdio desenha — apresentação, arte, texto, planilha, vídeo ou diagrama —
40
- e você refina à mão no editor. Vídeo sai com narração neural, trilha, efeitos
41
- sonoros e download direto em MP4 ou áudio.</p>
38
+ <section class="capa">
39
+ <div class="capa-luz" aria-hidden="true"></div>
40
+ <div class="capa-texto">
41
+ <span class="pilula selo-vivo"><i></i> o estúdio está no ar em localhost:7777</span>
42
+ <h1>Seis ferramentas.<br>Um documento só.<br><span class="grad">A IA opera tudo.</span></h1>
43
+ <p>Apresentação, arte, texto, planilha, vídeo e diagrama nascem do mesmo modelo —
44
+ a IA escreve o documento, o estúdio desenha, você refina à mão. Vídeo sai com
45
+ narração neural, trilha, efeito sonoro, câmera e keyframes em 3D, e baixa em MP4.</p>
46
+ <div class="capa-acoes">
47
+ <button class="btn primo grande" id="ir-corte">Criar um vídeo</button>
48
+ <button class="btn grande" id="ir-prisma">Criar uma apresentação</button>
49
+ </div>
50
+ </div>
51
+ <div class="capa-arte" aria-hidden="true">
52
+ <div class="quadro q1"><span></span><span></span><span></span></div>
53
+ <div class="quadro q2"><b>+38%</b><i>em 12 meses</i></div>
54
+ <div class="quadro q3"><u></u><u></u><u></u></div>
55
+ </div>
42
56
  </section>
43
57
 
58
+ <h2 class="secao-tit" id="tit-ferramentas">As ferramentas</h2>
44
59
  <section class="grade-ferr" id="ferramentas"></section>
45
60
 
61
+ <section class="poderes">
62
+ <h2 class="secao-tit">O que a IA consegue fazer aqui</h2>
63
+ <div class="grade-poder">
64
+ <article><b>Diretor antes da produção</b><p>Roteiro, narração falada, voz, câmera e a mídia a buscar — decidido antes de a primeira cena existir.</p></article>
65
+ <article><b>Câmera e paralaxe</b><p>O quadro se aproxima, atravessa e balança; as camadas andam em velocidades diferentes.</p></article>
66
+ <article><b>Keyframes em 3D</b><p>Giro em profundidade, curvas com nome (elástico, salto) e som no instante em que o elemento entra.</p></article>
67
+ <article><b>Mídia da internet</b><p>Foto, logo em SVG da marca e música com licença aberta — com o crédito guardado junto do arquivo.</p></article>
68
+ <article><b>Narração neural</b><p>Voz sintetizada em pt-BR, sincronia automática com a cena e a trilha abaixando sozinha enquanto a voz fala.</p></article>
69
+ <article><b>MP4 sem clique</b><p>Exporta o arquivo direto, ou vira projeto Remotion quando o vídeo é longo ou precisa de canal alfa.</p></article>
70
+ </div>
71
+ </section>
72
+
46
73
  <section class="faixa-ia">
47
74
  <h2>Como pedir</h2>
48
75
  <p>Fale com seu agente em português normal. Ele lê o manual, escolhe a ferramenta e devolve o link.</p>
@@ -51,7 +78,7 @@ usando o Prisma. Me devolva a URL "ver" da resposta.</pre>
51
78
  <div class="acoes">
52
79
  <button class="btn primo" id="copiar">Copiar</button>
53
80
  <a class="btn" href="/api/manual" target="_blank">Ler o manual</a>
54
- <span id="copiado" style="font-size:12px;color:#4ade80;opacity:0;transition:opacity .2s">copiado</span>
81
+ <span id="copiado" class="copiado">copiado</span>
55
82
  </div>
56
83
  </section>
57
84
 
@@ -70,15 +97,16 @@ usando o Prisma. Me devolva a URL "ver" da resposta.</pre>
70
97
  (function () {
71
98
  var $ = function (s) { return document.querySelector(s); };
72
99
  var FERRS = [];
100
+ var ARQUIVOS = [];
73
101
 
74
102
  function fugir(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;"); }
75
103
 
76
104
  fetch("/api/ferramentas").then(function (r) { return r.json(); }).then(function (lista) {
77
105
  FERRS = lista;
78
106
  $("#ferramentas").innerHTML = lista.map(function (f) {
79
- return '<div class="cartao-ferr" data-slug="' + f.slug + '">' +
80
- '<div class="icone" style="background:' + f.cor + '22;color:' + f.cor + '">' +
81
- svgIcone(iconeDe(f.slug)) + "</div>" +
107
+ return '<div class="cartao-ferr" data-slug="' + f.slug + '" style="--cor:' + f.cor + '" tabindex="0" role="button">' +
108
+ '<div class="brilho" aria-hidden="true"></div>' +
109
+ '<div class="icone">' + svgIcone(iconeDe(f.slug)) + "</div>" +
82
110
  "<b>" + fugir(f.nome) + "</b>" +
83
111
  '<div class="papel">' + fugir(f.papel) + "</div>" +
84
112
  "<p>" + fugir(f.resumo) + "</p>" +
@@ -87,6 +115,11 @@ usando o Prisma. Me devolva a URL "ver" da resposta.</pre>
87
115
 
88
116
  Array.prototype.forEach.call(document.querySelectorAll(".cartao-ferr"), function (c) {
89
117
  c.onclick = function () { criar(c.dataset.slug); };
118
+ // Teclado abre igual ao mouse: o cartão é um botão, e um botão que só
119
+ // responde a clique deixa de fora quem navega por Tab.
120
+ c.onkeydown = function (ev) {
121
+ if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); criar(c.dataset.slug); }
122
+ };
90
123
  });
91
124
  carregarArquivos();
92
125
  });
@@ -110,28 +143,59 @@ usando o Prisma. Me devolva a URL "ver" da resposta.</pre>
110
143
  });
111
144
  }
112
145
 
146
+ $("#ir-corte").onclick = function () { criar("corte"); };
147
+ $("#ir-prisma").onclick = function () { criar("prisma"); };
148
+
113
149
  function carregarArquivos() {
114
150
  fetch("/api/arquivos").then(function (r) { return r.json(); }).then(function (lista) {
115
- if (!lista.length) { $("#vazio").style.display = "block"; return; }
116
- $("#arquivos").innerHTML = lista.map(function (a) {
117
- var cor = corDe(a.ferramenta);
118
- var quando = a.atualizado ? a.atualizado.slice(0, 10).split("-").reverse().join("/") : "";
119
- return '<div class="item-arq">' +
120
- '<div class="tag" style="background:' + cor + '22;color:' + cor + '">' + svgIcone(iconeDe(a.ferramenta)) + "</div>" +
121
- '<div class="cresce"><b>' + fugir(a.titulo) + "</b>" +
122
- '<div class="meta">' + fugir(a.ferramenta) + " · " + a.pecas +
123
- (a.pecas === 1 ? " peça · " : " peças · ") + quando + "</div></div>" +
124
- '<a class="btn" href="/t/' + a.ferramenta + "/" + a.id + '">Editar</a>' +
125
- '<a class="btn primo" href="/v/' + a.id + '" target="_blank">Abrir</a></div>';
126
- }).join("");
151
+ ARQUIVOS = lista;
152
+ desenharArquivos(lista);
127
153
  });
128
154
  }
129
155
 
156
+ function desenharArquivos(lista) {
157
+ if (!lista.length) {
158
+ $("#arquivos").innerHTML = "";
159
+ $("#vazio").style.display = "block";
160
+ return;
161
+ }
162
+ $("#vazio").style.display = "none";
163
+ $("#arquivos").innerHTML = lista.map(function (a) {
164
+ var cor = corDe(a.ferramenta);
165
+ var quando = a.atualizado ? a.atualizado.slice(0, 10).split("-").reverse().join("/") : "";
166
+ return '<div class="item-arq" style="--cor:' + cor + '">' +
167
+ '<div class="tag">' + svgIcone(iconeDe(a.ferramenta)) + "</div>" +
168
+ '<div class="cresce"><b>' + fugir(a.titulo) + "</b>" +
169
+ '<div class="meta">' + fugir(a.ferramenta) + " · " + a.pecas +
170
+ (a.pecas === 1 ? " peça · " : " peças · ") + quando + "</div></div>" +
171
+ '<a class="btn" href="/t/' + a.ferramenta + "/" + a.id + '">Editar</a>' +
172
+ '<a class="btn primo" href="/v/' + a.id + '" target="_blank">Abrir</a></div>';
173
+ }).join("");
174
+ }
175
+
176
+ /* A BUSCA filtra o que já está na tela — ferramentas e arquivos.
177
+ Sem ida ao servidor: a lista inteira já veio, e um fetch por tecla
178
+ digitada seria trabalho para devolver o que está na memória. */
179
+ var busca = $("#busca");
180
+ busca.oninput = function () {
181
+ var q = busca.value.trim().toLowerCase();
182
+ Array.prototype.forEach.call(document.querySelectorAll(".cartao-ferr"), function (c) {
183
+ c.style.display = !q || c.textContent.toLowerCase().indexOf(q) >= 0 ? "" : "none";
184
+ });
185
+ desenharArquivos(!q ? ARQUIVOS : ARQUIVOS.filter(function (a) {
186
+ return (a.titulo + " " + a.ferramenta).toLowerCase().indexOf(q) >= 0;
187
+ }));
188
+ };
189
+ document.addEventListener("keydown", function (ev) {
190
+ if (ev.key === "/" && document.activeElement !== busca) { ev.preventDefault(); busca.focus(); }
191
+ if (ev.key === "Escape" && document.activeElement === busca) { busca.value = ""; busca.oninput(); busca.blur(); }
192
+ });
193
+
130
194
  $("#copiar").onclick = function () {
131
195
  navigator.clipboard.writeText($("#frase").textContent).then(function () {
132
196
  var c = $("#copiado");
133
- c.style.opacity = 1;
134
- setTimeout(function () { c.style.opacity = 0; }, 1500);
197
+ c.classList.add("on");
198
+ setTimeout(function () { c.classList.remove("on"); }, 1500);
135
199
  });
136
200
  };
137
201
 
@@ -144,6 +208,9 @@ usando o Prisma. Me devolva a URL "ver" da resposta.</pre>
144
208
  }
145
209
  };
146
210
  folha.onclick = function (e) { if (e.target === folha) folha.classList.remove("on"); };
211
+ document.addEventListener("keydown", function (e) {
212
+ if (e.key === "Escape") folha.classList.remove("on");
213
+ });
147
214
  })();
148
215
  </script>
149
216
  </body>
@@ -24,6 +24,12 @@
24
24
  Os valores NÃO mudam — são os mesmos que o render.css entende. O que
25
25
  muda é só o que a pessoa lê. */
26
26
  var OPC = {
27
+ // A câmera da cena: o quadro que se move. Os valores são os mesmos que o
28
+ // render.js conhece (CAMERAS); aqui vão só os nomes que se leem.
29
+ camera: [["", "Parada"], ["aproximar", "Aproximar"], ["afastar", "Afastar"],
30
+ ["pan-direita", "Atravessar para a direita"], ["pan-esquerda", "Atravessar para a esquerda"],
31
+ ["subir", "Subir"], ["descer", "Descer"], ["empurrar", "Empurrão (zoom com giro)"],
32
+ ["balanco", "Balanço"]],
27
33
  transicao: [["", "Nenhuma (corte seco)"], ["fade", "Suave"], ["deslizar", "Deslizar para o lado"],
28
34
  ["subir", "Subir"], ["zoom", "Aproximar"], ["limpar", "Varrer"],
29
35
  ["persiana", "Persiana"], ["flash", "Flash"]],
@@ -1221,6 +1227,12 @@
1221
1227
  g0.appendChild(linha("Duração (s)", inp(c.duracao || 4, function (v) { mudar(function () { c.duracao = v || 4; }); }, "number")));
1222
1228
  g0.appendChild(linha("Como entra", menu(OPC.transicao, c.transicao || "",
1223
1229
  function (v) { mudar(function () { if (v) c.transicao = v; else delete c.transicao; }); })));
1230
+ /* A câmera só aparece onde há tempo (vídeo): num slide parado ela
1231
+ não teria o que animar, e uma caixa que não faz nada é pior que
1232
+ caixa nenhuma. Guardada como string — é o formato que a spec
1233
+ documenta e o que o render.js aceita direto. */
1234
+ g0.appendChild(linha("Câmera", menu(OPC.camera, typeof c.camera === "string" ? c.camera : "",
1235
+ function (v) { mudar(function () { if (v) c.camera = v; else delete c.camera; }); })));
1224
1236
  }
1225
1237
  if (SLUG === "traco") {
1226
1238
  g0.appendChild(linha("Arranjo", selecao(["", "horizontal", "vertical"], c.auto || "",
@@ -1292,6 +1304,11 @@
1292
1304
  gp.appendChild(linha("Opacidade", inp(e.opacidade != null ? e.opacidade : 1, function (v) { edita("opacidade", v); }, "number")));
1293
1305
  gp.appendChild(linha("Como entra", menu(OPC.animacao, e.anim || "",
1294
1306
  function (v) { edita("anim", v); })));
1307
+ /* Profundidade: o quanto este elemento acompanha a câmera. 1 anda com o
1308
+ quadro, acima vem à frente, abaixo fica ao fundo. Sem câmera na cena
1309
+ não faz nada — e é por isso que mora aqui embaixo, e não em destaque. */
1310
+ gp.appendChild(linha("Profundidade", inp(e.paralaxe != null ? e.paralaxe : 1,
1311
+ function (v) { edita("paralaxe", v === 1 || v === "" ? "" : v); }, "number")));
1295
1312
  if (e.tipo === "numeros") {
1296
1313
  gp.appendChild(linha("Contar", selecao(["não", "sim"], e.contar ? "sim" : "não",
1297
1314
  function (v) { edita("contar", v === "sim" ? true : ""); })));
@@ -590,6 +590,11 @@ function renderElemento(e, ctx) {
590
590
  atrás, com bisel de luz em cima e o reflexo diagonal. Vale em QUALQUER
591
591
  elemento (texto, forma, ícone, imagem, lista, cartões), por isso mora
592
592
  aqui e não num tipo. O CSS faz o resto por `data-material`. */
593
+ /* A profundidade do elemento na cena: quanto ele acompanha a câmera.
594
+ >1 vem à frente (anda mais), <1 fica ao fundo (anda menos). É o que
595
+ transforma um zoom num plano com camadas. */
596
+ if (e.paralaxe != null && isFinite(Number(e.paralaxe))) no.dataset.paralaxe = Number(e.paralaxe);
597
+
593
598
  if (e.material) {
594
599
  no.dataset.material = e.material;
595
600
  if (e.raio != null) no.style.borderRadius = _px(e.raio);
@@ -1467,7 +1472,7 @@ function desenharConectores(cena, elementos, W, H) {
1467
1472
 
1468
1473
  /* ------------------------------------------------------------------ cena */
1469
1474
 
1470
- function renderCena(cena, doc, op) {
1475
+ function renderCenaBase(cena, doc, op) {
1471
1476
  op = op || {};
1472
1477
  cena = cena || {};
1473
1478
  var W = doc.largura || 1920, H = doc.altura || 1080;
@@ -1728,4 +1733,120 @@ function comporCabecaCorpo(fluxo, layout, nCabeca, nCorpo) {
1728
1733
  filhos[filhos.length - 1].classList.add("corpo-fim");
1729
1734
  }
1730
1735
 
1731
- if (typeof module !== "undefined" && module.exports) module.exports = { renderCena: renderCena };
1736
+
1737
+ /* ══ A CÂMERA DA CENA ══════════════════════════════════════════════════════
1738
+ *
1739
+ * `chaves` move UM elemento. O que faltava era mover o QUADRO: a cena inteira
1740
+ * se aproximando enquanto o texto entra, a panorâmica que atravessa a arte, o
1741
+ * leve giro que tira a cara de página parada. É a câmera do After Effects, e
1742
+ * sem ela todo movimento nasce preso ao elemento — o vídeo parece um slide com
1743
+ * animação, que é exatamente a reclamação.
1744
+ *
1745
+ * COMO FUNCIONA: o conteúdo da cena vai para dentro de um `.palco`, e a câmera
1746
+ * anima o palco com a mesma máquina de chaves dos elementos (mesma curva,
1747
+ * mesma interpolação, mesma conta de duração). O FUNDO fica onde está, na raiz
1748
+ * da cena: é o que dá profundidade de graça — o quadro anda sobre o fundo em
1749
+ * vez de arrastar tudo junto, que é como um zoom de câmera se comporta.
1750
+ *
1751
+ * PARALAXE: um elemento com `paralaxe` anda MAIS (>1, perto da câmera) ou
1752
+ * MENOS (<1, ao longe) que o palco. Como ele já pode ter as próprias chaves, a
1753
+ * diferença vai num embrulho em volta dele — duas animações no mesmo nó
1754
+ * disputariam a mesma propriedade `transform`, e a segunda ganharia calada.
1755
+ */
1756
+
1757
+ // Movimentos com NOME, porque quem escreve o documento não quer pensar em
1758
+ // chave: são os quatro que aparecem em toda peça de motion.
1759
+ var CAMERAS = {
1760
+ aproximar: [{ t: 0, escala: 1 }, { t: 1, escala: 1.12 }],
1761
+ afastar: [{ t: 0, escala: 1.12 }, { t: 1, escala: 1 }],
1762
+ "pan-direita": [{ t: 0, x: -70 }, { t: 1, x: 70 }],
1763
+ "pan-esquerda": [{ t: 0, x: 70 }, { t: 1, x: -70 }],
1764
+ subir: [{ t: 0, y: 60 }, { t: 1, y: -60 }],
1765
+ descer: [{ t: 0, y: -60 }, { t: 1, y: 60 }],
1766
+ // O "empurrão" é o zoom com um grau de giro: é o que dá mão humana ao quadro.
1767
+ empurrar: [{ t: 0, escala: 1.02, giro: -0.6 }, { t: 1, escala: 1.14, giro: 0.3 }],
1768
+ balanco: [{ t: 0, x: -18, giro: -0.5 }, { t: 0.5, x: 12, giro: 0.4 }, { t: 1, x: -6, giro: -0.2 }],
1769
+ };
1770
+
1771
+ function chavesDaCamera(camera) {
1772
+ if (!camera) return null;
1773
+ if (typeof camera === "string") return CAMERAS[camera] ? CAMERAS[camera].slice() : null;
1774
+ if (Array.isArray(camera)) return camera.slice();
1775
+ if (camera.chaves && camera.chaves.length) return camera.chaves.slice();
1776
+ if (camera.movimento && CAMERAS[camera.movimento]) return CAMERAS[camera.movimento].slice();
1777
+ return null;
1778
+ }
1779
+
1780
+ /* A chave do elemento de paralaxe: a da câmera, menos a que o palco já deu.
1781
+ Fator 0 devolve nada (anda com o palco, que é o padrão). */
1782
+ function chavesDeParalaxe(chaves, fator) {
1783
+ var f = fator - 1;
1784
+ if (!f) return null;
1785
+ return chaves.map(function (k) {
1786
+ var nova = { t: _numero(k.t, 0) };
1787
+ if (typeof k.x === "number") nova.x = k.x * f;
1788
+ if (typeof k.y === "number") nova.y = k.y * f;
1789
+ if (typeof k.escala === "number") nova.escala = 1 + (k.escala - 1) * f;
1790
+ return nova;
1791
+ });
1792
+ }
1793
+
1794
+ function aplicarCamera(no, cena, op) {
1795
+ var chaves = chavesDaCamera(cena.camera);
1796
+ if (!chaves || !chaves.length || !op.animar) return no;
1797
+
1798
+ var palco = document.createElement("div");
1799
+ palco.className = "palco";
1800
+ while (no.firstChild) palco.appendChild(no.firstChild);
1801
+ no.appendChild(palco);
1802
+
1803
+ var ctx = { raiz: no, duracao: cena.duracao || 4, animar: true };
1804
+ aplicarChaves(palco, {
1805
+ chaves: chaves,
1806
+ suavizar: (cena.camera && cena.camera.suavizar) || "suave",
1807
+ }, ctx);
1808
+
1809
+ /* PARALAXE — e o cuidado que decide se isto funciona ou quebra o layout.
1810
+ · Elemento SEM animação própria recebe a diferença direto: é o caso
1811
+ comum, e assim nada muda no DOM.
1812
+ · Elemento que JÁ anima (tem chaves suas) precisa de um embrulho —
1813
+ duas animações no mesmo nó disputam `transform` e a segunda ganha
1814
+ calada. Embrulhar só é seguro quando ele está posicionado em
1815
+ absoluto; no fluxo, o embrulho viraria um filho a mais no flex e
1816
+ mudaria o espaçamento da cena. Nesse caso a paralaxe é ignorada, e
1817
+ vale a animação do elemento, que é a mais específica. */
1818
+ var comProfundidade = palco.querySelectorAll("[data-paralaxe]");
1819
+ Array.prototype.forEach.call(comProfundidade, function (el) {
1820
+ var fator = parseFloat(el.getAttribute("data-paralaxe"));
1821
+ if (!isFinite(fator) || fator === 1) return;
1822
+ var extras = chavesDeParalaxe(chaves, fator);
1823
+ if (!extras) return;
1824
+ var suave = (cena.camera && cena.camera.suavizar) || "suave";
1825
+
1826
+ if (!el.style.animationName) {
1827
+ aplicarChaves(el, { chaves: extras, suavizar: suave }, ctx);
1828
+ return;
1829
+ }
1830
+ if (el.style.position !== "absolute") return;
1831
+
1832
+ var capa = document.createElement("div");
1833
+ capa.className = "paralaxe";
1834
+ capa.style.position = "absolute";
1835
+ ["left", "top", "right", "bottom", "width", "height"].forEach(function (k) {
1836
+ capa.style[k] = el.style[k];
1837
+ el.style[k] = "";
1838
+ });
1839
+ el.style.position = "relative";
1840
+ el.parentNode.insertBefore(capa, el);
1841
+ capa.appendChild(el);
1842
+ aplicarChaves(capa, { chaves: extras, suavizar: suave }, ctx);
1843
+ });
1844
+ return no;
1845
+ }
1846
+
1847
+ function renderCena(cena, doc, op) {
1848
+ var no = renderCenaBase(cena, doc, op);
1849
+ return aplicarCamera(no, cena || {}, op || {});
1850
+ }
1851
+
1852
+ if (typeof module !== "undefined" && module.exports) module.exports = { renderCena: renderCena, CAMERAS: CAMERAS };
@@ -1332,3 +1332,21 @@
1332
1332
  column-gap: calc(var(--u, 19px) * 3);
1333
1333
  }
1334
1334
  .cena .fluxo[data-composicao="cabeca-corpo"] .el.numeros .num b { white-space: nowrap; }
1335
+
1336
+ /* ─────────────────────────────────────────────────────────── câmera ────
1337
+ O palco é o conteúdo da cena; a câmera anima ele. O FUNDO fica na raiz
1338
+ de propósito: o quadro anda sobre o fundo, como faz uma câmera, em vez
1339
+ de arrastar tudo junto — é daí que vem a profundidade sem custo.
1340
+
1341
+ `inset: 0` repete a geometria que o `.fluxo` já tinha, para o embrulho
1342
+ não mudar uma única posição de elemento quando a cena não tem câmera. */
1343
+ .cena > .palco {
1344
+ position: absolute;
1345
+ inset: 0;
1346
+ z-index: 1;
1347
+ transform-origin: 50% 50%;
1348
+ will-change: transform;
1349
+ }
1350
+ /* O embrulho de paralaxe só existe em volta de elemento posicionado, e
1351
+ herda a caixa dele — ver o comentário em render.js. */
1352
+ .cena .paralaxe { transform-origin: 50% 50%; will-change: transform; }