primocode 9.10.0 → 10.0.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 +30 -6
- package/bin/primocode.js +100 -341
- package/lib/act.js +245 -23
- package/lib/browser.js +17 -0
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +4 -2
- package/lib/estudio/diretor.js +9 -1
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +41 -18
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/index.jsx +11 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +72 -0
- package/lib/estudio/lancamento.js +413 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/remotion.js +87 -43
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/vox.js +385 -0
- package/lib/fala.js +1 -94
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +8 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +79 -20
- package/lib/ui.js +13 -6
- package/lib/video/analise.js +89 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +247 -0
- package/lib/video/ops.js +322 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/render.js +221 -0
- package/lib/video/texto.js +142 -0
- package/lib/video/timeline.js +310 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +3 -4
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/lib/estudio/remotion.js
CHANGED
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
useVideoConfig, interpolate, staticFile, continueRender, delayRender,
|
|
99
99
|
} from 'remotion';
|
|
100
100
|
import { morfar, ehCor, misturarCor } from './morfar';
|
|
101
|
+
import { KIT, CURVAS_KIT, Filtros } from './kit';
|
|
101
102
|
|
|
102
103
|
/* ── SUAVIZAÇÃO ──────────────────────────────────────────────────────────
|
|
103
104
|
* Cada curva é uma função de 0..1 em 0..1. "suave" é o padrão porque
|
|
@@ -117,6 +118,8 @@ const CURVAS = {
|
|
|
117
118
|
if (t < 2.5 / d) return n * (t -= 2.25 / d) * t + 0.9375;
|
|
118
119
|
return n * (t -= 2.625 / d) * t + 0.984375;
|
|
119
120
|
},
|
|
121
|
+
// editorial, camera, arrasto, acelera (kit/base.jsx): a curva certa do guia VOX.
|
|
122
|
+
...CURVAS_KIT,
|
|
120
123
|
};
|
|
121
124
|
|
|
122
125
|
/* O valor de uma propriedade animada no segundo "t".
|
|
@@ -217,7 +220,12 @@ function Elemento({ el, t, dados }) {
|
|
|
217
220
|
|
|
218
221
|
const tLocal = t - (el.de || 0);
|
|
219
222
|
const a = animado(el.anima, tLocal);
|
|
220
|
-
|
|
223
|
+
/* A PEÇA DO KIT desenha nas coordenadas da cena (x, y dela são o lugar do
|
|
224
|
+
assunto, não o canto de uma caixa): a caixa dela é o quadro inteiro, e o
|
|
225
|
+
"anima" do elemento move o conjunto. */
|
|
226
|
+
const base = el.tipo === 'kit'
|
|
227
|
+
? { position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' }
|
|
228
|
+
: posicao(el);
|
|
221
229
|
const move = transformDe(a, {});
|
|
222
230
|
const estilo = {
|
|
223
231
|
...base,
|
|
@@ -355,6 +363,16 @@ function Elemento({ el, t, dados }) {
|
|
|
355
363
|
/* O GRUPO: uma tela dentro da tela. Os filhos vivem no tempo e no espaço
|
|
356
364
|
dele; com "recorte" o que passa da borda some (a tela do celular). O
|
|
357
365
|
grupo anima como qualquer elemento: cresce, sai do celular, cobre tudo. */
|
|
366
|
+
case 'kit': {
|
|
367
|
+
const Peca = KIT[el.peca];
|
|
368
|
+
if (!Peca) return null;
|
|
369
|
+
return (
|
|
370
|
+
<div style={{ ...estilo, transformOrigin: el.origem || ((el.x || 0) + 'px ' + (el.y || 0) + 'px') }}>
|
|
371
|
+
<Peca el={el} t={tLocal} fps={dados.fps} />
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
358
376
|
case 'grupo':
|
|
359
377
|
return (
|
|
360
378
|
<div style={{
|
|
@@ -516,11 +534,36 @@ export function Cena({ cena, dados, tFixo }) {
|
|
|
516
534
|
cam.rotacaoY != null ? 'rotateY(' + cam.rotacaoY + 'deg)' : '',
|
|
517
535
|
].filter(Boolean).join(' ');
|
|
518
536
|
|
|
537
|
+
/* A CÂMERA EM CAMADAS (o parallax do guia VOX): "camadas" são planos de
|
|
538
|
+
profundidade, e o que está longe anda menos que o que está perto. O
|
|
539
|
+
parallax é CONSEQUÊNCIA da câmera, não um efeito à parte: o plano do
|
|
540
|
+
assunto ("sujeito") recebe a câmera exata, os outros na proporção da
|
|
541
|
+
profundidade deles — e cada um com o desfoque da lente naquela distância.
|
|
542
|
+
Cada elemento diz a camada dele ("camada": índice); sem, é a do sujeito. */
|
|
543
|
+
const camadas = Array.isArray(cena.camadas) && cena.camadas.length ? cena.camadas : null;
|
|
544
|
+
const iSujeito = camadas ? Math.max(0, camadas.findIndex((c) => c.sujeito)) : 0;
|
|
545
|
+
const mundo = (els, f, desfoque) => (
|
|
546
|
+
<AbsoluteFill style={{
|
|
547
|
+
transform: f == null ? (camTransform || undefined) : [
|
|
548
|
+
'translate(' + (-(cam.x || 0) * f + tx) + 'px,' + (-(cam.y || 0) * f + ty) + 'px)',
|
|
549
|
+
'scale(' + ((1 + ((cam.zoom ?? 1) - 1) * f) * soco) + ')',
|
|
550
|
+
'rotate(' + ((cam.rotacao || 0) * f + tr) + 'deg)',
|
|
551
|
+
].join(' '),
|
|
552
|
+
transformOrigin: cena.cameraOrigem || 'center center',
|
|
553
|
+
// O mundo guarda o 3D dos filhos; a perspectiva vem do pai.
|
|
554
|
+
transformStyle: cena.perspectiva ? 'preserve-3d' : undefined,
|
|
555
|
+
filter: desfoque ? 'blur(' + desfoque + 'px)' : undefined,
|
|
556
|
+
}}>
|
|
557
|
+
{els.map((el, i) => <Elemento key={i} el={el} t={t} dados={dados} />)}
|
|
558
|
+
</AbsoluteFill>
|
|
559
|
+
);
|
|
560
|
+
|
|
519
561
|
return (
|
|
520
562
|
/* A perspectiva mora AQUI, um nível acima do mundo: se ficasse no mesmo
|
|
521
563
|
elemento da câmera, girar a câmera sairia achatado. */
|
|
522
564
|
<AbsoluteFill style={{ ...fundoCSS(cena.fundo), overflow: 'hidden',
|
|
523
565
|
perspective: cena.perspectiva ? cena.perspectiva + 'px' : undefined }}>
|
|
566
|
+
<Filtros />
|
|
524
567
|
{cena.fundo && cena.fundo.tipo === 'imagem' && (
|
|
525
568
|
<Img src={staticFile(cena.fundo.src)} style={{
|
|
526
569
|
position: 'absolute', inset: 0, width: '100%', height: '100%',
|
|
@@ -533,16 +576,13 @@ export function Cena({ cena, dados, tFixo }) {
|
|
|
533
576
|
objectFit: cena.fundo.ajuste || 'cover',
|
|
534
577
|
}} />
|
|
535
578
|
)}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
<Elemento key={i} el={el} t={t} dados={dados} />
|
|
544
|
-
))}
|
|
545
|
-
</AbsoluteFill>
|
|
579
|
+
{camadas
|
|
580
|
+
? camadas.map((c, ci) => (
|
|
581
|
+
<React.Fragment key={ci}>
|
|
582
|
+
{mundo((cena.elementos || []).filter((el) => (el.camada ?? iSujeito) === ci), (c.p || 1) / (camadas[iSujeito].p || 1), c.desfoque)}
|
|
583
|
+
</React.Fragment>
|
|
584
|
+
))
|
|
585
|
+
: mundo(cena.elementos || [], null, 0)}
|
|
546
586
|
{flash > 0.01 && <AbsoluteFill style={{ backgroundColor: cena.corFlash || '#fff', opacity: flash * 0.85 }} />}
|
|
547
587
|
</AbsoluteFill>
|
|
548
588
|
);
|
|
@@ -552,6 +592,7 @@ export function Cena({ cena, dados, tFixo }) {
|
|
|
552
592
|
const RAIZ_JSX = String.raw`import React from 'react';
|
|
553
593
|
import { AbsoluteFill, Audio, Sequence, staticFile, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
|
|
554
594
|
import { Cena } from './Cena';
|
|
595
|
+
import { Acabamento, Hud, bolhaCaminho } from './kit';
|
|
555
596
|
import plano from './plano.json';
|
|
556
597
|
import icones from './icones.json';
|
|
557
598
|
|
|
@@ -560,6 +601,7 @@ import icones from './icones.json';
|
|
|
560
601
|
* o "piscar" de fundo que denuncia corte mal feito. */
|
|
561
602
|
function Transicao({ tipo, progresso, children }) {
|
|
562
603
|
const p = progresso;
|
|
604
|
+
const { width: W, height: H } = useVideoConfig();
|
|
563
605
|
let estilo = {};
|
|
564
606
|
if (tipo === 'esmaecer') estilo = { opacity: p };
|
|
565
607
|
else if (tipo === 'deslizar') estilo = { transform: 'translateX(' + (1 - p) * 100 + '%)' };
|
|
@@ -572,6 +614,12 @@ function Transicao({ tipo, progresso, children }) {
|
|
|
572
614
|
// O SOCO: a cena entra grande e bate no lugar.
|
|
573
615
|
else if (tipo === 'soco') estilo = { transform: 'scale(' + (1.35 - 0.35 * p) + ')', opacity: Math.min(1, p * 3) };
|
|
574
616
|
else if (tipo === 'flash') estilo = { opacity: Math.min(1, p * 2), filter: 'brightness(' + (1 + (1 - p) * 3) + ')' };
|
|
617
|
+
/* O LÍQUIDO: a cena nova entra como uma mancha que cresce de um canto e
|
|
618
|
+
engole a anterior, com a borda viva (os lóbulos respiram enquanto crescem). */
|
|
619
|
+
else if (tipo === 'liquido' && p < 1) {
|
|
620
|
+
const k = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
|
|
621
|
+
estilo = { clipPath: "path('" + bolhaCaminho(W * 0.78, H * 0.62, Math.hypot(W, H) * 0.95 * k, p * 3, 2) + "')" };
|
|
622
|
+
}
|
|
575
623
|
return <AbsoluteFill style={estilo}>{children}</AbsoluteFill>;
|
|
576
624
|
}
|
|
577
625
|
|
|
@@ -591,7 +639,7 @@ export const Paginas = () => {
|
|
|
591
639
|
};
|
|
592
640
|
|
|
593
641
|
export const Video = () => {
|
|
594
|
-
const { fps } = useVideoConfig();
|
|
642
|
+
const { fps, width: largura, height: altura } = useVideoConfig();
|
|
595
643
|
const frame = useCurrentFrame();
|
|
596
644
|
const dados = { fps, icones };
|
|
597
645
|
|
|
@@ -632,31 +680,17 @@ export const Video = () => {
|
|
|
632
680
|
);
|
|
633
681
|
})}
|
|
634
682
|
|
|
635
|
-
{/* O ACABAMENTO: granulação de filme, vinheta e a luz da marca passando
|
|
636
|
-
É o que tira a cara de apresentação
|
|
637
|
-
{plano.acabamento
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
<
|
|
645
|
-
|
|
646
|
-
backgroundSize: (plano.acabamento.passoGrade || 120) + 'px ' + (plano.acabamento.passoGrade || 120) + 'px',
|
|
647
|
-
backgroundPosition: (frame * 0.2) + 'px ' + (frame * 0.1) + 'px' }} />
|
|
648
|
-
)}
|
|
649
|
-
{plano.acabamento && plano.acabamento.vinheta && (
|
|
650
|
-
<AbsoluteFill style={{ pointerEvents: 'none', background: 'radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,' + plano.acabamento.vinheta + ') 100%)' }} />
|
|
651
|
-
)}
|
|
652
|
-
{plano.acabamento && plano.acabamento.grao && (
|
|
653
|
-
<AbsoluteFill style={{ pointerEvents: 'none', opacity: plano.acabamento.grao, mixBlendMode: 'overlay' }}>
|
|
654
|
-
<svg width="100%" height="100%">
|
|
655
|
-
<filter id="grao"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" seed={frame % 60} /></filter>
|
|
656
|
-
<rect width="100%" height="100%" filter="url(#grao)" />
|
|
657
|
-
</svg>
|
|
658
|
-
</AbsoluteFill>
|
|
659
|
-
)}
|
|
683
|
+
{/* O ACABAMENTO: granulação de filme, vinheta e a luz da marca passando
|
|
684
|
+
(kit/base.jsx). É o que tira a cara de apresentação. */}
|
|
685
|
+
<Acabamento ac={plano.acabamento} frame={frame} />
|
|
686
|
+
{/* O HUD do motion designer: cantos, timecode, fps e a seção da cena. */}
|
|
687
|
+
{plano.hud && (() => {
|
|
688
|
+
const atual = pedacos.filter((q) => frame >= q.de).pop() || pedacos[0];
|
|
689
|
+
const total = pedacos.length ? pedacos[pedacos.length - 1].de + pedacos[pedacos.length - 1].dur : 1;
|
|
690
|
+
// A cena diz se é escura (o HUD claro) ou clara (o HUD escuro).
|
|
691
|
+
const escuro = !(atual && atual.cena.escuro === false);
|
|
692
|
+
return <Hud hud={plano.hud} frame={frame} fps={fps} total={total} W={largura} H={altura} secao={atual && atual.cena.secao} escuro={escuro} />;
|
|
693
|
+
})()}
|
|
660
694
|
|
|
661
695
|
{plano.audio && plano.audio.narracao && (
|
|
662
696
|
<Audio src={staticFile(plano.audio.narracao)} volume={plano.audio.volumeNarracao ?? 1} />
|
|
@@ -800,6 +834,8 @@ function montar({ pasta, titulo, plano, icones = {} }) {
|
|
|
800
834
|
escrever(path.join(src, 'Video.jsx'), RAIZ_JSX);
|
|
801
835
|
escrever(path.join(src, 'Cena.jsx'), CENA_JSX);
|
|
802
836
|
escrever(path.join(src, 'morfar.js'), require('./morfar.js').fonteEsm());
|
|
837
|
+
// O kit de motion (kit.js): as peças que o elemento {"tipo":"kit"} monta.
|
|
838
|
+
for (const f of require('./kit.js').arquivos()) escrever(path.join(src, f.nome), f.conteudo);
|
|
803
839
|
escrever(path.join(src, 'plano.json'), JSON.stringify(plano, null, 2));
|
|
804
840
|
escrever(path.join(src, 'icones.json'), JSON.stringify(icones, null, 2));
|
|
805
841
|
escrever(path.join(src, 'fontes.css'), fontesCSS(plano));
|
|
@@ -826,16 +862,24 @@ function montar({ pasta, titulo, plano, icones = {} }) {
|
|
|
826
862
|
do usuário e roda com rede disponível no `npm install`; o Chromium do
|
|
827
863
|
Remotion baixa a fonte no primeiro quadro e o Remotion espera por ela. */
|
|
828
864
|
function fontesCSS(plano) {
|
|
865
|
+
/* Toda chave que começa com "fonte" (fonte, fonteMono, fonteDef…), em
|
|
866
|
+
qualquer profundidade: dentro de grupo, na legenda, nas peças do kit e
|
|
867
|
+
no HUD. Fonte que o plano usa e o CSS não declara sai em Times. */
|
|
829
868
|
const usadas = new Set();
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
869
|
+
const andar = (o) => {
|
|
870
|
+
if (!o || typeof o !== 'object') return;
|
|
871
|
+
if (Array.isArray(o)) return o.forEach(andar);
|
|
872
|
+
for (const [k, v] of Object.entries(o)) {
|
|
873
|
+
if (/^fonte/.test(k) && typeof v === 'string' && v.trim()) usadas.add(v.trim());
|
|
874
|
+
else if (v && typeof v === 'object') andar(v);
|
|
834
875
|
}
|
|
835
|
-
}
|
|
876
|
+
};
|
|
877
|
+
for (const cena of plano.cenas || []) andar(cena.elementos || []);
|
|
878
|
+
andar(plano.hud);
|
|
836
879
|
if (!usadas.size) return '/* nenhuma fonte externa neste vídeo */\n';
|
|
837
880
|
const familias = [...usadas]
|
|
838
|
-
|
|
881
|
+
// O Google devolve o que a família tem e ignora o resto: pedir todos os pesos e o itálico não quebra.
|
|
882
|
+
.map((f) => 'family=' + encodeURIComponent(f).replace(/%20/g, '+') + ':ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,600')
|
|
839
883
|
.join('&');
|
|
840
884
|
return `@import url('https://fonts.googleapis.com/css2?${familias}&display=block');\n`;
|
|
841
885
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ritmo.js — o tempo de um vídeo feito de partes: onde cada uma corta, quando
|
|
3
|
+
* cada coisa entra, e a música que abaixa quando a voz fala.
|
|
4
|
+
*
|
|
5
|
+
* "O áudio comanda o quadro." — guia MOTION VOX STYLE
|
|
6
|
+
*
|
|
7
|
+
* Os estilos de cena por parte (vox.js, lancamento.js) dividem isto: o corte
|
|
8
|
+
* cai no beat (um pouco antes da primeira palavra da parte), cada elemento
|
|
9
|
+
* entra no instante em que a palavra dele é dita, e o volume da trilha segue
|
|
10
|
+
* a narração. Puro: tempos entram, números saem.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { quando } = require('./cinema.js');
|
|
16
|
+
|
|
17
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Onde cada parte começa e quanto dura.
|
|
21
|
+
*
|
|
22
|
+
* Com voz, a parte começa `antes` segundos antes da primeira palavra (o corte
|
|
23
|
+
* seco no beat) e vai até a próxima; a última ganha `cauda` de respiro. Sem
|
|
24
|
+
* voz, cada parte dura o que se leva para ler o que ela mostra.
|
|
25
|
+
*
|
|
26
|
+
* @returns {{ini:number[], dur:number[], total:number, temVoz:boolean}}
|
|
27
|
+
*/
|
|
28
|
+
function tempoDasPartes(partes, tempos = [], { semVoz = () => 2.6, antes = 0.12, cauda = 1.1, minimo = 1.1 } = {}) {
|
|
29
|
+
const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
|
|
30
|
+
const ini = [];
|
|
31
|
+
if (temVoz) partes.forEach((_, i) => ini.push(i === 0 ? 0 : r2(Math.max(ini[i - 1] + minimo, tempos[i].de - antes))));
|
|
32
|
+
else partes.forEach((p, i) => ini.push(i === 0 ? 0 : r2(ini[i - 1] + Math.max(minimo, semVoz(partes[i - 1])))));
|
|
33
|
+
const total = r2(temVoz ? Math.max(ini[ini.length - 1] + minimo, tempos[tempos.length - 1].ate + cauda) : ini[ini.length - 1] + Math.max(minimo, semVoz(partes[partes.length - 1])) + 0.4);
|
|
34
|
+
const dur = ini.map((t, i) => r2((i < ini.length - 1 ? ini[i + 1] : total) - t));
|
|
35
|
+
return { ini, dur, total, temVoz };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Quando cada texto entra, no tempo DA CENA: o instante em que a primeira
|
|
40
|
+
* palavra forte dele é dita. Sem voz (ou sem achar), espalha os textos pelo
|
|
41
|
+
* começo da cena, respeitando a ordem.
|
|
42
|
+
*/
|
|
43
|
+
function gatilhos(textos, tempo, inicio, dur, { t0 = 0.3, ate = 0.62 } = {}) {
|
|
44
|
+
const palavras = (tempo && tempo.palavras) || [];
|
|
45
|
+
const fim = Math.max(t0 + 0.2, dur * ate);
|
|
46
|
+
if (!palavras.length) return textos.map((_, i) => r2(t0 + (fim - t0) * (i / Math.max(1, textos.length))));
|
|
47
|
+
const abs = quando(textos, palavras, inicio + fim, inicio + t0);
|
|
48
|
+
// Nunca antes do corte, nunca tarde demais para ser lido, sempre em ordem.
|
|
49
|
+
let antes = 0;
|
|
50
|
+
return abs.map((t) => {
|
|
51
|
+
const rel = Math.min(Math.max(0.08, t - inicio), Math.max(0.1, dur - 0.9));
|
|
52
|
+
antes = Math.max(antes + 0.1, rel);
|
|
53
|
+
return r2(antes);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** As palavras faladas numa parte, no tempo da cena: [{texto, t}]. */
|
|
58
|
+
function palavrasNaCena(tempo, inicio) {
|
|
59
|
+
return ((tempo && tempo.palavras) || []).map((w) => ({ texto: w.texto, t: r2(Math.max(0.05, w.de - inicio)) }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** O volume da trilha no tempo: cheia no respiro, baixa quando a voz fala, some no fim. */
|
|
63
|
+
function volumesDaTrilha(tempos, total, { alto = 0.22, baixo = 0.11 } = {}) {
|
|
64
|
+
const k = (t, v) => ({ t: r2(t), v });
|
|
65
|
+
const vs = [k(0, 0.05), k(0.4, alto)];
|
|
66
|
+
// Um quadro-chave de cada vez, sempre depois do anterior: fala colada na outra não volta no tempo.
|
|
67
|
+
const poe = (t, v) => vs.push(k(Math.max(vs[vs.length - 1].t + 0.01, t), v));
|
|
68
|
+
for (const tp of tempos || []) {
|
|
69
|
+
if (!tp || !(tp.ate > tp.de)) continue;
|
|
70
|
+
poe(tp.de - 0.15, alto); poe(tp.de, baixo); poe(tp.ate, baixo); poe(tp.ate + 0.35, alto);
|
|
71
|
+
}
|
|
72
|
+
poe(total - 0.6, alto * 0.9);
|
|
73
|
+
poe(total, 0);
|
|
74
|
+
return vs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { tempoDasPartes, gatilhos, palavrasNaCena, volumesDaTrilha, r2 };
|