primocode 9.8.0 → 9.10.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.
@@ -97,6 +97,7 @@ import {
97
97
  AbsoluteFill, Img, Video, Audio, Sequence, useCurrentFrame,
98
98
  useVideoConfig, interpolate, staticFile, continueRender, delayRender,
99
99
  } from 'remotion';
100
+ import { morfar, ehCor, misturarCor } from './morfar';
100
101
 
101
102
  /* ── SUAVIZAÇÃO ──────────────────────────────────────────────────────────
102
103
  * Cada curva é uma função de 0..1 em 0..1. "suave" é o padrão porque
@@ -133,7 +134,14 @@ function valorEm(trilha, t, padrao) {
133
134
  if (t >= a.t && t <= b.t) {
134
135
  const bruto = b.t === a.t ? 1 : (t - a.t) / (b.t - a.t);
135
136
  const curva = CURVAS[b.e || a.e || 'suave'] || CURVAS.suave;
136
- return a.v + (b.v - a.v) * curva(bruto);
137
+ const k = curva(bruto);
138
+ /* Texto no lugar do número: é COR ("#ff6a00") ou CAMINHO (o "d" de um
139
+ SVG). A cor mistura; o caminho se deforma (morfar.js): a bolinha
140
+ vira foguete sem corte. */
141
+ if (typeof a.v === 'string' || typeof b.v === 'string') {
142
+ return ehCor(a.v) && ehCor(b.v) ? misturarCor(a.v, b.v, k) : morfar(a.v, b.v, k);
143
+ }
144
+ return a.v + (b.v - a.v) * k;
137
145
  }
138
146
  }
139
147
  return padrao;
@@ -153,12 +161,17 @@ function transformDe(a, base) {
153
161
  if (x != null || y != null || z != null) {
154
162
  partes.push('translate3d(' + (x || 0) + 'px,' + (y || 0) + 'px,' + (z || 0) + 'px)');
155
163
  }
156
- if (a.escala != null) partes.push('scale(' + a.escala + ')');
157
- if (a.escalaX != null) partes.push('scaleX(' + a.escalaX + ')');
158
- if (a.escalaY != null) partes.push('scaleY(' + a.escalaY + ')');
164
+ /* GIRA DEPOIS DE ENCOLHER. No CSS o último da lista age primeiro, então a
165
+ escala vai por último: o elemento encolhe e só então gira. Na ordem
166
+ inversa, uma janela de 1920 px girava no tamanho cheio e só depois
167
+ encolhia em X e Y — a profundidade não encolhe, e sobrava um risco de
168
+ 1920 px atravessando a tela. */
159
169
  if (a.rotacao != null) partes.push('rotate(' + a.rotacao + 'deg)');
160
170
  if (a.rotacaoX != null) partes.push('rotateX(' + a.rotacaoX + 'deg)');
161
171
  if (a.rotacaoY != null) partes.push('rotateY(' + a.rotacaoY + 'deg)');
172
+ if (a.escala != null) partes.push('scale(' + a.escala + ')');
173
+ if (a.escalaX != null) partes.push('scaleX(' + a.escalaX + ')');
174
+ if (a.escalaY != null) partes.push('scaleY(' + a.escalaY + ')');
162
175
  return partes.join(' ');
163
176
  }
164
177
 
@@ -222,7 +235,7 @@ function Elemento({ el, t, dados }) {
222
235
  estilo.backgroundColor = el.vidro.cor || 'rgba(255,255,255,0.12)';
223
236
  estilo.border = el.vidro.borda || '1px solid rgba(255,255,255,0.22)';
224
237
  }
225
- if (el.raio != null) estilo.borderRadius = medida(a.raio ?? el.raio);
238
+ if (el.raio != null || a.raio != null) estilo.borderRadius = medida(a.raio ?? el.raio);
226
239
  if (el.sombra) estilo.boxShadow = el.sombra;
