primocode 8.42.1 → 9.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.
Files changed (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +599 -327
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +31 -192
  7. package/lib/catalogo.js +60 -60
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +545 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. package/studio/web/view.html +0 -27
@@ -1,233 +0,0 @@
1
- /* ==========================================================================
2
- sfx.js — efeitos sonoros do Corte, sintetizados na hora.
3
-
4
- Nenhum arquivo, nenhuma licença: whoosh é ruído filtrado com varredura,
5
- impacto é um seno grave despencando, click é um blip curto. Tudo entra no
6
- mesmo grafo da trilha (via `destino`), então a gravação MP4 captura os
7
- efeitos junto — sem depender de "compartilhar áudio da aba".
8
-
9
- Uso:
10
- var sfx = criarSfx(ctx, destino); // destino: nó do grafo (compressor)
11
- sfx.whoosh(); // transição de cena
12
- sfx.impacto(); // ênfase (flash, número grande)
13
- sfx.click(); // entrada de elemento, feedback discreto
14
-
15
- O volume de cada efeito já vem calibrado para sentar SOB a narração.
16
- ========================================================================== */
17
-
18
- function criarSfx(ctx, destino) {
19
- "use strict";
20
- if (!ctx || !destino) return null;
21
-
22
- var saida = ctx.createGain();
23
- saida.gain.value = 1;
24
- saida.connect(destino);
25
-
26
- var ruido = null;
27
- function bufferRuido() {
28
- if (ruido) return ruido;
29
- ruido = ctx.createBuffer(1, ctx.sampleRate * 1.2, ctx.sampleRate);
30
- var d = ruido.getChannelData(0);
31
- for (var i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
32
- return ruido;
33
- }
34
-
35
- /* Varredura de ruído — o "passar de página" do vídeo. `variacao` muda o
36
- miolo da varredura para os whooshes não saírem todos idênticos. */
37
- function whoosh(variacao) {
38
- var agora = ctx.currentTime;
39
- var v = (variacao == null ? Math.random() : variacao) % 1;
40
- var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
41
- s.buffer = bufferRuido();
42
- f.type = "bandpass";
43
- f.Q.value = 1.1;
44
- f.frequency.setValueAtTime(300 + v * 300, agora);
45
- f.frequency.exponentialRampToValueAtTime(2200 + v * 1400, agora + 0.16);
46
- f.frequency.exponentialRampToValueAtTime(500, agora + 0.42);
47
- g.gain.setValueAtTime(0.0001, agora);
48
- g.gain.exponentialRampToValueAtTime(0.11, agora + 0.10);
49
- g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.45);
50
- s.connect(f); f.connect(g); g.connect(saida);
51
- s.start(agora); s.stop(agora + 0.5);
52
- }
53
-
54
- /* Seno grave despencando + um sopro de ruído no ataque. */
55
- function impacto() {
56
- var agora = ctx.currentTime;
57
- var o = ctx.createOscillator(), g = ctx.createGain();
58
- o.type = "sine";
59
- o.frequency.setValueAtTime(120, agora);
60
- o.frequency.exponentialRampToValueAtTime(36, agora + 0.5);
61
- g.gain.setValueAtTime(0.30, agora);
62
- g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.9);
63
- o.connect(g); g.connect(saida);
64
- o.start(agora); o.stop(agora + 1.0);
65
-
66
- var s = ctx.createBufferSource(), gr = ctx.createGain(), f = ctx.createBiquadFilter();
67
- s.buffer = bufferRuido();
68
- f.type = "lowpass";
69
- f.frequency.value = 900;
70
- gr.gain.setValueAtTime(0.09, agora);
71
- gr.gain.exponentialRampToValueAtTime(0.0001, agora + 0.22);
72
- s.connect(f); f.connect(gr); gr.connect(saida);
73
- s.start(agora); s.stop(agora + 0.3);
74
- }
75
-
76
- /* Blip de interface: curto, agudo, discreto. */
77
- function click() {
78
- var agora = ctx.currentTime;
79
- var o = ctx.createOscillator(), g = ctx.createGain();
80
- o.type = "sine";
81
- o.frequency.setValueAtTime(2100, agora);
82
- g.gain.setValueAtTime(0.06, agora);
83
- g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.05);
84
- o.connect(g); g.connect(saida);
85
- o.start(agora); o.stop(agora + 0.07);
86
- }
87
-
88
- /* ── A biblioteca cresceu porque três sons não fazem motion ───────────
89
- Pedido do dono: "efeitos sonoros… bem motion mesmo". Whoosh, impacto e
90
- click cobrem a transição; o que entra num elemento — um número que
91
- aparece, um ícone que pousa, uma barra que sobe — pedia mais. Todos
92
- sintetizados, sem arquivo e sem licença, calibrados para ficar SOB a
93
- narração. */
94
-
95
- /* Bolha estourando: um seno que sobe rápido e some. Entrada de item. */
96
- function pop() {
97
- var agora = ctx.currentTime;
98
- var o = ctx.createOscillator(), g = ctx.createGain();
99
- o.type = "sine";
100
- o.frequency.setValueAtTime(380, agora);
101
- o.frequency.exponentialRampToValueAtTime(900, agora + 0.06);
102
- g.gain.setValueAtTime(0.0001, agora);
103
- g.gain.exponentialRampToValueAtTime(0.16, agora + 0.012);
104
- g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.14);
105
- o.connect(g); g.connect(saida);
106
- o.start(agora); o.stop(agora + 0.16);
107
- }
108
-
109
- /* Sino de notificação: dois parciais (fundamental + quinta acima), cauda
110
- longa. Confirmação, número final, "pronto". */
111
- function ding() {
112
- var agora = ctx.currentTime;
113
- [[1320, 0.11], [1980, 0.05], [2640, 0.025]].forEach(function (p, i) {
114
- var o = ctx.createOscillator(), g = ctx.createGain();
115
- o.type = "sine";
116
- o.frequency.value = p[0];
117
- g.gain.setValueAtTime(0.0001, agora);
118
- g.gain.exponentialRampToValueAtTime(p[1], agora + 0.008);
119
- g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.9 - i * 0.15);
120
- o.connect(g); g.connect(saida);
121
- o.start(agora); o.stop(agora + 1.0);
122
- });
123
- }
124
-
125
- /* Subida (riser): ruído filtrado que sobe por um segundo e meio e para
126
- seco — a tensão antes do número grande ou da assinatura. */
127
- function subida() {
128
- var agora = ctx.currentTime;
129
- var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
130
- s.buffer = bufferRuido();
131
- s.loop = true;
132
- f.type = "bandpass"; f.Q.value = 2.2;
133
- f.frequency.setValueAtTime(180, agora);
134
- f.frequency.exponentialRampToValueAtTime(3800, agora + 1.5);
135
- g.gain.setValueAtTime(0.0001, agora);
136
- g.gain.exponentialRampToValueAtTime(0.13, agora + 1.4);
137
- g.gain.setValueAtTime(0.0001, agora + 1.52);
138
- s.connect(f); f.connect(g); g.connect(saida);
139
- s.start(agora); s.stop(agora + 1.6);
140
- }
141
-
142
- /* Tique de relógio/teclado: madeira seca. Passo de lista, contagem. */
143
- function tique() {
144
- var agora = ctx.currentTime;
145
- var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
146
- s.buffer = bufferRuido();
147
- f.type = "highpass"; f.frequency.value = 2600;
148
- g.gain.setValueAtTime(0.14, agora);
149
- g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.04);
150
- s.connect(f); f.connect(g); g.connect(saida);
151
- s.start(agora); s.stop(agora + 0.05);
152
- }
153
-
154
- /* Brilho: arpejo rápido e agudo, o "sparkle" de um destaque ou de uma
155
- estrela acendendo. */
156
- function brilho() {
157
- var agora = ctx.currentTime;
158
- [1568, 2093, 2637, 3136].forEach(function (hz, i) {
159
- var o = ctx.createOscillator(), g = ctx.createGain();
160
- o.type = "triangle";
161
- o.frequency.value = hz;
162
- var t = agora + i * 0.045;
163
- g.gain.setValueAtTime(0.0001, t);
164
- g.gain.exponentialRampToValueAtTime(0.06, t + 0.01);
165
- g.gain.exponentialRampToValueAtTime(0.0001, t + 0.35);
166
- o.connect(g); g.connect(saida);
167
- o.start(t); o.stop(t + 0.4);
168
- });
169
- }
170
-
171
- /* Um ARQUIVO de som (o efeito baixado da internet pelo agente, ou o do
172
- usuário), tocado dentro do MESMO grafo — é o que faz ele entrar no MP4
173
- exportado junto da trilha. Decodificado uma vez por URL. */
174
- var cacheArquivos = {};
175
- function arquivo(url, volume) {
176
- if (!url) return;
177
- var pronto = cacheArquivos[url] || (cacheArquivos[url] = fetch(url)
178
- .then(function (r) { return r.arrayBuffer(); })
179
- .then(function (b) { return ctx.decodeAudioData(b); })
180
- .catch(function () { return null; }));
181
- pronto.then(function (buffer) {
182
- if (!buffer) return;
183
- var s = ctx.createBufferSource(), g = ctx.createGain();
184
- s.buffer = buffer;
185
- g.gain.value = volume == null ? 0.7 : Math.max(0, Math.min(1, volume));
186
- s.connect(g); g.connect(saida);
187
- s.start(ctx.currentTime);
188
- });
189
- return pronto;
190
- }
191
-
192
- /* O efeito certo para a ENTRADA de uma cena, pela transição dela.
193
- flash e zoom pedem peso; corte seco não pede nada. */
194
- function transicao(tipo, indice) {
195
- if (tipo === "corte") return;
196
- if (tipo === "flash" || tipo === "zoom") return impacto();
197
- whoosh(((indice || 0) * 0.37) % 1);
198
- }
199
-
200
- /* Toca pelo NOME ou por {arquivo, volume}. É o que o player chama para o
201
- `som` de um elemento ou de uma cena. Nome desconhecido não toca nada —
202
- e não quebra. */
203
- function tocar(som) {
204
- if (!som) return;
205
- if (typeof som === "string") { if (typeof api[som] === "function" && som !== "tocar") api[som](); return; }
206
- if (som.arquivo) return arquivo(som.arquivo, som.volume);
207
- if (som.nome && typeof api[som.nome] === "function") api[som.nome]();
208
- }
209
-
210
- var api = {
211
- whoosh: whoosh,
212
- impacto: impacto,
213
- click: click,
214
- pop: pop,
215
- ding: ding,
216
- subida: subida,
217
- tique: tique,
218
- brilho: brilho,
219
- arquivo: arquivo,
220
- tocar: tocar,
221
- transicao: transicao,
222
- volume: function (v) { saida.gain.value = v; },
223
- };
224
- return api;
225
- }
226
-
227
- /* Os nomes que o documento pode usar em `som`/`sfx` — lidos pela spec e pelo
228
- servidor, para recusar nome inventado com a lista certa. */
229
- var SONS = ["whoosh", "impacto", "click", "pop", "ding", "subida", "tique", "brilho"];
230
-
231
- if (typeof module !== "undefined" && module.exports) {
232
- module.exports = { criarSfx: criarSfx, SONS: SONS };
233
- }
@@ -1,464 +0,0 @@
1
- /* ==========================================================================
2
- video.js — o vídeo vira ARQUIVO. Sem gravar a tela de ninguém.
3
-
4
- ── O QUE ESTAVA ERRADO ─────────────────────────────────────────────────
5
- O botão de baixar usava getDisplayMedia: pedia permissão, o usuário
6
- escolhia uma janela, e o navegador salvava o que estivesse aparecendo.
7
- Isso é gravação de tela, não exportação. O dono foi direto ao ponto, em
8
- quatro conversas diferentes:
9
-
10
- "O mp4 não vai ser uma gravação da tela do usuário, tem nada a ver.
11
- Tu clica no botão download ele já vai baixar o arquivo."
12
-
13
- E não é preferência. Gravando a tela: o ponteiro do mouse entra no vídeo,
14
- uma notificação entra no vídeo, a resolução é a da janela e não a do
15
- projeto, e quem for VENDER a ferramenta não vai pedir para o cliente
16
- compartilhar a tela para exportar um arquivo dele mesmo.
17
-
18
- ── COMO FUNCIONA ───────────────────────────────────────────────────────
19
- O vídeo é DESENHADO. Um canvas do tamanho exato do projeto é fotografado
20
- a partir da cena que está tocando, quadro a quadro, e vira faixa de vídeo
21
- por captureStream. O áudio vem do grafo que o player já monta — trilha,
22
- narração e efeitos, com o ducking que abaixa a música na voz. O
23
- MediaRecorder junta os dois num arquivo. Zero permissão.
24
-
25
- ── POR QUE ELE DIRIGE O PLAYER EM VEZ DE RENDERIZAR SOZINHO ────────────
26
- Porque o player já sabe tocar: ele encadeia as cenas, dispara a narração
27
- no tempo certo, aplica ducking e toca os efeitos de transição. Refazer
28
- isso aqui seria manter duas versões da mesma lógica — e elas divergiriam,
29
- como já aconteceu neste código com o editor e o renderizador. Aqui o
30
- exportador só pergunta "qual cena está na tela agora?" e fotografa.
31
-
32
- ── POR QUE EM TEMPO REAL ───────────────────────────────────────────────
33
- O ideal seria renderizar sem pressa e carimbar cada quadro com o tempo
34
- dele (MediaStreamTrackGenerator). Foi testado: o MediaRecorder não produz
35
- um byte com faixa vinda de gerador — zero em mp4, webm/vp8 e webm/vp9. Só
36
- o captureStream de canvas grava, e ele é preso ao relógio.
37
-
38
- O custo aparece na taxa de quadros: rasterizar 1920×1080 leva ~76 ms.
39
- Mas quase nunca é preciso rasterizar — numa cena de 4 segundos a entrada
40
- dura menos de um, e o resto do tempo o quadro está PARADO. Este arquivo
41
- pergunta ao navegador se ainda há animação rodando; enquanto há,
42
- redesenha; quando todas terminam, para, e o captureStream segue emitindo
43
- o último quadro na taxa cheia. Movimento suave onde há movimento, imagem
44
- limpa onde não há.
45
- ========================================================================== */
46
-
47
- var VideoExport = (function () {
48
- "use strict";
49
-
50
- /* Do mais compatível para o menos.
51
- *
52
- * H.264 com AAC é o MP4 que toca em tudo — WhatsApp, Instagram,
53
- * PowerPoint, TV, editor de vídeo. AV1 dentro de MP4 é MP4 de verdade mas
54
- * ainda não toca em todo lugar, então vem depois. O webm é o último
55
- * recurso: abre em navegador e pouco mais.
56
- *
57
- * Testar só "video/mp4" não basta: o Chrome responde false para o nome
58
- * solto e true para a forma com codec. Já custou MP4 a quem pediu MP4. */
59
- var FORMATOS = [
60
- "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
61
- "video/mp4;codecs=avc1.4d002a,mp4a.40.2",
62
- "video/mp4;codecs=avc1,mp4a.40.2",
63
- "video/mp4;codecs=avc1",
64
- "video/mp4",
65
- "video/webm;codecs=vp9,opus",
66
- "video/webm;codecs=vp8,opus",
67
- "video/webm",
68
- ];
69
-
70
- function formato() {
71
- if (!window.MediaRecorder) return null;
72
- for (var i = 0; i < FORMATOS.length; i++) {
73
- if (MediaRecorder.isTypeSupported(FORMATOS[i])) return FORMATOS[i];
74
- }
75
- return null;
76
- }
77
-
78
- function disponivel() {
79
- return !!(formato() && document.createElement("canvas").captureStream
80
- && typeof Exportar !== "undefined" && Exportar.retrato);
81
- }
82
-
83
- function ehMp4(tipo) { return /mp4/.test(tipo || ""); }
84
- function extensaoDe(tipo) { return ehMp4(tipo) ? ".mp4" : ".webm"; }
85
-
86
- /* Ainda há algo se mexendo nesta cena?
87
- *
88
- * É a pergunta que decide se vale gastar dezenas de milissegundos
89
- * redesenhando. Animação infinita (Ken Burns, cubo 3D girando, brilho de
90
- * gradiente) nunca termina — e aí redesenhar é o certo, porque a imagem
91
- * MUDA mesmo. */
92
- function aindaAnima(no) {
93
- if (!no || !no.getAnimations) return true; // sem a API: redesenha sempre
94
- var todas;
95
- try { todas = no.getAnimations({ subtree: true }); }
96
- catch (e) { return true; }
97
- for (var i = 0; i < todas.length; i++) {
98
- if (todas[i].playState === "running") return true;
99
- }
100
- return false;
101
- }
102
-
103
- function duracaoDaCena(c) {
104
- var d = c && c.duracao;
105
- return (typeof d === "number" && isFinite(d) && d > 0) ? d : 4;
106
- }
107
-
108
- /* ------------------------------------------------------------ a fonte -- */
109
-
110
- /* Uma "fonte" diz ao exportador o que fotografar e por quanto tempo.
111
- *
112
- * Quando o player está na página, ele é a fonte: sabe tocar, narrar e
113
- * mixar. Fora dele (no editor, ou num teste), o exportador monta uma fonte
114
- * própria que percorre as cenas por conta — mais simples, sem narração.
115
- */
116
- function fonteDoDocumento(doc, palco) {
117
- var cenas = doc.cenas || [];
118
- var nos = cenas.map(function (c) {
119
- var no = renderCena(c, doc, { animar: true });
120
- no.style.position = "static";
121
- no.style.transform = "none";
122
- no.style.display = "none";
123
- palco.appendChild(no);
124
- return no;
125
- });
126
- var atual = -1;
127
- var t0 = 0;
128
- var total = cenas.reduce(function (t, c) { return t + duracaoDaCena(c); }, 0);
129
-
130
- function qualCena(t) {
131
- var acc = 0;
132
- for (var i = 0; i < cenas.length; i++) {
133
- acc += duracaoDaCena(cenas[i]);
134
- if (t < acc) return i;
135
- }
136
- return cenas.length - 1;
137
- }
138
-
139
- return {
140
- duracao: total,
141
- comecar: function () { t0 = performance.now(); },
142
- parar: function () {},
143
- no: function () {
144
- var t = (performance.now() - t0) / 1000;
145
- var i = qualCena(t);
146
- if (i !== atual) {
147
- atual = i;
148
- nos.forEach(function (n, k) { n.style.display = k === i ? "block" : "none"; });
149
- // reinicia entrada e transição, como o player faz
150
- nos[i].classList.remove("animar");
151
- void nos[i].offsetWidth;
152
- nos[i].classList.add("animar");
153
- }
154
- return nos[i];
155
- },
156
- };
157
- }
158
-
159
- /* ------------------------------------------------------------ clipes -- */
160
-
161
- /* Desenha os vídeos do usuário no canvas, ANTES do desenho da cena.
162
- *
163
- * O retrato de uma cena é feita por SVG com foreignObject, e foreignObject
164
- * NÃO rasteriza <video>: o quadro do vídeo sairia em branco, por mais
165
- * correto que o resto estivesse. Então o vídeo é desenhado direto no
166
- * canvas — drawImage aceita um <video> e pega o quadro atual — e a cena
167
- * entra por cima, onde o buraco do vídeo é transparente.
168
- *
169
- * A conta de posição vem do layout real: getBoundingClientRect do vídeo
170
- * contra o da cena, convertido para as coordenadas do projeto. Assim vale
171
- * para clipe posicionado à mão (x/y) e para clipe que o layout arrumou.
172
- *
173
- * "Onde o buraco é transparente" não era verdade e por isso a gravação
174
- * sumia do arquivo: a cena carrega o fundo do tema, opaco, e ele cobria o
175
- * vídeo inteiro. Um vídeo do usuário que não aparece no vídeo do usuário —
176
- * e nada denunciava, porque na tela, no DOM, estava certo.
177
- *
178
- * Então numa cena com clipe a ordem é: a cor de chão do tema, o vídeo, e a
179
- * cena SEM fundo por cima (é o que `semFundo` faz no clone). O texto, a
180
- * legenda e a marca continuam por cima da imagem, que é onde devem estar.
181
- */
182
- function desenharClipes(ctx2d, cenaEl, L, A) {
183
- var vids = cenaEl.querySelectorAll("video");
184
- if (!vids.length) return;
185
- var caixaCena = cenaEl.getBoundingClientRect();
186
- if (!caixaCena.width || !caixaCena.height) return;
187
- var escX = L / caixaCena.width;
188
- var escY = A / caixaCena.height;
189
-
190
- Array.prototype.forEach.call(vids, function (v) {
191
- // readyState < 2 é vídeo sem quadro nenhum decodificado ainda: pedir
192
- // para desenhar levanta exceção em alguns navegadores e mancha o
193
- // canvas em outros.
194
- if (v.readyState < 2 || !v.videoWidth) return;
195
- // Clipe de SÓ ÁUDIO: entra pelo som e não pelo quadro. Ele tem 1px na
196
- // tela, então sem esta linha o export desenharia uma tira de um pixel
197
- // esticada por cima da cena montada — visível, e sem causa aparente.
198
- if (v.dataset && v.dataset.som === "1") return;
199
- var r = v.getBoundingClientRect();
200
- var dx = (r.left - caixaCena.left) * escX;
201
- var dy = (r.top - caixaCena.top) * escY;
202
- var dw = r.width * escX;
203
- var dh = r.height * escY;
204
- if (dw <= 0 || dh <= 0) return;
205
-
206
- // "cobrir" recorta o vídeo para encher a caixa sem deformar — o mesmo
207
- // que o object-fit faz na tela. Sem isto o export esticaria a imagem
208
- // e ficaria diferente do que a pessoa aprovou no player.
209
- var conter = v.style.objectFit === "contain";
210
- var escala = conter
211
- ? Math.min(dw / v.videoWidth, dh / v.videoHeight)
212
- : Math.max(dw / v.videoWidth, dh / v.videoHeight);
213
- var lw = v.videoWidth * escala;
214
- var lh = v.videoHeight * escala;
215
- var lx = dx + (dw - lw) / 2;
216
- var ly = dy + (dh - lh) / 2;
217
-
218
- ctx2d.save();
219
- ctx2d.beginPath();
220
- ctx2d.rect(dx, dy, dw, dh);
221
- ctx2d.clip();
222
- // "cinza", "sepia", "contraste": na tela é filter do CSS, e drawImage
223
- // ignora o CSS do elemento. O canvas tem o mesmo filtro, com a mesma
224
- // gramática — então é só repassar, e o arquivo sai como o player.
225
- var f = window.getComputedStyle(v).filter;
226
- if (f && f !== "none") { try { ctx2d.filter = f; } catch (e) {} }
227
- try { ctx2d.drawImage(v, lx, ly, lw, lh); } catch (e) { /* quadro ainda não veio */ }
228
- ctx2d.restore();
229
- });
230
- }
231
-
232
- /* O chão da cena, para o clipe que não ocupa o quadro inteiro. Sem isto,
233
- a volta do vídeo sairia preta em vez da cor do tema. */
234
- function corDeChao(no) {
235
- var c = window.getComputedStyle(no).backgroundColor;
236
- return (c && c !== "rgba(0, 0, 0, 0)" && c !== "transparent") ? c : "#0b0e14";
237
- }
238
-
239
- /* Prepara o CLONE da cena que tem gravação — só o clone, a tela do usuário
240
- não muda. Duas coisas precisam sair, e as duas escondiam o vídeo:
241
-
242
- · o fundo da cena, opaco por cima da gravação. `data-fundo` sai junto
243
- porque os padrões (aurora, raios, ondas) são pseudo-elementos: zerar
244
- `background` deixaria a camada de cima intacta;
245
-
246
- · o próprio <video>. foreignObject não decodifica vídeo, mas também não
247
- deixa o espaço vazio: pinta a caixa do elemento no cinza padrão
248
- (#333). O quadro saía com a gravação desenhada embaixo e um retângulo
249
- cinza chapado exatamente em cima dela. */
250
- function semFundo(clone) {
251
- clone.removeAttribute("data-fundo");
252
- clone.style.background = "transparent";
253
- clone.style.backgroundImage = "none";
254
- Array.prototype.forEach.call(clone.querySelectorAll("video"), function (v) {
255
- v.style.visibility = "hidden";
256
- v.style.background = "transparent";
257
- // A caixa que segura o vídeo também pinta (é o cinza de espera, útil na
258
- // tela enquanto o arquivo carrega). No quadro exportado ela é só mais
259
- // uma tampa em cima da gravação — e foi a última a ser encontrada,
260
- // porque some junto com o resto do fundo e parece o fundo.
261
- for (var pai = v.parentNode; pai && pai !== clone; pai = pai.parentNode) {
262
- pai.style.background = "transparent";
263
- pai.style.backgroundImage = "none";
264
- }
265
- });
266
- }
267
-
268
- /* Cena com vídeo nunca fica "parada": o clipe corre mesmo sem animação
269
- CSS, e pular o redesenho congelaria a imagem do usuário. */
270
- function temClipe(no) {
271
- return !!(no && no.querySelector && no.querySelector("video"));
272
- }
273
-
274
- /* --------------------------------------------------------------- gerar -- */
275
-
276
- /* Monta o arquivo e devolve { blob, bytes, formato, mp4, segundos }.
277
- *
278
- * `op.fonte` — quem diz o que fotografar (o player, normalmente).
279
- * `op.fluxoAudio` — MediaStream já mixado. Sem ele o vídeo sai mudo.
280
- * `op.aoAndar(fracao)` — progresso, para a interface mostrar.
281
- */
282
- function gerar(arq, op) {
283
- op = op || {};
284
- var doc = (arq && arq.doc) || {};
285
- var cenas = doc.cenas || [];
286
- if (!cenas.length) return Promise.reject(new Error("este arquivo não tem cenas"));
287
-
288
- var tipo = formato();
289
- if (!tipo) return Promise.reject(new Error("este navegador não sabe gerar vídeo"));
290
-
291
- var L = doc.largura || 1920;
292
- var A = doc.altura || 1080;
293
- var FPS = op.fps || 30;
294
- var andar = op.aoAndar || function () {};
295
-
296
- var tela = document.createElement("canvas");
297
- tela.width = L; tela.height = A;
298
- var ctx2d = tela.getContext("2d", { alpha: false });
299
- ctx2d.fillStyle = "#000";
300
- ctx2d.fillRect(0, 0, L, A);
301
-
302
- // Palco próprio só quando o exportador tem de montar as cenas sozinho.
303
- // Fica DENTRO do documento (fora dele não há layout nem animação) mas
304
- // fora da vista — e não com display:none, que também mataria o layout.
305
- var palco = null;
306
- var fonte = op.fonte;
307
- if (!fonte) {
308
- palco = document.createElement("div");
309
- palco.setAttribute("aria-hidden", "true");
310
- palco.style.cssText = "position:fixed;left:-99999px;top:0;pointer-events:none;"
311
- + "width:" + L + "px;height:" + A + "px;overflow:hidden";
312
- document.body.appendChild(palco);
313
- fonte = fonteDoDocumento(doc, palco);
314
- }
315
-
316
- var fluxo = tela.captureStream(FPS);
317
- if (op.fluxoAudio) {
318
- op.fluxoAudio.getAudioTracks().forEach(function (f) { fluxo.addTrack(f); });
319
- }
320
-
321
- var pedacos = [];
322
- var rec = new MediaRecorder(fluxo, {
323
- mimeType: tipo,
324
- videoBitsPerSecond: op.bitrate || (L * A > 1000000 ? 8000000 : 4000000),
325
- });
326
- rec.ondataavailable = function (ev) { if (ev.data && ev.data.size) pedacos.push(ev.data); };
327
-
328
- var totalMs = (op.duracao || fonte.duracao || 0) * 1000;
329
- var cancelado = false;
330
- var folhaCss = null;
331
-
332
- function limpar() {
333
- try { if (fonte.parar) fonte.parar(); } catch (e) {}
334
- if (palco) { try { palco.remove(); } catch (e) {} }
335
- fluxo.getVideoTracks().forEach(function (t) { try { t.stop(); } catch (e) {} });
336
- }
337
-
338
- var promessa = Exportar.css().then(function (folha) {
339
- folhaCss = folha;
340
- // A fonte pode ter o que carregar antes do primeiro quadro — a gravação
341
- // do usuário, que sem quadro decodificado desenharia em branco.
342
- return fonte.preparar ? fonte.preparar() : null;
343
- }).then(function () {
344
- rec.start(1000);
345
- if (fonte.comecar) fonte.comecar();
346
-
347
- return new Promise(function (pronto) {
348
- var t0 = performance.now();
349
- var primeiro = true;
350
- /* A camada de cima de uma cena com clipe (título, legenda, marca),
351
- já rasterizada. Enquanto nada nela anima, só o quadro do vídeo
352
- muda — e rasterizar o SVG inteiro trinta vezes por segundo era o
353
- que engasgava a máquina: o decodificador do <video> ficava sem
354
- CPU, o clipe atrasava, o player o puxava de volta e a busca não
355
- terminava nunca (medido: seeking=true e readyState=1 do 0,7s ao
356
- fim, arquivo com o chão da cena no lugar da gravação). Com a
357
- camada guardada, cada quadro é dois drawImage. */
358
- var camada = null;
359
-
360
- function pintarComClipe(no, img) {
361
- // chão do tema → gravação do usuário → cena sem fundo
362
- ctx2d.fillStyle = corDeChao(no);
363
- ctx2d.fillRect(0, 0, L, A);
364
- desenharClipes(ctx2d, no, L, A);
365
- ctx2d.drawImage(img, 0, 0, L, A);
366
- }
367
-
368
- function volta() {
369
- if (cancelado) return pronto();
370
- var t = performance.now() - t0;
371
- if (t >= totalMs) return pronto();
372
- andar(Math.min(0.995, t / totalMs));
373
-
374
- var no = fonte.no();
375
- if (!no) return setTimeout(volta, 1000 / FPS);
376
-
377
- // Quadro idêntico ao anterior não precisa ser refeito: o
378
- // captureStream repete o último sozinho, na taxa cheia.
379
- if (!primeiro && !aindaAnima(no) && !temClipe(no)) {
380
- return setTimeout(volta, 1000 / FPS);
381
- }
382
- primeiro = false;
383
-
384
- var comVideo = temClipe(no);
385
- if (comVideo && camada && camada.no === no && !aindaAnima(no)) {
386
- pintarComClipe(no, camada.img);
387
- return setTimeout(volta, 1000 / FPS);
388
- }
389
-
390
- Exportar.retrato(no, L, A, folhaCss, false, comVideo ? semFundo : null)
391
- .then(function (img) {
392
- if (cancelado) return pronto();
393
- if (comVideo) {
394
- camada = aindaAnima(no) ? null : { no: no, img: img };
395
- pintarComClipe(no, img);
396
- } else {
397
- ctx2d.drawImage(img, 0, 0, L, A);
398
- }
399
- volta();
400
- }).catch(function () {
401
- // Um quadro que não rasterizou não derruba o vídeo: segue, e o
402
- // quadro anterior continua na tela.
403
- setTimeout(volta, 1000 / FPS);
404
- });
405
- }
406
- volta();
407
- });
408
- }).then(function () {
409
- return new Promise(function (ok) {
410
- // Um instante a mais antes de parar: o codificador precisa fechar o
411
- // último trecho, e parar em cima do fim corta o final do vídeo.
412
- setTimeout(function () {
413
- rec.onstop = function () { ok(); };
414
- if (rec.state !== "inactive") rec.stop(); else ok();
415
- }, 400);
416
- });
417
- }).then(function () {
418
- limpar();
419
- var blob = new Blob(pedacos, { type: tipo.split(";")[0] });
420
- if (!blob.size) throw new Error("o navegador não produziu vídeo");
421
- andar(1);
422
- return {
423
- ok: true, blob: blob, bytes: blob.size,
424
- formato: extensaoDe(tipo), tipo: tipo, mp4: ehMp4(tipo),
425
- segundos: totalMs / 1000,
426
- };
427
- }).catch(function (e) {
428
- limpar();
429
- throw e;
430
- });
431
-
432
- promessa.cancelar = function () { cancelado = true; };
433
- return promessa;
434
- }
435
-
436
- /* Entrega ao usuário o arquivo que `gerar` montou. Separado de `baixar`
437
- porque a exportação ESCONDIDA (studio_exportar) gera pelo mesmo caminho e
438
- não baixa: manda o blob de volta ao estúdio. Um só `gerar` para os dois. */
439
- function salvar(r, nome) {
440
- var url = URL.createObjectURL(r.blob);
441
- var a = document.createElement("a");
442
- a.href = url;
443
- a.download = (nome || "video")
444
- .replace(/[^\w\-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() + r.formato;
445
- document.body.appendChild(a);
446
- a.click();
447
- a.remove();
448
- setTimeout(function () { URL.revokeObjectURL(url); }, 8000);
449
- return r;
450
- }
451
-
452
- /* Gera e entrega o arquivo. É isto que o botão chama. */
453
- function baixar(arq, op) {
454
- op = op || {};
455
- return gerar(arq, op).then(function (r) { return salvar(r, op.nome || arq.titulo); });
456
- }
457
-
458
- return {
459
- disponivel: disponivel, formato: formato, gerar: gerar, baixar: baixar, salvar: salvar,
460
- FORMATOS: FORMATOS, _aindaAnima: aindaAnima,
461
- };
462
- })();
463
-
464
- if (typeof module !== "undefined" && module.exports) module.exports = VideoExport;