primocode 10.13.0 → 10.14.1
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 +66 -12
- package/bin/primocode.js +148 -8
- package/lib/act.js +56 -3
- package/lib/catalogo.js +12 -1
- package/lib/config.js +1 -0
- package/lib/estudio/cota.js +157 -0
- package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
- package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
- package/lib/estudio/dados/projetos/clube-agent.json +120 -0
- package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
- package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
- package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
- package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
- package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
- package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
- package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
- package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
- package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
- package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
- package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
- package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
- package/lib/estudio/dados/projetos/clube-clube.json +108 -0
- package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
- package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
- package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
- package/lib/estudio/dados/projetos/clube-dub.json +119 -0
- package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
- package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
- package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
- package/lib/estudio/dados/projetos/clube-frame.json +108 -0
- package/lib/estudio/dados/projetos/clube-glass.json +118 -0
- package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
- package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
- package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
- package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
- package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
- package/lib/estudio/dados/projetos/clube-morph.json +119 -0
- package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
- package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
- package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
- package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
- package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
- package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
- package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
- package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
- package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
- package/lib/estudio/dados/projetos/clube-promo.json +114 -0
- package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
- package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
- package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
- package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
- package/lib/estudio/dados/projetos/clube-robo.json +125 -0
- package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
- package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
- package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
- package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
- package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
- package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
- package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
- package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
- package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
- package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
- package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
- package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
- package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
- package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
- package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
- package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
- package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
- package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
- package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
- package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
- package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
- package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
- package/lib/estudio/editor.js +290 -0
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/index.js +97 -2
- package/lib/estudio/kit-ae/base.jsx +1146 -0
- package/lib/estudio/kit-ae/cenas.jsx +395 -0
- package/lib/estudio/kit-ae/primitivas.jsx +911 -0
- package/lib/estudio/maquina-diretor.js +393 -0
- package/lib/estudio/maquina-elementos.js +134 -0
- package/lib/estudio/maquina-layout.js +198 -0
- package/lib/estudio/maquina-som.js +478 -0
- package/lib/estudio/maquina-trilha.js +160 -0
- package/lib/estudio/maquina-voz.js +429 -0
- package/lib/estudio/maquina.js +516 -0
- package/lib/estudio/projetos.js +72 -0
- package/lib/headless.js +71 -0
- package/lib/hooks.js +72 -0
- package/lib/limites.js +100 -0
- package/lib/local/cerebro.json +398 -12
- package/lib/local/entrada.js +46 -2
- package/lib/marca-kit.js +312 -0
- package/lib/mcp.js +286 -6
- package/lib/perigo.js +31 -3
- package/lib/planos.js +49 -19
- package/lib/preparar.js +111 -0
- package/lib/retomar.js +55 -0
- package/lib/sandbox.js +197 -0
- package/lib/skills.js +3 -1
- package/lib/tools.js +13 -4
- package/lib/ui.js +1 -1
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/* take.js — motor do "take único" (lousa contínua) em HTML seek(t). window.TK no navegador, require() no node.
|
|
2
|
+
*
|
|
3
|
+
* Tudo aqui é função pura do tempo: os mesmos argumentos dão sempre o mesmo número. Por isso o render pode pedir
|
|
4
|
+
* qualquer instante (e vários instantes dentro de um quadro, para o desfoque de movimento do obturador).
|
|
5
|
+
* Método estudado no onetake (05-pesquisa/referencias/2026-10-12-onetake-take-unico.md), código escrito do zero.
|
|
6
|
+
* Tutorial: 07-videos/linguagem-edicao/tutorial-take-unico.md.
|
|
7
|
+
*
|
|
8
|
+
* curvas ease.* · bezier · spring · springVel · ring · rng · spline
|
|
9
|
+
* entrada wordRise · letterDrop · popIn · typeOn · drawOn
|
|
10
|
+
* carry morphRect · iris · zoomThrough · hop · gather · carryTela (+ chain)
|
|
11
|
+
* contato impact · press · click · shake
|
|
12
|
+
* câmera operador (molas que perseguem) · flutuar · view · project · unproject · projectBox · screenTravel · lattice
|
|
13
|
+
*/
|
|
14
|
+
(function (root) {
|
|
15
|
+
'use strict';
|
|
16
|
+
const clamp = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v));
|
|
17
|
+
const lerp = (a, b, k) => a + (b - a) * k;
|
|
18
|
+
const seg = (t, t0, t1) => (t1 === t0 ? (t >= t1 ? 1 : 0) : clamp((t - t0) / (t1 - t0)));
|
|
19
|
+
|
|
20
|
+
// ── curvas ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// cubic-bezier do CSS/AE como função de u. board e saas são as curvas da casa (regras-vigentes.md).
|
|
22
|
+
function bezier(x1, y1, x2, y2) {
|
|
23
|
+
const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx, cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
|
|
24
|
+
const X = s => ((ax * s + bx) * s + cx) * s, Y = s => ((ay * s + by) * s + cy) * s, dX = s => (3 * ax * s + 2 * bx) * s + cx;
|
|
25
|
+
return u => {
|
|
26
|
+
if (u <= 0) return 0; if (u >= 1) return 1;
|
|
27
|
+
let s = u;
|
|
28
|
+
for (let i = 0; i < 8; i++) { const e = X(s) - u, d = dX(s); if (Math.abs(e) < 1e-7) return Y(s); if (Math.abs(d) < 1e-6) break; s -= e / d; }
|
|
29
|
+
let lo = 0, hi = 1; s = u;
|
|
30
|
+
for (let i = 0; i < 50; i++) { const v = X(s); if (Math.abs(v - u) < 1e-7) break; if (v < u) lo = s; else hi = s; s = (lo + hi) / 2; }
|
|
31
|
+
return Y(s);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const pOut = n => u => 1 - Math.pow(1 - u, n);
|
|
35
|
+
const pInOut = n => u => (u < 0.5 ? Math.pow(2, n - 1) * Math.pow(u, n) : 1 - Math.pow(2 - 2 * u, n) / 2);
|
|
36
|
+
const expoIn = u => (u <= 0 ? 0 : (Math.pow(2, 10 * u) - 1) / 1023);
|
|
37
|
+
// t80 entre parênteses: fração do tempo até 80 % do caminho. Baixo = estalo que assenta; ~.7 = S mole.
|
|
38
|
+
const ease = {
|
|
39
|
+
linear: u => u,
|
|
40
|
+
smooth: u => u * u * (3 - 2 * u), // .71
|
|
41
|
+
sineInOut: u => (1 - Math.cos(Math.PI * u)) / 2, // câmera que acompanha
|
|
42
|
+
cubicInOut: pInOut(3), quintInOut: pInOut(5),
|
|
43
|
+
cubicOut: pOut(3), quartOut: pOut(4), quintOut: pOut(5), // .42 · .33 · .28
|
|
44
|
+
expoOut: u => (u >= 1 ? 1 : (1 - Math.pow(2, -10 * u)) / (1 - Math.pow(2, -10))), // .23 estalo
|
|
45
|
+
expoInOut: u => (u < 0.5 ? expoIn(2 * u) / 2 : 1 - expoIn(2 - 2 * u) / 2), // chicote
|
|
46
|
+
board: bezier(0.333, 0, 0.2285, 1), // viagem da lousa (Shelby v3)
|
|
47
|
+
saas: bezier(0.86, 0, 0.07, 1), // troca de cena SaaS
|
|
48
|
+
};
|
|
49
|
+
const tween = (t, t0, t1, c = ease.expoOut) => c(seg(t, t0, t1));
|
|
50
|
+
|
|
51
|
+
// Mola amortecida 0 → 1 em forma fechada. Padrão da casa ζ .56 ω 12 (~112 %, assenta em ~.6 s).
|
|
52
|
+
function spring(tau, zeta = 0.56, omega = 12) {
|
|
53
|
+
if (tau <= 0) return 0;
|
|
54
|
+
if (zeta < 1) { const wd = omega * Math.sqrt(1 - zeta * zeta); return 1 - Math.exp(-zeta * omega * tau) * (Math.cos(wd * tau) + (zeta * omega / wd) * Math.sin(wd * tau)); }
|
|
55
|
+
return 1 - Math.exp(-omega * tau) * (1 + omega * tau);
|
|
56
|
+
}
|
|
57
|
+
function springVel(tau, zeta = 0.56, omega = 12) {
|
|
58
|
+
if (tau <= 0) return 0;
|
|
59
|
+
if (zeta < 1) { const s = Math.sqrt(1 - zeta * zeta); return (omega / s) * Math.exp(-zeta * omega * tau) * Math.sin(omega * s * tau); }
|
|
60
|
+
return omega * omega * tau * Math.exp(-omega * tau);
|
|
61
|
+
}
|
|
62
|
+
const ring = (tau, decay = 9, omega = 26) => (tau <= 0 ? 0 : Math.exp(-decay * tau) * Math.sin(omega * tau));
|
|
63
|
+
function rng(seed = 1) { let s = (seed >>> 0) || 1; return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return (s >>> 0) / 4294967296; }; }
|
|
64
|
+
// Hermite por nós {t, x, y}: o caminho da mão / do ponto.
|
|
65
|
+
function spline(K, t) {
|
|
66
|
+
if (t <= K[0].t) return [K[0].x, K[0].y];
|
|
67
|
+
const L = K.length; if (t >= K[L - 1].t) return [K[L - 1].x, K[L - 1].y];
|
|
68
|
+
let i = 0; while (K[i + 1].t < t) i++;
|
|
69
|
+
const a = K[i], b = K[i + 1], h = b.t - a.t, u = (t - a.t) / h, p = K[Math.max(0, i - 1)], n = K[Math.min(L - 1, i + 2)];
|
|
70
|
+
const m0 = [(b.x - p.x) / (b.t - p.t) * h, (b.y - p.y) / (b.t - p.t) * h], m1 = [(n.x - a.x) / (n.t - a.t) * h, (n.y - a.y) / (n.t - a.t) * h];
|
|
71
|
+
const h00 = 2 * u ** 3 - 3 * u ** 2 + 1, h10 = u ** 3 - 2 * u ** 2 + u, h01 = -2 * u ** 3 + 3 * u ** 2, h11 = u ** 3 - u ** 2;
|
|
72
|
+
return [h00 * a.x + h10 * m0[0] + h01 * b.x + h11 * m1[0], h00 * a.y + h10 * m0[1] + h01 * b.y + h11 * m1[1]];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── entrada ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// Palavra que pousa: mola com subida e escala; a opacidade vai na frente da mola para nunca ler como fade.
|
|
77
|
+
function wordRise(t, t0, { zeta = 0.62, omega = 20, dist = 28, s0 = 0.92, fade = 1.6 } = {}) {
|
|
78
|
+
const k = spring(t - t0, zeta, omega);
|
|
79
|
+
return { k, op: clamp(k * fade), y: (1 - k) * dist, s: s0 + (1 - s0) * k };
|
|
80
|
+
}
|
|
81
|
+
// Letra que cai e achata pela velocidade (squash), escalonada.
|
|
82
|
+
function letterDrop(t, t0, i, { stagger = 0.05, zeta = 0.5, omega = 24, dist = 60, squash = 0.006, max = 0.22 } = {}) {
|
|
83
|
+
const tau = t - t0 - i * stagger, k = spring(tau, zeta, omega), sq = clamp(Math.abs(springVel(tau, zeta, omega)) * squash, 0, max);
|
|
84
|
+
return { k, op: clamp(k * 1.6), y: (1 - k) * -dist, sx: 1 + sq * 0.6, sy: 1 - sq };
|
|
85
|
+
}
|
|
86
|
+
function popIn(t, t0, { zeta = 0.62, omega = 16, rise = 34, s0 = 0.9, fade = 1.5 } = {}) {
|
|
87
|
+
const k = spring(t - t0, zeta, omega);
|
|
88
|
+
return { k, op: clamp(k * fade), s: s0 + (1 - s0) * k, dy: (1 - k) * rise };
|
|
89
|
+
}
|
|
90
|
+
function typeOn(t, t0, txt, { cps = 30 } = {}) { const n = clamp(Math.floor((t - t0) * cps), 0, txt.length); return { n, str: txt.slice(0, n), done: n >= txt.length }; }
|
|
91
|
+
// Traço desenhado (SVG stroke-dashoffset): 0 → 1 do comprimento.
|
|
92
|
+
const drawOn = (t, t0, t1, c = ease.cubicInOut) => c(seg(t, t0, t1));
|
|
93
|
+
|
|
94
|
+
// ── carry: a próxima cena nasce desta ─────────────────────────────────────────────────────────────────────
|
|
95
|
+
// Um retângulo vira outro (botão → janela, foto → pílula). Com spring: {zeta, omega}.
|
|
96
|
+
function morphRect(t, t0, t1, A, B, { curve = ease.expoOut, spring: sp = null } = {}) {
|
|
97
|
+
const k = sp ? spring(t - t0, sp.zeta ?? 0.7, sp.omega ?? 14) : curve(seg(t, t0, t1));
|
|
98
|
+
const x = lerp(A.x, B.x, k), y = lerp(A.y, B.y, k), w = lerp(A.w, B.w, k), h = lerp(A.h, B.h, k);
|
|
99
|
+
return { k, x, y, w, h, r: lerp(A.r ?? 0, B.r ?? 0, k) };
|
|
100
|
+
}
|
|
101
|
+
// Círculo que abre do assunto até cobrir o quadro. Regra da casa: inOut cúbico de .3–.4 s, borda suave.
|
|
102
|
+
function iris(t, t0, t1, cx, cy, { W = 1080, H = 1920, curve = ease.cubicInOut, r0 = 0 } = {}) {
|
|
103
|
+
const k = curve(seg(t, t0, t1)), far = Math.max(Math.hypot(cx, cy), Math.hypot(W - cx, cy), Math.hypot(cx, H - cy), Math.hypot(W - cx, H - cy)) + 4;
|
|
104
|
+
return { k, r: lerp(r0, far, k) };
|
|
105
|
+
}
|
|
106
|
+
// Mergulho em escala log até o alvo (mundo) encher a tela; devolve a câmera {x, y, zoom} que faz isso.
|
|
107
|
+
function zoomThrough(t, t0, t1, from, target, { W = 1080, H = 1920, curve = ease.cubicInOut, fit = 'cover' } = {}) {
|
|
108
|
+
const k = curve(seg(t, t0, t1)), zEnd = fit === 'cover' ? Math.max(W / target.w, H / target.h) : Math.min(W / target.w, H / target.h);
|
|
109
|
+
return { k, x: lerp(from.x, target.x + target.w / 2, k), y: lerp(from.y, target.y + target.h / 2, k), zoom: Math.exp(lerp(Math.log(from.zoom), Math.log(zEnd), k)) };
|
|
110
|
+
}
|
|
111
|
+
// Pulo em arco de um lugar ao outro (pouse com impact).
|
|
112
|
+
function hop(t, t0, t1, a, b, { height = 160, curve = ease.smooth } = {}) {
|
|
113
|
+
const k = curve(seg(t, t0, t1));
|
|
114
|
+
return { k, x: lerp(a.x, b.x, k), y: lerp(a.y, b.y, k) - Math.sin(k * Math.PI) * height };
|
|
115
|
+
}
|
|
116
|
+
// Item i de n viaja em arco até o lugar novo, em leque e escalonado.
|
|
117
|
+
function gather(t, t0, t1, i, n, a, b, { stagger = 0.06, arc = 90, spin = 0.25, curve = ease.cubicInOut } = {}) {
|
|
118
|
+
const k = curve(seg(t, t0 + i * stagger, t1 + i * stagger)), o = i - (n - 1) / 2, bump = Math.sin(k * Math.PI);
|
|
119
|
+
return { k, x: lerp(a.x, b.x, k), y: lerp(a.y, b.y, k) - bump * arc, rot: bump * o * spin };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Vários movimentos de um mesmo valor em sequência: passos [[t0, dur, v1, curva?], …] (da clínica, 13/10).
|
|
123
|
+
function chain(t, v0, passos) { let v = v0; for (const [t0, d, v1, c] of passos) { if (t < t0) break; v = lerp(v, v1, (c || ease.cubicInOut)(seg(t, t0, t0 + d))); } return v; }
|
|
124
|
+
// Carry em tela: A (mundo, visto pela câmera em t0) → B (mundo, visto em t1), interpolado NA TELA e devolvido ao mundo
|
|
125
|
+
// pela câmera de agora. O que é carregado nunca fica para trás nem sai do quadro durante um chicote (clínica, 13/10).
|
|
126
|
+
// camAt(t) → {x, y, zoom, rot}; arco em px de tela (bump para cima).
|
|
127
|
+
function carryTela(t, t0, t1, A, B, camAt, { curve = ease.cubicInOut, arco = 0, W = 1080, H = 1920 } = {}) {
|
|
128
|
+
const k = curve(seg(t, t0, t1)), sa = project(view(camAt(t0), { W, H }), A.x, A.y), sb = project(view(camAt(t1), { W, H }), B.x, B.y);
|
|
129
|
+
const [x, y] = unproject(view(camAt(t), { W, H }), lerp(sa[0], sb[0], k), lerp(sa[1], sb[1], k) - arco * Math.sin(Math.PI * clamp(k)));
|
|
130
|
+
return { x, y, k };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── contato ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
function impact(t, tHit, { decay = 7, omega = 22, sqx = 0.12, sqy = 0.15 } = {}) { const r = ring(t - tHit, decay, omega); return { r, sx: 1 + r * sqx, sy: 1 - r * sqy }; }
|
|
135
|
+
function press(t, tc, { depth = 0.14, decay = 10, omega = 30 } = {}) { const d = t - tc, sq = d > 0 ? ring(d, decay, omega) * depth : 0; return { s: 1 - Math.abs(sq), down: d > 0 }; }
|
|
136
|
+
// Clique (medido no .aep do Click VFX da MahdiEdits, 05-pesquisa/referencias/2026-10-12-click-vfx-mahdiedits.md):
|
|
137
|
+
// o aperto começa em tc; cursor 1 → .636 em `down` s e volta em `up` s (Easy Ease); botão 1 → .875 no mesmo tempo.
|
|
138
|
+
// Anel pequeno de apoio (cliques em rajada): r0 → r1 em `dur`. Para o anel grande use anelClique().
|
|
139
|
+
const easyEase = bezier(1 / 3, 0, 2 / 3, 1);
|
|
140
|
+
function click(t, tc, { dip = 0.636, btn = 0.875, down = 0.233, up = 0.233, r0 = 12, r1 = 120, dur = 0.5 } = {}) {
|
|
141
|
+
const d = t - tc;
|
|
142
|
+
const k = d < 0 ? 0 : d < down ? easyEase(d / down) : 1 - easyEase(seg(d, down, down + up));
|
|
143
|
+
const u = seg(d, 0, dur);
|
|
144
|
+
return { s: 1 - (1 - dip) * k, btn: 1 - (1 - btn) * k, ring: d >= 0 && u < 1 ? { r: lerp(r0, r1, ease.quartOut(u)), op: (1 - u) * (1 - u), w: lerp(6, 1.5, u) } : null };
|
|
145
|
+
}
|
|
146
|
+
// Anel grande do clique: nasce no aperto e cresce até sair do quadro, sem fade (some porque sai da tela).
|
|
147
|
+
// Raio = r1 · bezier(.145, 0, 0, 1)(d / 2,883), r1 = 1,17 × a distância até o canto mais longe.
|
|
148
|
+
const anelCurva = bezier(0.145, 0, 0, 1);
|
|
149
|
+
function anelClique(t, tc, x, y, { W = 1080, H = 1920, dur = 2.883 } = {}) {
|
|
150
|
+
const d = t - tc; if (d < 0 || d > dur) return null;
|
|
151
|
+
const far = Math.max(Math.hypot(x, y), Math.hypot(W - x, y), Math.hypot(x, H - y), Math.hypot(W - x, H - y));
|
|
152
|
+
return { r: 1.17 * far * anelCurva(d / dur), k: d / dur };
|
|
153
|
+
}
|
|
154
|
+
// Desenha o anel visto através de uma grade de pontos, com glow em tela (screen). k = escala do original (W/2357).
|
|
155
|
+
const _fx = {};
|
|
156
|
+
function desenhaAnel(ctx, x, y, r, { cor = '#C8102E', k = 1080 / 2357, glow = 0.59 } = {}) {
|
|
157
|
+
if (!r || r < 1) return;
|
|
158
|
+
const Wc = ctx.canvas.width, Hc = ctx.canvas.height;
|
|
159
|
+
if (!_fx.off || _fx.off.width !== Wc) {
|
|
160
|
+
_fx.off = document.createElement('canvas'); _fx.off.width = Wc; _fx.off.height = Hc;
|
|
161
|
+
const p = document.createElement('canvas'), pas = Math.max(4, Math.round(21.6 * k)); p.width = p.height = pas;
|
|
162
|
+
const pg = p.getContext('2d'); pg.fillStyle = '#fff'; pg.beginPath(); pg.arc(pas / 2, pas / 2, 10.9 * k / 2, 0, 6.2832); pg.fill();
|
|
163
|
+
_fx.pat = p;
|
|
164
|
+
}
|
|
165
|
+
const o = _fx.off.getContext('2d');
|
|
166
|
+
o.setTransform(1, 0, 0, 1, 0, 0); o.globalCompositeOperation = 'source-over'; o.clearRect(0, 0, Wc, Hc);
|
|
167
|
+
o.filter = `blur(${(40 * k).toFixed(1)}px)`; o.strokeStyle = cor; o.lineWidth = 103 * k;
|
|
168
|
+
o.beginPath(); o.arc(x, y, r, 0, 6.2832); o.stroke(); o.filter = 'none';
|
|
169
|
+
o.globalCompositeOperation = 'destination-in'; o.fillStyle = o.createPattern(_fx.pat, 'repeat'); o.fillRect(0, 0, Wc, Hc);
|
|
170
|
+
ctx.save(); ctx.drawImage(_fx.off, 0, 0);
|
|
171
|
+
ctx.globalCompositeOperation = 'screen'; ctx.globalAlpha = glow;
|
|
172
|
+
ctx.filter = `blur(${(40 * k).toFixed(1)}px)`; ctx.drawImage(_fx.off, 0, 0);
|
|
173
|
+
ctx.filter = `blur(${(120 * k).toFixed(1)}px)`; ctx.drawImage(_fx.off, 0, 0);
|
|
174
|
+
ctx.restore();
|
|
175
|
+
}
|
|
176
|
+
// Tremor que decai depois de um pouso; zero antes do golpe; determinístico.
|
|
177
|
+
function shake(t, tHit, { amp = 8, decay = 9, freq = 17, seed = 1 } = {}) {
|
|
178
|
+
const tau = t - tHit; if (tau <= 0) return { x: 0, y: 0, rot: 0 };
|
|
179
|
+
const r = rng(seed), fx = freq * (1 + 0.1 * r()), fy = freq * (1.31 + 0.1 * r()), e = amp * Math.exp(-decay * tau), w = 2 * Math.PI * tau;
|
|
180
|
+
return { x: e * Math.sin(w * fx), y: 0.7 * e * Math.sin(w * fy), rot: 0.0012 * e * Math.sin(w * freq * 0.77) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── câmera ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
// Operador: want(t) diz o que enquadrar {x, y, zoom, w}; uma mola por eixo persegue (posição ζ .82, zoom em log
|
|
185
|
+
// ζ .95 a .8 w), integrada uma vez a 240 Hz e amostrada. Assim ela atrasa, inclina e assenta, e cada quadro
|
|
186
|
+
// continua função de t. Saída quantizada (1/16 px, 0,05 % de zoom): câmera assentada repete o quadro exato.
|
|
187
|
+
// Estalo mais rápido que a mola: mire o pouso ~.15 s antes.
|
|
188
|
+
function operador(want, { dur, hz = 240, zeta = 0.82, zetaZ = 0.95 } = {}) {
|
|
189
|
+
const n = Math.ceil(dur * hz) + 2, X = new Float64Array(n), Y = new Float64Array(n), Z = new Float64Array(n), dt = 1 / hz;
|
|
190
|
+
const w0 = want(0); let x = w0.x, y = w0.y, lz = Math.log(w0.zoom), vx = 0, vy = 0, vz = 0;
|
|
191
|
+
for (let i = 0; i < n; i++) {
|
|
192
|
+
const g = want(i * dt), w = g.w ?? 8, wz = 0.8 * w;
|
|
193
|
+
vx += (-2 * zeta * w * vx - w * w * (x - g.x)) * dt; vy += (-2 * zeta * w * vy - w * w * (y - g.y)) * dt;
|
|
194
|
+
vz += (-2 * zetaZ * wz * vz - wz * wz * (lz - Math.log(g.zoom))) * dt;
|
|
195
|
+
x += vx * dt; y += vy * dt; lz += vz * dt;
|
|
196
|
+
X[i] = Math.round(x * 16) / 16; Y[i] = Math.round(y * 16) / 16; Z[i] = Math.round(Math.exp(lz) * 2000) / 2000;
|
|
197
|
+
}
|
|
198
|
+
return { at(t) { const f = clamp(t * hz, 0, n - 1), i = Math.floor(f), j = Math.min(n - 1, i + 1), u = f - i; return { x: lerp(X[i], X[j], u), y: lerp(Y[i], Y[j], u), zoom: lerp(Z[i], Z[j], u), rot: 0 }; } };
|
|
199
|
+
}
|
|
200
|
+
// Alvo em degraus: a cada chave {t, x, y, zoom, w} o operador passa a perseguir aquele enquadramento.
|
|
201
|
+
function alvos(keys) { return t => { let k = keys[0]; for (const c of keys) if (c.t <= t) k = c; return k; }; }
|
|
202
|
+
// Mão do operador: duas senoides lentas. amp em px de tela; env(t) 0..1 baixa nos respiros (nunca zero: regra da casa).
|
|
203
|
+
function flutuar(t, amp = 4) { return { x: amp * (Math.sin(1.7 * t) + 0.6 * Math.sin(2.9 * t + 1)), y: amp * (Math.sin(1.3 * t + 2) + 0.5 * Math.sin(3.3 * t)) }; }
|
|
204
|
+
// Matriz [a, b, c, d, e, f] de uma camada a `depth` vista pela câmera: CSS matrix(...) com transform-origin 0 0.
|
|
205
|
+
function view(cam, { W = 1080, H = 1920, depth = 0 } = {}) {
|
|
206
|
+
const f = 1 / (1 + Math.max(-0.9, depth)), cx = W / 2 + (cam.x - W / 2) * f, cy = H / 2 + (cam.y - H / 2) * f;
|
|
207
|
+
const z = Math.exp(Math.log(cam.zoom) * f), c = Math.cos((cam.rot || 0) * f) * z, s = Math.sin((cam.rot || 0) * f) * z;
|
|
208
|
+
return [c, s, -s, c, W / 2 - (c * cx - s * cy), H / 2 - (s * cx + c * cy)];
|
|
209
|
+
}
|
|
210
|
+
const project = (M, x, y) => [M[0] * x + M[2] * y + M[4], M[1] * x + M[3] * y + M[5]];
|
|
211
|
+
const unproject = (M, x, y) => { const d = M[0] * M[3] - M[1] * M[2], u = x - M[4], v = y - M[5]; return [(M[3] * u - M[2] * v) / d, (M[0] * v - M[1] * u) / d]; };
|
|
212
|
+
function projectBox(M, b) {
|
|
213
|
+
const P = [[b.x, b.y], [b.x + b.w, b.y], [b.x, b.y + b.h], [b.x + b.w, b.y + b.h]].map(p => project(M, p[0], p[1]));
|
|
214
|
+
const xs = P.map(p => p[0]), ys = P.map(p => p[1]), x0 = Math.min(...xs), y0 = Math.min(...ys);
|
|
215
|
+
return { x: x0, y: y0, w: Math.max(...xs) - x0, h: Math.max(...ys) - y0 };
|
|
216
|
+
}
|
|
217
|
+
// Quanto os cantos da tela andam entre duas câmeras: a parte da câmera no __motion (que dimensiona o obturador).
|
|
218
|
+
function screenTravel(c0, c1, { W = 1080, H = 1920 } = {}) {
|
|
219
|
+
const A = view(c0, { W, H }), B = view(c1, { W, H }); let d = 0;
|
|
220
|
+
for (const [sx, sy] of [[0, 0], [W, 0], [0, H], [W, H], [W / 2, H / 2]]) { const [wx, wy] = unproject(A, sx, sy), p = project(B, wx, wy); d = Math.max(d, Math.hypot(p[0] - sx, p[1] - sy)); }
|
|
221
|
+
return d;
|
|
222
|
+
}
|
|
223
|
+
// Grade de pontos distante: algo para o movimento deslizar contra. Pontos com `px` constante na tela.
|
|
224
|
+
function lattice(cam, { W = 1080, H = 1920, depth = 1.2, spacing = 44, px = 1.5 } = {}) {
|
|
225
|
+
const M = view(cam, { W, H, depth }), c = [[0, 0], [W, 0], [0, H], [W, H]].map(p => unproject(M, p[0], p[1]));
|
|
226
|
+
const xs = c.map(p => p[0]), ys = c.map(p => p[1]);
|
|
227
|
+
return { M, spacing, r: px / Math.hypot(M[0], M[1]), x0: Math.floor(Math.min(...xs) / spacing) * spacing, x1: Math.max(...xs), y0: Math.floor(Math.min(...ys) / spacing) * spacing, y1: Math.max(...ys) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const TK = { clamp, lerp, seg, bezier, ease, tween, chain, carryTela, spring, springVel, ring, rng, spline,
|
|
231
|
+
wordRise, letterDrop, popIn, typeOn, drawOn, morphRect, iris, zoomThrough, hop, gather,
|
|
232
|
+
impact, press, click, anelClique, desenhaAnel, shake, operador, alvos, flutuar, view, project, unproject, projectBox, screenTravel, lattice };
|
|
233
|
+
root.TK = TK;
|
|
234
|
+
if (typeof module === 'object' && module.exports) module.exports = TK;
|
|
235
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* editor.js — o editor de vídeo por comando: `estudio_editar` reabre o plano que a máquina de motion usou
|
|
3
|
+
* (cenas.json, salvo junto do vídeo), aplica as mudanças, re-renderiza e guarda a versão (v1, v2…).
|
|
4
|
+
*
|
|
5
|
+
* Operações (cada mudança é {op, ...}; o modelo traduz o pedido em linguagem natural):
|
|
6
|
+
* texto {cena, campo?='titulo', valor} · texto {de, para} cor {fundo?, destaque?, tinta?} · cor {paleta}
|
|
7
|
+
* fonte {titulo?, texto?, destaque?} ritmo {fator} (0.8 = mais rápido) · duracao {cena, segundos} · duracao {total}
|
|
8
|
+
* remover {cena} · mover {cena, para} · duplicar {cena, para?} (cena = 1, 2, 3…)
|
|
9
|
+
* trilha {valor: false | "busca" | caminho} volume {alvo: voz|trilha|sfx, db} formato {valor: 9:16|1:1|16:9}
|
|
10
|
+
* Com narração, o tempo é da voz: ritmo, duração, remover, mover e duplicar ficam de fora (avisa).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const maq = () => require('./maquina.js');
|
|
19
|
+
const dir = () => require('./maquina-diretor.js');
|
|
20
|
+
const ler = (f) => JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
21
|
+
const gravar = (f, o) => { fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f, JSON.stringify(o)); };
|
|
22
|
+
const ARQ = (pasta) => path.join(pasta, 'cenas.json');
|
|
23
|
+
const IDX = (pasta) => path.join(pasta, 'versoes', 'indice.json');
|
|
24
|
+
const arred = (v, n = 100) => Math.round(v * n) / n;
|
|
25
|
+
|
|
26
|
+
// ── versões ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function indice(pasta) {
|
|
29
|
+
try { return ler(IDX(pasta)); } catch { return { atual: 0, versoes: [] }; }
|
|
30
|
+
}
|
|
31
|
+
function registrarVersao(pasta, spec, video, resumo) {
|
|
32
|
+
const ix = indice(pasta);
|
|
33
|
+
const n = ix.versoes.reduce((m, v) => Math.max(m, v.n), 0) + 1;
|
|
34
|
+
const d = path.join(pasta, 'versoes', 'v' + n);
|
|
35
|
+
fs.mkdirSync(d, { recursive: true });
|
|
36
|
+
gravar(path.join(d, 'cenas.json'), spec);
|
|
37
|
+
let arq = null;
|
|
38
|
+
if (video && fs.existsSync(video)) { arq = path.join(d, 'video.mp4'); fs.copyFileSync(video, arq); }
|
|
39
|
+
ix.versoes.push({ n, resumo: resumo || '', previa: !!spec.previa, video: arq ? path.relative(pasta, arq) : null });
|
|
40
|
+
ix.atual = n;
|
|
41
|
+
gravar(IDX(pasta), ix);
|
|
42
|
+
gravar(ARQ(pasta), spec);
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Gancho da máquina: guarda o plano e abre a versão. Prévia só guarda o plano (sem versão). */
|
|
47
|
+
function salvarSpec(pasta, { pl, args = {}, trilha = null, voz = null, video, previa = false }) {
|
|
48
|
+
const { saida, ...resto } = args;
|
|
49
|
+
const spec = { pl, args: resto, trilha, voz, volumes: {}, previa: !!previa };
|
|
50
|
+
if (previa) {
|
|
51
|
+
gravar(ARQ(pasta), spec);
|
|
52
|
+
let v = video;
|
|
53
|
+
if (video && fs.existsSync(video)) { v = path.join(pasta, 'video-previa.mp4'); if (v !== video) fs.copyFileSync(video, v); }
|
|
54
|
+
return { video: v, versao: null };
|
|
55
|
+
}
|
|
56
|
+
fs.rmSync(path.join(pasta, 'versoes'), { recursive: true, force: true });
|
|
57
|
+
return { versao: registrarVersao(pasta, spec, video, 'original'), video };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── operações sobre o plano ──────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function refluir(pl) {
|
|
63
|
+
let t = 0;
|
|
64
|
+
pl.cenas.forEach((c, i) => { c.inicio = arred(t, 1000); t += c.dur; if (pl.cenas[i + 1]) c.proxBg = null; });
|
|
65
|
+
pl.cenas.forEach((c, i) => { if (pl.cenas[i + 1]) c.proxBg = pl.cenas[i + 1].pal.bg; else delete c.proxBg; });
|
|
66
|
+
pl.duracao = arred(t);
|
|
67
|
+
}
|
|
68
|
+
const idx = (pl, n) => { const i = Number(n) - 1; return Number.isInteger(i) && i >= 0 && i < pl.cenas.length ? i : -1; };
|
|
69
|
+
const limpo = (s) => String(s == null ? '' : s).replace(/[\r\n]+/g, ' ').trim().slice(0, 200);
|
|
70
|
+
const hexOk = (h) => /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.test(String(h || ''));
|
|
71
|
+
const hex = (h) => (String(h).startsWith('#') ? String(h) : '#' + h);
|
|
72
|
+
|
|
73
|
+
function recolorir(pl, paleta) {
|
|
74
|
+
const velho = pl.variacoes || {};
|
|
75
|
+
const novo = maq().variacoes(paleta);
|
|
76
|
+
pl.cenas.forEach((c) => {
|
|
77
|
+
const chave = ['normal', 'fundo', 'destaque'].find((k) => velho[k] && velho[k].bg === (c.pal && c.pal.bg)) || 'normal';
|
|
78
|
+
c.pal = { ...novo[chave] };
|
|
79
|
+
});
|
|
80
|
+
pl.paleta = paleta; pl.variacoes = novo;
|
|
81
|
+
if (pl.legenda) {
|
|
82
|
+
const ac = paleta.destaque, C = maq().contraste;
|
|
83
|
+
pl.legenda.corDestaque = C(ac, '#000000') >= 4.5 ? ac : (C(paleta.tinta, '#000000') >= 4.5 ? paleta.tinta : '#FFD54A');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ESTRUTURAIS = new Set(['ritmo', 'duracao', 'remover', 'mover', 'duplicar']);
|
|
88
|
+
|
|
89
|
+
/** Aplica uma mudança ao spec. Devolve {ok, resumo} ou {ok:false, error}. */
|
|
90
|
+
function aplicar(spec, m, ctx = {}) {
|
|
91
|
+
const pl = spec.pl;
|
|
92
|
+
const op = String(m && m.op || '').toLowerCase().replace(/-/g, '_');
|
|
93
|
+
if (spec.voz && ESTRUTURAIS.has(op)) return { ok: false, error: `com narração o tempo é da voz: "${op}" não se aplica (gere de novo sem voz, ou mude só texto, cor, fonte, trilha e volume).` };
|
|
94
|
+
const falta = (n) => ({ ok: false, error: `não existe a cena ${n} (o vídeo tem ${pl.cenas.length}).` });
|
|
95
|
+
switch (op) {
|
|
96
|
+
case 'texto': {
|
|
97
|
+
if (m.de != null) {
|
|
98
|
+
let n = 0;
|
|
99
|
+
const tr = (o) => { for (const k of Object.keys(o)) { if (typeof o[k] === 'string' && o[k].includes(m.de)) { o[k] = o[k].split(m.de).join(String(m.para == null ? '' : m.para)); n++; } else if (Array.isArray(o[k])) o[k] = o[k].map((x) => (typeof x === 'string' && x.includes(m.de) ? (n++, x.split(m.de).join(String(m.para == null ? '' : m.para))) : x)); } };
|
|
100
|
+
pl.cenas.forEach((c) => tr(c));
|
|
101
|
+
return n ? { ok: true, resumo: `"${m.de}" → "${m.para}" em ${n} campo(s)` } : { ok: false, error: `não achei "${m.de}" no texto das cenas.` };
|
|
102
|
+
}
|
|
103
|
+
const i = idx(pl, m.cena); if (i < 0) return falta(m.cena);
|
|
104
|
+
const campo = String(m.campo || 'titulo');
|
|
105
|
+
if (!/^(titulo|sub|texto|autor|botao|rotulo|valor|antes|depois|tela|fala)$/.test(campo) && !(campo in pl.cenas[i])) return { ok: false, error: 'campo desconhecido: ' + campo };
|
|
106
|
+
pl.cenas[i][campo] = Array.isArray(m.valor) ? m.valor.map(limpo) : limpo(m.valor);
|
|
107
|
+
return { ok: true, resumo: `cena ${m.cena}: ${campo} = "${String(m.valor).slice(0, 40)}"` };
|
|
108
|
+
}
|
|
109
|
+
case 'cor': case 'paleta': {
|
|
110
|
+
let p = { ...pl.paleta };
|
|
111
|
+
if (m.paleta || (m.valor && !m.fundo && !m.destaque && !m.tinta)) {
|
|
112
|
+
const nome = String(m.paleta || m.valor).toLowerCase();
|
|
113
|
+
const base = dir().PALETAS[nome];
|
|
114
|
+
if (!base) return { ok: false, error: 'paleta desconhecida: ' + nome + ' (' + Object.keys(dir().PALETAS).join(', ') + ')' };
|
|
115
|
+
p = { fundo: base.fundo, tinta: base.tinta, destaque: base.destaque };
|
|
116
|
+
if (pl.direcao) pl.direcao.paleta = nome;
|
|
117
|
+
}
|
|
118
|
+
for (const k of ['fundo', 'tinta', 'destaque']) if (m[k] != null) { if (!hexOk(m[k])) return { ok: false, error: `cor inválida em ${k}: ${m[k]}` }; p[k] = hex(m[k]); }
|
|
119
|
+
recolorir(pl, p);
|
|
120
|
+
return { ok: true, resumo: `cores: fundo ${p.fundo}, tinta ${p.tinta}, destaque ${p.destaque}` };
|
|
121
|
+
}
|
|
122
|
+
case 'fonte': {
|
|
123
|
+
let n = 0;
|
|
124
|
+
for (const k of ['titulo', 'texto', 'destaque']) if (m[k]) { pl.fontes[k] = limpo(m[k]).slice(0, 40); n++; }
|
|
125
|
+
if (m.valor && !n) { pl.fontes.titulo = pl.fontes.texto = pl.fontes.destaque = limpo(m.valor).slice(0, 40); n = 1; }
|
|
126
|
+
if (pl.legenda) { pl.legenda.fonte = pl.fontes.texto; pl.legenda.fonteDestaque = pl.fontes.destaque; }
|
|
127
|
+
return n ? { ok: true, resumo: 'fontes: ' + JSON.stringify(pl.fontes) } : { ok: false, error: 'fonte pede titulo, texto ou destaque.' };
|
|
128
|
+
}
|
|
129
|
+
case 'ritmo': {
|
|
130
|
+
const f = Number(m.fator);
|
|
131
|
+
if (!(f > 0.2 && f < 5)) return { ok: false, error: 'ritmo pede fator entre 0.2 e 5 (0.8 = 20% mais rápido).' };
|
|
132
|
+
pl.cenas.forEach((c) => { c.dur = arred(Math.max(1.2, Math.min(7.5, c.dur * f))); });
|
|
133
|
+
refluir(pl);
|
|
134
|
+
return { ok: true, resumo: `ritmo ×${f}: ${pl.duracao}s` };
|
|
135
|
+
}
|
|
136
|
+
case 'duracao': {
|
|
137
|
+
if (m.total != null) {
|
|
138
|
+
const k = Number(m.total) / pl.duracao;
|
|
139
|
+
if (!(k > 0.2 && k < 5)) return { ok: false, error: 'duração total fora do limite.' };
|
|
140
|
+
pl.cenas.forEach((c) => { c.dur = arred(Math.max(1.2, Math.min(7.5, c.dur * k))); });
|
|
141
|
+
} else {
|
|
142
|
+
const i = idx(pl, m.cena); if (i < 0) return falta(m.cena);
|
|
143
|
+
const s = Number(m.segundos); if (!(s >= 1 && s <= 10)) return { ok: false, error: 'segundos entre 1 e 10.' };
|
|
144
|
+
pl.cenas[i].dur = arred(s);
|
|
145
|
+
}
|
|
146
|
+
refluir(pl);
|
|
147
|
+
return { ok: true, resumo: `duração: ${pl.duracao}s` };
|
|
148
|
+
}
|
|
149
|
+
case 'remover': {
|
|
150
|
+
const i = idx(pl, m.cena); if (i < 0) return falta(m.cena);
|
|
151
|
+
if (pl.cenas.length < 2) return { ok: false, error: 'o vídeo precisa de ao menos 1 cena.' };
|
|
152
|
+
pl.cenas.splice(i, 1); pl.trocas.splice(Math.max(0, i - 1), 1);
|
|
153
|
+
refluir(pl);
|
|
154
|
+
return { ok: true, resumo: `cena ${m.cena} removida (${pl.cenas.length} cenas, ${pl.duracao}s)` };
|
|
155
|
+
}
|
|
156
|
+
case 'mover': {
|
|
157
|
+
const i = idx(pl, m.cena), j = idx(pl, m.para);
|
|
158
|
+
if (i < 0) return falta(m.cena); if (j < 0) return falta(m.para);
|
|
159
|
+
const [c] = pl.cenas.splice(i, 1); pl.cenas.splice(j, 0, c);
|
|
160
|
+
refluir(pl);
|
|
161
|
+
return { ok: true, resumo: `cena ${m.cena} → posição ${m.para}` };
|
|
162
|
+
}
|
|
163
|
+
case 'duplicar': {
|
|
164
|
+
const i = idx(pl, m.cena); if (i < 0) return falta(m.cena);
|
|
165
|
+
const para = m.para == null ? i + 1 : Math.max(0, Math.min(pl.cenas.length, Number(m.para) - 1));
|
|
166
|
+
pl.cenas.splice(para, 0, JSON.parse(JSON.stringify(pl.cenas[i])));
|
|
167
|
+
pl.trocas.splice(Math.max(0, para - 1), 0, pl.trocas[Math.min(pl.trocas.length - 1, Math.max(0, para - 1))] || 'corte');
|
|
168
|
+
refluir(pl);
|
|
169
|
+
return { ok: true, resumo: `cena ${m.cena} duplicada (${pl.cenas.length} cenas, ${pl.duracao}s)` };
|
|
170
|
+
}
|
|
171
|
+
case 'trilha': {
|
|
172
|
+
const v = m.valor;
|
|
173
|
+
if (v === false || v === 0 || /^(nenhuma|sem|sem_trilha|false|off|nao|não)$/i.test(String(v))) { spec.trilha = null; spec.semTrilha = true; pl.bpm = 0; return { ok: true, resumo: 'sem trilha' }; }
|
|
174
|
+
if (typeof v === 'string' && fs.existsSync(v)) { spec.trilha = { arquivo: v, inicio: 0 }; spec.semTrilha = false; return { ok: true, resumo: 'trilha: ' + path.basename(v) }; }
|
|
175
|
+
spec.buscaTrilha = typeof v === 'string' && v !== 'true' ? v : null; spec.semTrilha = false; spec.trilha = null; spec.trocarTrilha = true;
|
|
176
|
+
return { ok: true, resumo: 'trilha nova' + (spec.buscaTrilha ? ': ' + spec.buscaTrilha : '') };
|
|
177
|
+
}
|
|
178
|
+
case 'volume': {
|
|
179
|
+
const alvo = String(m.alvo || '').toLowerCase();
|
|
180
|
+
if (!['voz', 'trilha', 'sfx'].includes(alvo)) return { ok: false, error: 'volume pede alvo voz, trilha ou sfx.' };
|
|
181
|
+
const db = m.mudo ? -60 : Number(m.db);
|
|
182
|
+
if (!isFinite(db) || db < -60 || db > 12) return { ok: false, error: 'volume pede db entre -60 e 12 (0 = como a máquina mixou).' };
|
|
183
|
+
spec.volumes = { ...(spec.volumes || {}), [alvo]: db };
|
|
184
|
+
return { ok: true, resumo: `volume ${alvo}: ${db} dB` };
|
|
185
|
+
}
|
|
186
|
+
case 'formato': {
|
|
187
|
+
const f = maq().FORMATOS[String(m.valor)]; if (!f) return { ok: false, error: 'formato: 9:16, 1:1 ou 16:9.' };
|
|
188
|
+
pl.w = f[0]; pl.h = f[1]; pl.formato = String(m.valor);
|
|
189
|
+
if (pl.legenda) { pl.legenda.tamanho = pl.h > pl.w ? 52 : 44; pl.legenda.y = pl.h > pl.w ? 0.91 : 0.9; }
|
|
190
|
+
return { ok: true, resumo: 'formato ' + m.valor };
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
return { ok: false, error: 'mudança desconhecida: "' + op + '" (texto, cor, fonte, ritmo, duracao, remover, mover, duplicar, trilha, volume, formato).' };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── render ───────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
async function renderizarSpec(spec, pasta, { previa = false, deps = {}, semRede = false } = {}) {
|
|
200
|
+
const M = maq();
|
|
201
|
+
const pl = spec.pl;
|
|
202
|
+
const renderizar = deps.renderizar || require('./effectcraft.js').renderizar;
|
|
203
|
+
const tm = M.tempos(pl);
|
|
204
|
+
const r = await renderizar({ pasta, script: M.script(pl), comp: 'MAIN', fontes: M.fontesDoPlano(pl), conferir: previa ? [] : tm.map((x) => x.conferir), previa });
|
|
205
|
+
if (!r.ok) return r;
|
|
206
|
+
if (previa) {
|
|
207
|
+
const v = path.join(pasta, 'video-previa.mp4');
|
|
208
|
+
fs.copyFileSync(r.video, v);
|
|
209
|
+
return { ok: true, video: v, quadros: r.quadros || [], mix: null };
|
|
210
|
+
}
|
|
211
|
+
const som = require('./maquina-som.js');
|
|
212
|
+
const eventos = M.lerSons(r.saida);
|
|
213
|
+
const final = path.join(pasta, 'video-motion.mp4');
|
|
214
|
+
let mix = { ok: false, video: r.video }, sons = [];
|
|
215
|
+
try {
|
|
216
|
+
const vol = spec.volumes || {};
|
|
217
|
+
const indice = deps.sfxIndice || await som.prepararBiblioteca({ assunto: spec.args && spec.args.assunto || '', acoes: [...new Set(eventos.map((e) => e.tipo))], rede: !semRede });
|
|
218
|
+
sons = som.escolherSons(eventos, { assunto: spec.args && spec.args.assunto || '', indice, semente: pl.semente || 1 });
|
|
219
|
+
// a voz mais alta = o resto mais baixo (o final fecha em −14 LUFS, então só a diferença conta)
|
|
220
|
+
const dv = vol.voz || 0;
|
|
221
|
+
const mixar = deps.mixar || som.mixar;
|
|
222
|
+
mix = mixar({ pasta, mudo: r.video, saida: final, sons, trilha: spec.trilha && spec.trilha.arquivo, trilhaInicio: spec.trilha ? spec.trilha.inicio || 0 : 0, voz: spec.voz && spec.voz.mp3, duracao: pl.duracao, vol: { trilha: (vol.trilha || 0) - dv, sfx: (vol.sfx || 0) - dv } });
|
|
223
|
+
} catch (e) { mix = { ok: false, error: e.message, video: r.video }; }
|
|
224
|
+
let video = mix.ok ? mix.video : r.video;
|
|
225
|
+
if (video !== final && fs.existsSync(video)) { fs.copyFileSync(video, final); video = final; }
|
|
226
|
+
return { ok: true, video, quadros: r.quadros || [], mix, lufs: mix.ok ? mix.lufs : null, aviso: mix.ok ? undefined : 'o vídeo saiu sem a mixagem de áudio: ' + (mix.error || 'sem ffmpeg') };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── a ferramenta ─────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* estudio_editar. `args`: {mudancas:[...], voltar_para?, previa?, final?, listar?}. `deps`: renderizar, mixar, sfxIndice, trilha, semRede.
|
|
233
|
+
*/
|
|
234
|
+
async function editar(args = {}, { pasta, deps = {} } = {}) {
|
|
235
|
+
if (!pasta) return { ok: false, error: 'faltou a pasta do projeto.' };
|
|
236
|
+
const ix = indice(pasta);
|
|
237
|
+
if (!fs.existsSync(ARQ(pasta))) return { ok: false, error: 'esse vídeo não tem cenas.json: só dá para editar o que a máquina de motion (estudio_motion) gerou.' };
|
|
238
|
+
const lista = () => ix.versoes.map((v) => ({ n: v.n, resumo: v.resumo, previa: v.previa || undefined, atual: v.n === ix.atual || undefined }));
|
|
239
|
+
if (args.listar) return { ok: true, atual: ix.atual, versoes: lista() };
|
|
240
|
+
|
|
241
|
+
if (args.voltar_para != null) {
|
|
242
|
+
const v = ix.versoes.find((x) => x.n === Number(args.voltar_para));
|
|
243
|
+
if (!v) return { ok: false, error: `não existe a v${args.voltar_para} (há ${ix.versoes.map((x) => 'v' + x.n).join(', ') || 'nenhuma'}).` };
|
|
244
|
+
const spec = ler(path.join(pasta, 'versoes', 'v' + v.n, 'cenas.json'));
|
|
245
|
+
gravar(ARQ(pasta), spec);
|
|
246
|
+
ix.atual = v.n; gravar(IDX(pasta), ix);
|
|
247
|
+
let video = v.video ? path.join(pasta, v.video) : null;
|
|
248
|
+
if (video && fs.existsSync(video)) { const f = path.join(pasta, 'video-motion.mp4'); fs.copyFileSync(video, f); video = f; }
|
|
249
|
+
return { ok: true, versao: v.n, video, versoes: lista(), nota: `voltou para a v${v.n}; a próxima mudança vira a v${ix.versoes.reduce((m, x) => Math.max(m, x.n), 0) + 1}.` };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// O plano limita as edições guardadas por vídeo (grátis 3). Prévia e a versão original não contam.
|
|
253
|
+
const gastas = indice(pasta).versoes.filter((v) => !v.previa && v.resumo !== 'original').length;
|
|
254
|
+
const lim = await (deps.cabeMais ? deps.cabeMais(gastas) : require('../limites.js').cabeMais('versoesEditor', gastas));
|
|
255
|
+
if (!lim.ok && !args.previa) return { ok: false, error: lim.error + ' Dá para voltar a uma versão (voltar_para) ou ver as prévias.' };
|
|
256
|
+
|
|
257
|
+
const spec = ler(ARQ(pasta));
|
|
258
|
+
const mudancas = Array.isArray(args.mudancas) ? args.mudancas : [];
|
|
259
|
+
if (!mudancas.length && !args.final) return { ok: false, error: 'mudancas está vazio (ou use final:true para renderizar a prévia em qualidade final).' };
|
|
260
|
+
const feitas = [], recusadas = [];
|
|
261
|
+
for (const m of mudancas) {
|
|
262
|
+
const r = aplicar(spec, m, { pasta });
|
|
263
|
+
(r.ok ? feitas : recusadas).push(r.ok ? r.resumo : r.error);
|
|
264
|
+
}
|
|
265
|
+
if (!feitas.length && !args.final) return { ok: false, error: 'nenhuma mudança valeu: ' + recusadas.join(' | ') };
|
|
266
|
+
|
|
267
|
+
if (spec.trocarTrilha) { // trilha nova por busca
|
|
268
|
+
const semRede = !!(deps.semRede || args.semRede);
|
|
269
|
+
try {
|
|
270
|
+
let r;
|
|
271
|
+
if (deps.trilha) { const t0 = await deps.trilha(pasta, spec.buscaTrilha); r = t0 && t0.arquivo ? t0 : null; }
|
|
272
|
+
else { r = await require('./maquina-trilha.js').escolher({ chave: dir().achar(String(spec.args && spec.args.assunto || '')).chave, busca: spec.buscaTrilha, pasta, rede: !semRede }); if (!(r && r.arquivo)) r = null; }
|
|
273
|
+
if (r) { spec.trilha = { arquivo: r.arquivo, inicio: 0 }; spec.pl.bpm = r.bpm || 0; }
|
|
274
|
+
else recusadas.push('não achei trilha nova; o vídeo fica sem música.');
|
|
275
|
+
} catch (e) { recusadas.push('trilha: ' + e.message); }
|
|
276
|
+
delete spec.trocarTrilha;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const previa = !!args.previa;
|
|
280
|
+
spec.previa = previa;
|
|
281
|
+
const r = await renderizarSpec(spec, pasta, { previa, deps, semRede: !!(deps.semRede || args.semRede) });
|
|
282
|
+
if (!r.ok) return r;
|
|
283
|
+
const resumo = feitas.join('; ') || 'render final';
|
|
284
|
+
// versão: a prévia também entra (marcada), pra dar pra voltar
|
|
285
|
+
const versao = registrarVersao(pasta, spec, r.video, resumo);
|
|
286
|
+
return { ok: true, versao, previa, video: r.video, quadros: r.quadros, duracao: spec.pl.duracao, cenas: spec.pl.cenas.length, feitas, recusadas, versoes: lista2(pasta), lufs: r.lufs, aviso: r.aviso };
|
|
287
|
+
}
|
|
288
|
+
const lista2 = (pasta) => indice(pasta).versoes.map((v) => ({ n: v.n, resumo: v.resumo, previa: v.previa || undefined }));
|
|
289
|
+
|
|
290
|
+
module.exports = { salvarSpec, aplicar, editar, indice, renderizarSpec, registrarVersao };
|