227
240
  if (el.mascara) {
228
241
  estilo.WebkitMaskImage = el.mascara;
@@ -298,18 +311,68 @@ function Elemento({ el, t, dados }) {
298
311
 
299
312
  case 'forma': {
300
313
  const forma = el.forma || 'retangulo';
314
+ /* O TRAÇO: qualquer forma, desenhada por um caminho SVG na caixa
315
+ "vista" (0 0 100 100 por padrão). "caminho" e "cor" animam, e é
316
+ isso que deixa uma forma virar outra no mesmo plano. */
317
+ if (forma === 'traco') {
318
+ const g = el.fundo && typeof el.fundo === 'object' && el.fundo.tipo === 'gradiente' ? el.fundo : null;
319
+ const gid = 'g' + String(el.id || JSON.stringify(g || '')).replace(/[^a-z0-9]/gi, '').slice(0, 24);
320
+ const ang = ((g && g.angulo) ?? 180) * Math.PI / 180;
321
+ /* Volume: o degradê RADIAL com o centro fora do meio é a luz batendo de
322
+ um lado (a bola vira esfera), e a sombra segue o CONTORNO da forma
323
+ (drop-shadow), não a caixa dela. */
324
+ const paradas = g ? g.cores.map((c, i) => <stop key={i} offset={i / Math.max(1, g.cores.length - 1)} stopColor={c} />) : null;
325
+ const sombra = el.sombra ? 'drop-shadow(' + el.sombra + ')' : '';
326
+ return (
327
+ <div style={{ ...estilo, width: medida(el.largura, '200px'), height: medida(el.altura, '200px'),
328
+ filter: [estilo.filter, sombra].filter(Boolean).join(' ') || undefined, boxShadow: undefined }}>
329
+ <svg viewBox={el.vista || '0 0 100 100'} width="100%" height="100%" style={{ overflow: 'visible', display: 'block' }}>
330
+ {g && (
331
+ <defs>
332
+ {g.radial
333
+ ? <radialGradient id={gid} cx={(g.centro && g.centro[0]) ?? 0.35} cy={(g.centro && g.centro[1]) ?? 0.3} r={g.raio ?? 0.75}>{paradas}</radialGradient>
334
+ : <linearGradient id={gid} x1={0.5 - Math.sin(ang) / 2} y1={0.5 + Math.cos(ang) / 2} x2={0.5 + Math.sin(ang) / 2} y2={0.5 - Math.cos(ang) / 2}>{paradas}</linearGradient>}
335
+ </defs>
336
+ )}
337
+ <path d={a.caminho ?? el.caminho} fill={el.vazado ? 'none' : (g ? 'url(#' + gid + ')' : (a.cor ?? el.cor ?? '#fff'))}
338
+ stroke={el.linha || undefined} strokeWidth={el.espessura || undefined} strokeLinejoin="round" strokeLinecap="round" />
339
+ </svg>
340
+ </div>
341
+ );
342
+ }
301
343
  return (
302
344
  <div style={{
303
345
  ...estilo,
304
346
  width: medida(el.largura, '200px'),
305
347
  height: forma === 'linha' ? medida(el.espessura, '4px') : medida(el.altura, '200px'),
306
- ...(el.fundo && typeof el.fundo === 'object' ? fundoCSS(el.fundo) : { background: el.fundo || el.cor || '#fff' }),
348
+ ...(el.fundo && typeof el.fundo === 'object' ? fundoCSS(el.fundo) : { background: a.cor ?? el.fundo ?? el.cor ?? '#fff' }),
307
349
  borderRadius: forma === 'circulo' ? '50%' : medida(a.raio ?? el.raio, '0px'),
308
350
  border: el.contorno || undefined,
309
351
  }} />
310
352
  );
311
353
  }
312
354
 
355
+ /* O GRUPO: uma tela dentro da tela. Os filhos vivem no tempo e no espaço
356
+ dele; com "recorte" o que passa da borda some (a tela do celular). O
357
+ grupo anima como qualquer elemento: cresce, sai do celular, cobre tudo. */
358
+ case 'grupo':
359
+ return (
360
+ <div style={{
361
+ ...estilo,
362
+ width: medida(el.largura, '100%'), height: medida(el.altura, '100%'),
363
+ overflow: el.recorte ? 'hidden' : 'visible',
364
+ /* 3D: sem recorte, o grupo deixa os filhos no espaço da cena
365
+ (preserve-3d). Com recorte ele vira uma superfície plana, e a
366
+ "perspectiva" dele dá profundidade ao que está dentro. */
367
+ transformStyle: el.recorte ? undefined : 'preserve-3d',
368
+ perspective: el.perspectiva ? el.perspectiva + 'px' : undefined,
369
+ ...(el.fundo ? (typeof el.fundo === 'object' ? fundoCSS(el.fundo) : { background: el.fundo }) : {}),
370
+ border: el.contorno || undefined,
371
+ }}>
372
+ {(el.elementos || []).map((f, i) => <Elemento key={i} el={f} t={tLocal} dados={dados} />)}
373
+ </div>
374
+ );
375
+
313
376
  case 'imagem':
314
377
  return (
315
378
  <div style={{ ...estilo, overflow: 'hidden' }}>
@@ -445,10 +508,19 @@ export function Cena({ cena, dados, tFixo }) {
445
508
  tx || ty ? 'translate(' + tx + 'px,' + ty + 'px)' : '',
446
509
  cam.zoom != null || soco !== 1 ? 'scale(' + ((cam.zoom ?? 1) * soco) + ')' : '',
447
510
  cam.rotacao != null || tr ? 'rotate(' + ((cam.rotacao || 0) + tr) + 'deg)' : '',
511
+ /* A CÂMERA 3D: ela gira em volta do mundo (rotacaoX/rotacaoY) e avança
512
+ nele (z). O que está mais perto anda mais que o que está longe — é a
513
+ profundidade de verdade, não um zoom. */
514
+ cam.z != null ? 'translateZ(' + cam.z + 'px)' : '',
515
+ cam.rotacaoX != null ? 'rotateX(' + cam.rotacaoX + 'deg)' : '',
516
+ cam.rotacaoY != null ? 'rotateY(' + cam.rotacaoY + 'deg)' : '',
448
517
  ].filter(Boolean).join(' ');
449
518
 
450
519
  return (
451
- <AbsoluteFill style={{ ...fundoCSS(cena.fundo), overflow: 'hidden' }}>
520
+ /* A perspectiva mora AQUI, um nível acima do mundo: se ficasse no mesmo
521
+ elemento da câmera, girar a câmera sairia achatado. */
522
+ <AbsoluteFill style={{ ...fundoCSS(cena.fundo), overflow: 'hidden',
523
+ perspective: cena.perspectiva ? cena.perspectiva + 'px' : undefined }}>
452
524
  {cena.fundo && cena.fundo.tipo === 'imagem' && (
453
525
  <Img src={staticFile(cena.fundo.src)} style={{
454
526
  position: 'absolute', inset: 0, width: '100%', height: '100%',
@@ -464,9 +536,7 @@ export function Cena({ cena, dados, tFixo }) {
464
536
  <AbsoluteFill style={{
465
537
  transform: camTransform || undefined,
466
538
  transformOrigin: cena.cameraOrigem || 'center center',
467
- // A perspectiva é o que transforma rotateX/rotateY em 3D de verdade;
468
- // sem ela, girar em X só achata o elemento.
469
- perspective: cena.perspectiva ? cena.perspectiva + 'px' : undefined,
539
+ // O mundo guarda o 3D dos filhos; a perspectiva vem do pai.
470
540
  transformStyle: cena.perspectiva ? 'preserve-3d' : undefined,
471
541
  }}>
472
542
  {(cena.elementos || []).map((el, i) => (
@@ -729,6 +799,7 @@ function montar({ pasta, titulo, plano, icones = {} }) {
729
799
  escrever(path.join(src, 'Root.jsx'), rootJsx(plano));
730
800
  escrever(path.join(src, 'Video.jsx'), RAIZ_JSX);
731
801
  escrever(path.join(src, 'Cena.jsx'), CENA_JSX);
802
+ escrever(path.join(src, 'morfar.js'), require('./morfar.js').fonteEsm());
732
803
  escrever(path.join(src, 'plano.json'), JSON.stringify(plano, null, 2));
733
804
  escrever(path.join(src, 'icones.json'), JSON.stringify(icones, null, 2));
734
805
  escrever(path.join(src, 'fontes.css'), fontesCSS(plano));
@@ -0,0 +1,62 @@
1
+ /**
2
+ * sons-leves.js — os efeitos do motion: leves, limpos, sintetizados.
3
+ *
4
+ * "Animações suaves, leves e limpas, coloca música, narração e efeitos
5
+ * sonoros." — Joel (28/09/2026)
6
+ *
7
+ * Os efeitos da edição (edicao.js SONS) são de corte de reels: whoosh cheio,
8
+ * estalo seco, impacto grave. Para o motion contínuo eles pesam. Estes são a
9
+ * versão leve: sopro em vez de whoosh, bolha em vez de estalo, um sino para o
10
+ * que nasce, um toque grave e macio para o que chega.
11
+ *
12
+ * Tudo sai do ffmpeg na hora (nada baixado, nada com licença de terceiros),
13
+ * uma vez por projeto.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const EFEITOS = {
22
+ // Sopro: ruído rosa filtrado, entra e sai devagar. É o ar da passagem.
23
+ sopro: ['-f', 'lavfi', '-i', 'anoisesrc=d=0.9:c=pink:a=0.5', '-af',
24
+ 'highpass=f=300,lowpass=f=2600,afade=t=in:st=0:d=0.45:curve=qsin,afade=t=out:st=0.45:d=0.45:curve=qsin,volume=0.7'],
25
+ // Bolha: um seno curto que sobe, sem ataque seco.
26
+ bolha: ['-f', 'lavfi', '-i', "aevalsrc='0.6*sin(2*PI*(520+900*t)*t)*exp(-26*t)*(1-exp(-400*t))':s=44100:d=0.22"],
27
+ // Clique: o tique limpo de um trackpad.
28
+ clique: ['-f', 'lavfi', '-i', "aevalsrc='0.5*sin(2*PI*2400*t)*exp(-90*t)+0.15*(random(0)*2-1)*exp(-160*t)':s=44100:d=0.08"],
29
+ // Brilho: dois sinos em quinta, decaindo — o que nasce.
30
+ brilho: ['-f', 'lavfi', '-i', "aevalsrc='0.32*sin(2*PI*880*t)*exp(-3.2*t)+0.22*sin(2*PI*1320*t)*exp(-4*t)+0.12*sin(2*PI*1760*t)*exp(-5*t)':s=44100:d=1.4"],
31
+ // Subida: um seno que cresce devagar — anuncia a transformação.
32
+ subida: ['-f', 'lavfi', '-i', "aevalsrc='0.28*sin(2*PI*(180+260*t*t)*t)*(t/1.4)':s=44100:d=1.4", '-af', 'afade=t=out:st=1.2:d=0.2'],
33
+ // Toque grave e macio: o que chega e assenta.
34
+ toque: ['-f', 'lavfi', '-i', "aevalsrc='0.7*sin(2*PI*(110-50*t)*t)*exp(-7*t)':s=44100:d=0.6", '-af', 'lowpass=f=900'],
35
+ };
36
+
37
+ /**
38
+ * Gera os efeitos na pasta do projeto (midia/sfx-<nome>.mp3) e devolve
39
+ * { nome: caminho relativo }. O que já existe não é refeito; o que o ffmpeg
40
+ * não conseguiu gerar fica de fora (o vídeo sai sem aquele som, não quebra).
41
+ *
42
+ * `rodar(args)` é injetável: o padrão chama o ffmpeg por spawn síncrono.
43
+ */
44
+ function preparar(pasta, { rodar = null } = {}) {
45
+ const exec = rodar || ((args) => {
46
+ try { require('child_process').execFileSync('ffmpeg', args, { stdio: 'ignore', timeout: 30000 }); return true; }
47
+ catch { return false; }
48
+ });
49
+ const sons = {};
50
+ for (const [nome, args] of Object.entries(EFEITOS)) {
51
+ const rel = `midia/sfx-${nome}.mp3`;
52
+ const cheio = path.join(pasta, rel);
53
+ if (!fs.existsSync(cheio)) {
54
+ fs.mkdirSync(path.dirname(cheio), { recursive: true });
55
+ if (!exec(['-y', '-loglevel', 'error', ...args, '-q:a', '3', cheio])) continue;
56
+ }
57
+ sons[nome] = rel;
58
+ }
59
+ return sons;
60
+ }
61
+
62
+ module.exports = { EFEITOS, preparar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0",
3
+ "version": "9.10.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/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -42,6 +42,27 @@ a 4s. Fecho: 2s. Variar a duração é o que cria ritmo; repetir cria sono.
42
42
  **Silêncio visual.** Um quadro com uma frase e mais nada vale mais que um com
43
43
  seis elementos. Espaço vazio é composição, não desperdício.
44
44
 
45
+ ## Motion é contínuo, não uma tela por ideia
46
+
47
+ Slide troca de tela. Motion é UM plano onde o mesmo objeto VIRA o próximo: a
48
+ bolinha acende um fogo, se deforma em foguete, mergulha no celular e vira o
49
+ ícone; o cursor gira e clica; o ícone salta e cobre a tela com o app dentro.
50
+
51
+ Em vídeo de produto, anúncio e vinheta, escreva **uma cena de 8 a 15 s** no
52
+ `estudio_plano`, não uma cena por frase:
53
+
54
+ - **`forma: "traco"`**: forma livre por `caminho` (`d` de SVG, caixa 0 0 100
55
+ 100, M/L/C/Q/Z). `caminho` e `cor` animam:
56
+ `"caminho": [{"t":1.6,"v":BOLA},{"t":2.5,"v":FOGUETE,"e":"elastico"}]`.
57
+ - **`tipo: "grupo"`**: elementos que andam juntos, ou a tela dentro da tela
58
+ (`"recorte": true`). Filhos contam tempo e posição a partir do grupo.
59
+ - **A troca é no mesmo lugar e instante**: o foguete some onde o ícone nasce; o
60
+ app nasce do tamanho e da COR do ícone.
61
+
62
+ Espreme ao quicar e deixe a câmera andar até o fim.
63
+
64
+ Vídeo por roteiro já sai assim (estilo padrão `motion`); escreva à mão só o que o roteiro não descreve.
65
+
45
66
  ## Cor e tipo
46
67
 
47
68
  Escolha a paleta a partir do ASSUNTO, e escreva os valores. Três a cinco