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.
- package/README.md +2 -2
- package/lib/estudio/cinema.js +5 -3
- package/lib/estudio/edicao.js +32 -7
- package/lib/estudio/ende.js +109 -0
- package/lib/estudio/index.js +38 -9
- package/lib/estudio/morfar.js +191 -0
- package/lib/estudio/motion.js +336 -0
- package/lib/estudio/plano.js +47 -6
- package/lib/estudio/producao.js +106 -0
- package/lib/estudio/remotion.js +81 -10
- package/lib/estudio/sons-leves.js +62 -0
- package/package.json +2 -2
- package/skills/motion-de-primeira/SKILL.md +21 -0
package/lib/estudio/remotion.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|