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,1852 +0,0 @@
1
- /* ==========================================================================
2
- render.js — desenha uma CENA (Prisma, Tela, Corte, Traço).
3
- Uma cena é uma tela de largura x altura com elementos dentro.
4
- Dois modos: layout nomeado (o estúdio posiciona) ou "livre" (x/y/l/a).
5
- ========================================================================== */
6
-
7
- /* ── A QUEBRA DA LEGENDA ─────────────────────────────────────────────────
8
- * Legenda de vídeo tem regra de ofício, e ela não é opinião: linha curta o
9
- * bastante para o olho pegar de relance (~38 caracteres em 1080p), no máximo
10
- * três linhas, e a quebra caindo em espaço — nunca no meio da palavra.
11
- *
12
- * E as linhas saem EQUILIBRADAS. Encher a primeira até o talo e deixar duas
13
- * palavras na segunda é o que faz a legenda parecer torta mesmo estando
14
- * centralizada — foi essa a reclamação.
15
- */
16
- var LEGENDA_LARGURA = 38;
17
-
18
- /** Quebra gulosa numa largura dada: enche a linha até não caber mais. */
19
- function envolver(palavras, largura) {
20
- var linhas = [];
21
- var atual = "";
22
- for (var i = 0; i < palavras.length; i++) {
23
- var tenta = atual ? atual + " " + palavras[i] : palavras[i];
24
- if (atual && tenta.length > largura) { linhas.push(atual); atual = palavras[i]; }
25
- else atual = tenta;
26
- }
27
- if (atual) linhas.push(atual);
28
- return linhas;
29
- }
30
-
31
- function quebrarLegenda(texto) {
32
- var t = String(texto == null ? "" : texto).replace(/\s+/g, " ").trim();
33
- if (!t) return "";
34
- if (t.length <= LEGENDA_LARGURA) return t;
35
-
36
- var palavras = t.split(" ");
37
- var quantas = envolver(palavras, LEGENDA_LARGURA).length;
38
-
39
- /* O EQUILÍBRIO.
40
- *
41
- * A quebra gulosa enche a primeira linha até o talo e larga duas palavras na
42
- * última — é isso que faz a legenda parecer torta mesmo estando
43
- * centralizada, e foi essa a reclamação.
44
- *
45
- * A primeira tentativa foi dividir o comprimento pelo número de linhas e
46
- * cortar no alvo. Não serve: o resto se acumula na última, que estourava a
47
- * largura (41 caracteres numa régua de 38).
48
- *
49
- * O que serve é apertar a régua: procurar a MENOR largura que ainda cabe no
50
- * mesmo número de linhas. Aí nenhuma linha passa do teto — a régua só
51
- * encolheu — e todas ficam parecidas, porque foi o aperto que as igualou.
52
- */
53
- var melhor = LEGENDA_LARGURA;
54
- for (var w = LEGENDA_LARGURA - 1; w >= 12; w--) {
55
- if (envolver(palavras, w).length > quantas) break;
56
- melhor = w;
57
- }
58
- return envolver(palavras, melhor).join("\n");
59
- }
60
-
61
- var PAPEIS = {
62
- chapeu: { classe: "chapeu" },
63
- titulo: { tamanho: 84, peso: 660, altura: 1.06 },
64
- subtitulo: { tamanho: 46, peso: 620, altura: 1.14 },
65
- apoio: { tamanho: 30, peso: 400, altura: 1.45, cor: "var(--c-fraco)" },
66
- corpo: { tamanho: 25, peso: 400, altura: 1.5, classe: "corpo" },
67
- legenda: { tamanho: 18, peso: 400, altura: 1.45, cor: "var(--c-fraco)", classe: "corpo" },
68
- /* Legenda de FALA, para vídeo — outra coisa de `legenda`, que é a nota de
69
- rodapé de um slide. 18px sobre 1080p é ilegível no celular, e legenda de
70
- vídeo é lida em movimento, muitas vezes sem som. Grande, com peso, e com
71
- chapa escura atrás para funcionar sobre qualquer imagem. */
72
- fala: { tamanho: 52, peso: 640, altura: 1.25, classe: "fala" },
73
- };
74
-
75
- var LAYOUTS = ["capa", "titulo", "secao", "duas-colunas", "tres-colunas",
76
- "citacao", "numeros", "imagem-direita", "imagem-esquerda",
77
- "centro", "livre"];
78
-
79
- function _fugir(s) {
80
- return String(s == null ? "" : s)
81
- .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
82
- }
83
-
84
- function _rico(s) {
85
- return _fugir(s)
86
- .replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>")
87
- .replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<i>$2</i>")
88
- .replace(/`([^`]+)`/g, "<code>$1</code>");
89
- }
90
-
91
- function _px(v) { return (typeof v === "number" ? v + "px" : v); }
92
-
93
- /* Envolve cada palavra em um span, sem tocar nas tags que já existem. */
94
- function _porPalavra(html) {
95
- var p = 0;
96
- return html.split(/(<[^>]*>)/).map(function (parte) {
97
- if (parte.charAt(0) === "<") return parte; // tag: passa direto
98
- return parte.replace(/(\S+)/g, function (w) {
99
- return '<span class="pal" style="--p:' + (p++) + '">' + w + "</span>";
100
- });
101
- }).join("");
102
- }
103
-
104
- /* ------------------------------------------------------------------ fundo */
105
-
106
- function aplicarFundo(no, fundo) {
107
- if (!fundo) return;
108
- if (typeof fundo === "string") {
109
- // Os ONZE que o render.css define.
110
- //
111
- // A lista aqui tinha seis. Os outros cinco — aurora, raios, ondas,
112
- // nevoa, xadrez — caíam no `else` e viravam `background: "aurora"`:
113
- // CSS inválido, fundo NENHUM. E são justamente os que a spec recomenda
114
- // para vídeo, então toda cena que pedia aurora ou raios saía chapada.
115
- // Era metade do "os vídeos ficaram todos iguais".
116
- if (/^(malha|brilho|grade|pontos|suave|vinheta|aurora|nevoa|ondas|raios|xadrez)$/.test(fundo)) {
117
- no.setAttribute("data-fundo", fundo);
118
- } else if (fundo === "marca") {
119
- /* A cor da casa, chapada e sem efeito nenhum. Num anúncio de marca é o
120
- fundo mais usado de todos — e escrevê-lo como `"#21C25E"` obrigaria a
121
- repetir o hexadecimal em cada cena, que é como uma delas acaba com a
122
- cor de ontem depois de a marca mudar de tom. */
123
- no.style.background = "var(--c-tom)";
124
- no.setAttribute("data-fundo", "marca");
125
- } else no.style.background = fundo;
126
- return;
127
- }
128
- if (fundo.tipo === "gradiente") {
129
- no.style.background = "linear-gradient(" + (fundo.angulo != null ? fundo.angulo : 160) + "deg, " +
130
- (fundo.de || "#111") + ", " + (fundo.para || "#333") + ")";
131
- } else if (fundo.imagem) {
132
- no.style.backgroundImage = "url('" + fundo.imagem + "')";
133
- no.style.backgroundSize = "cover";
134
- no.style.backgroundPosition = fundo.posicao || "center";
135
- if (fundo.escurecer) {
136
- var v = document.createElement("div");
137
- v.className = "cobertura";
138
- v.style.background = "rgba(4,6,10," + fundo.escurecer + ")";
139
- no.appendChild(v);
140
- }
141
- }
142
- }
143
-
144
- /* --------------------------------------------------------------- gráficos */
145
-
146
- function svgGrafico(e, l, a) {
147
- /* Os dados podem chegar como "dados" ou como "itens", e o tipo como
148
- "grafico", "estilo" ou "tipo".
149
-
150
- O nome certo é `dados` + `grafico`, e é o que a spec ensina. Mas quem
151
- escreve um gráfico — a IA ou uma pessoa — chuta `itens`, porque é o
152
- campo de TODOS os outros elementos da mesma spec (lista, cartoes,
153
- numeros usam `itens`). Com o nome trocado esta função devolvia string
154
- vazia: o gráfico simplesmente não existia na tela, sem erro nenhum.
155
- Aceitar as duas grafias custa uma linha e evita a falha muda. */
156
- var bruto = e.dados || e.itens || [];
157
- var dados = bruto.map(function (d) {
158
- return Array.isArray(d) ? { r: d[0], v: +d[1] || 0 } : { r: d.rotulo, v: +d.valor || 0 };
159
- });
160
- if (!dados.length) return "";
161
- var cores = e.cores || ["var(--c-tom)", "var(--c-tom2)", "#7c9cff", "#2fbf7f", "#f472b6", "#facc15"];
162
- var max = Math.max.apply(null, dados.map(function (d) { return d.v; })) || 1;
163
- var tipo = e.grafico || e.estilo || e.tipoGrafico || "barras";
164
- var pad = { e: 8, d: 8, t: 14, b: 42 };
165
- var iw = l - pad.e - pad.d, ih = a - pad.t - pad.b;
166
- var p = ['<svg viewBox="0 0 ' + l + " " + a + '" width="' + l + '" height="' + a + '">'];
167
-
168
- if (tipo === "pizza" || tipo === "rosca") {
169
- var total = dados.reduce(function (s, d) { return s + d.v; }, 0) || 1;
170
- var cx = l / 2, cy = a / 2 - 8, r = Math.min(l, a) / 2 - 34, ang = -Math.PI / 2;
171
- dados.forEach(function (d, i) {
172
- var fim = ang + (d.v / total) * Math.PI * 2;
173
- var x1 = cx + r * Math.cos(ang), y1 = cy + r * Math.sin(ang);
174
- var x2 = cx + r * Math.cos(fim), y2 = cy + r * Math.sin(fim);
175
- p.push('<path d="M' + cx + " " + cy + " L" + x1 + " " + y1 +
176
- " A" + r + " " + r + " 0 " + (fim - ang > Math.PI ? 1 : 0) + " 1 " + x2 + " " + y2 +
177
- ' Z" fill="' + cores[i % cores.length] + '"/>');
178
- ang = fim;
179
- });
180
- if (tipo === "rosca") p.push('<circle cx="' + cx + '" cy="' + cy + '" r="' + (r * 0.58) + '" fill="var(--c-fundo)"/>');
181
- } else if (tipo === "linha" || tipo === "area") {
182
- var passo = dados.length > 1 ? iw / (dados.length - 1) : 0;
183
- var pts = dados.map(function (d, i) {
184
- return [pad.e + i * passo, pad.t + ih - (d.v / max) * ih];
185
- });
186
- var caminho = pts.map(function (q, i) { return (i ? "L" : "M") + q[0] + " " + q[1]; }).join(" ");
187
- if (tipo === "area") {
188
- p.push('<path d="' + caminho + " L" + pts[pts.length - 1][0] + " " + (pad.t + ih) +
189
- " L" + pts[0][0] + " " + (pad.t + ih) + ' Z" fill="' + cores[0] + '" opacity=".18"/>');
190
- }
191
- p.push('<path d="' + caminho + '" fill="none" stroke="' + cores[0] + '" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>');
192
- pts.forEach(function (q) { p.push('<circle cx="' + q[0] + '" cy="' + q[1] + '" r="6" fill="' + cores[0] + '"/>'); });
193
- } else {
194
- var larg = iw / dados.length;
195
- var barra = Math.min(larg * 0.68, 130); // barra larga demais vira bloco
196
- dados.forEach(function (d, i) {
197
- var h = (d.v / max) * ih;
198
- var x = pad.e + i * larg + (larg - barra) / 2;
199
- p.push('<rect x="' + x + '" y="' + (pad.t + ih - h) + '" width="' + barra +
200
- '" height="' + Math.max(h, 2) + '" rx="7" fill="' + cores[i % cores.length] + '"/>');
201
- });
202
- }
203
-
204
- if (tipo !== "pizza" && tipo !== "rosca") {
205
- var larg2 = iw / dados.length;
206
- dados.forEach(function (d, i) {
207
- p.push('<text x="' + (pad.e + i * larg2 + larg2 / 2) + '" y="' + (a - 16) +
208
- '" text-anchor="middle" fill="var(--c-fraco)" font-size="17" font-family="inherit">' +
209
- _fugir(d.r) + "</text>");
210
- });
211
- } else {
212
- dados.forEach(function (d, i) {
213
- p.push('<rect x="' + (16 + i * 132) + '" y="' + (a - 26) + '" width="12" height="12" rx="3" fill="' + cores[i % cores.length] + '"/>');
214
- p.push('<text x="' + (34 + i * 132) + '" y="' + (a - 15) +
215
- '" fill="var(--c-fraco)" font-size="16" font-family="inherit">' + _fugir(d.r) + "</text>");
216
- });
217
- }
218
- p.push("</svg>");
219
- return p.join("");
220
- }
221
-
222
- /* -------------------------------------------------------------- elementos */
223
-
224
- /* O KIT DE ANÚNCIO — os quatro últimos da lista abaixo.
225
- *
226
- * Nasceram de um vídeo de referência mandado pelo dono — um comercial de
227
- * fintech — com uma frase só: "ainda não tá bom, tá genérico. Quero nesse
228
- * nível."
229
- *
230
- * Medindo a peça de hoje contra aquela, o que faltava não era tema nem cor:
231
- * tema claro e `marca.cor` já existiam e resolvem o mundo da marca. O que não
232
- * existia era o PRODUTO. Um anúncio de cartão mostra o cartão, mostra o app
233
- * funcionando dentro do celular, mostra a linha de produtos e assina no fim.
234
- * Com o vocabulário antigo, "o cartão" era um retângulo verde com a palavra
235
- * "cartão" escrita dentro — e é exatamente isso que se lê como genérico.
236
- *
237
- * ── A LISTA ABAIXO É LIDA POR UM TESTE, e por isso não tem comentário dentro.
238
- * O `vocabulario.test.js` recorta este objeto com uma expressão regular e
239
- * pega as palavras: qualquer texto entre as chaves vira "tipo que o servidor
240
- * precisa aceitar", e o teste passa a cobrar `fintech`, `genérico` e `dono`.
241
- * Comentário sobre os tipos mora AQUI FORA. */
242
- var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
243
- citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1,
244
- objeto3d: 1, clipe: 1,
245
- selo: 1, progresso: 1, avatar: 1, dispositivo: 1, conversa: 1, passos: 1,
246
- opcoes: 1,
247
- cartao: 1, app: 1, vitrine: 1, assinatura: 1 };
248
- /* Os sete últimos são o vocabulário do vídeo de referência: pílula de estado,
249
- trilha de progresso, foto de pessoa, celular/notebook com conteúdo dentro,
250
- balões de conversa, etapas com uma acesa, e pergunta com alternativas.
251
-
252
- O comentário mora AQUI FORA porque test/vocabulario.test.js lê esta lista
253
- com expressão regular, comparando com a do servidor — e palavra de
254
- comentário dentro das chaves entrava na lista como se fosse um tipo. */
255
-
256
- /* ── chaves de animação ──────────────────────────────────────────────────
257
- Transforma a lista de instantes de um elemento numa regra @keyframes de
258
- verdade e prende ela no elemento.
259
-
260
- Os nomes precisam ser únicos por página: o mesmo documento aparece várias
261
- vezes ao mesmo tempo (palco, miniaturas do painel, clipes da tira), e
262
- dois @keyframes de mesmo nome com conteúdo diferente se atropelariam —
263
- a última regra injetada venceria em TODAS as cópias. */
264
- var _nChave = 0;
265
-
266
- function _numero(v, padrao) {
267
- return typeof v === "number" && isFinite(v) ? v : padrao;
268
- }
269
-
270
- function aplicarChaves(no, e, ctx) {
271
- var chaves = e.chaves.slice().sort(function (a, b) {
272
- return _numero(a.t, 0) - _numero(b.t, 0);
273
- });
274
-
275
- // Um campo OMITIDO numa chave herda o valor da chave anterior — não pula
276
- // para um padrão fixo. Sem isto, uma chave que só mexe em `opacidade`
277
- // zerava a `escala` implícita: o elemento nascia com scale(0) e sumia da
278
- // tela, e a animação parecia "bugada e atrasada". Herdar é o que a pessoa
279
- // espera: "o que eu não toquei, fica como estava". O primeiro valor de
280
- // cada campo, quando nem a primeira chave o declara, é o neutro (escala e
281
- // opacidade em 1, deslocamento e giro em 0) — nunca zero para escala.
282
- // giroX/giroY/prof são o 3D: viram rotateX/rotateY/translateZ com uma
283
- // perspectiva na frente. É o que faz um cartão virar de verdade em vez de
284
- // só encolher — pedido do dono: "quero que a IA faça coisas 3D, mexa com
285
- // keyframes". escalaX/escalaY esticam num eixo só (a "gota" que se
286
- // achata ao pousar). `prof`, não `z`: `z` no elemento já é a ordem das
287
- // camadas (zIndex), e o mesmo nome com dois sentidos é armadilha.
288
- var NEUTRO = { x: 0, y: 0, escala: 1, escalaX: 1, escalaY: 1, giro: 0, giroX: 0, giroY: 0, prof: 0, opacidade: 1, desfoque: 0 };
289
- var CAMPOS = ["x", "y", "escala", "escalaX", "escalaY", "giro", "giroX", "giroY", "prof", "opacidade", "desfoque"];
290
- var herdado = {};
291
- CAMPOS.forEach(function (c) {
292
- // semente: primeiro valor declarado em qualquer chave, senão o neutro.
293
- for (var i = 0; i < chaves.length; i++) {
294
- if (typeof chaves[i][c] === "number" && isFinite(chaves[i][c])) { herdado[c] = chaves[i][c]; return; }
295
- }
296
- herdado[c] = NEUTRO[c];
297
- });
298
-
299
- // Quais campos esta animação USA. Decidido UMA vez, ANTES das chaves, e é
300
- // o que faz o navegador interpolar: a lista de `transform` precisa ter as
301
- // MESMAS funções, na mesma ordem, em toda chave. Emitir só o que é
302
- // diferente de zero deixava a chave do meio de `giroY: -90 → 0 → 12` sem
303
- // perspective/rotateY — listas incompatíveis, o navegador desiste da
304
- // interpolação por função e cai na matriz, e a perspectiva se perde no meio
305
- // do giro. Vale igual para `rotate(0deg)` e para o blur.
306
- var usado = {};
307
- CAMPOS.forEach(function (c) {
308
- usado[c] = herdado[c] !== NEUTRO[c] || chaves.some(function (k) {
309
- return typeof k[c] === "number" && isFinite(k[c]) && k[c] !== NEUTRO[c];
310
- });
311
- });
312
- // O MESMO juiz que decide o preserve-3d (linha do `transformStyle`): se os
313
- // dois discordarem, sobra elemento achatado com perspectiva ou o contrário.
314
- var tem3d = tem3dEm(chaves);
315
-
316
- var nome = "ch" + (++_nChave);
317
- var passos = chaves.map(function (k) {
318
- CAMPOS.forEach(function (c) {
319
- if (typeof k[c] === "number" && isFinite(k[c])) herdado[c] = k[c];
320
- });
321
- var pct = Math.max(0, Math.min(1, _numero(k.t, 0))) * 100;
322
- var mov = [];
323
- // x e y de uma chave são DESLOCAMENTO a partir de onde o elemento está.
324
- // Assim a chave continua valendo se a pessoa arrastar o elemento no
325
- // palco depois — o contrário obrigaria a refazer todas as chaves.
326
- var dx = herdado.x, dy = herdado.y;
327
- // A perspectiva vai NA transformação, não no pai: o pai é a cena, e uma
328
- // perspectiva lá mudaria todos os elementos.
329
- if (tem3d) mov.push("perspective(1200px)");
330
- if (usado.x || usado.y) mov.push("translate(" + dx + "px," + dy + "px)");
331
- if (usado.prof) mov.push("translateZ(" + herdado.prof + "px)");
332
- if (usado.escala) mov.push("scale(" + herdado.escala + ")");
333
- if (usado.escalaX || usado.escalaY) mov.push("scale(" + herdado.escalaX + "," + herdado.escalaY + ")");
334
- if (usado.giroX) mov.push("rotateX(" + herdado.giroX + "deg)");
335
- if (usado.giroY) mov.push("rotateY(" + herdado.giroY + "deg)");
336
- if (usado.giro) mov.push("rotate(" + herdado.giro + "deg)");
337
-
338
- var linhas = ["transform:" + (mov.length ? mov.join(" ") : "none")];
339
- linhas.push("opacity:" + herdado.opacidade);
340
- if (usado.desfoque) {
341
- linhas.push("filter:blur(" + Math.max(0, herdado.desfoque) + "px)");
342
- }
343
- return pct.toFixed(3) + "%{" + linhas.join(";") + "}";
344
- }).join("");
345
-
346
- // A regra vai na RAIZ da cena, não dentro do elemento.
347
- //
348
- // Presa no próprio elemento ela era apagada segundos depois: quem desenha
349
- // o conteúdo (texto, lista, número) escreve o innerHTML e leva junto tudo
350
- // que estivesse ali. O elemento continuava com `animation-name: ch1`
351
- // apontando para uma regra que não existia mais — a animação simplesmente
352
- // não acontecia, sem erro nenhum no console. A cena só recebe filhos, e
353
- // por isso a regra sobrevive lá.
354
- var folha = document.createElement("style");
355
- folha.textContent = "@keyframes " + nome + "{" + passos + "}";
356
- (ctx.raiz || no).appendChild(folha);
357
-
358
- var dur = _numero(ctx.duracao, 4);
359
- no.style.animationName = nome;
360
- no.style.animationDuration = dur + "s";
361
- no.style.animationTimingFunction = curvaDe(e.suavizar);
362
- no.style.animationFillMode = "both";
363
- no.style.animationIterationCount = e.repetir ? "infinite" : "1";
364
- if (e.repetir && e.vaiEVolta) no.style.animationDirection = "alternate";
365
- if (tem3dEm(chaves)) no.style.transformStyle = "preserve-3d";
366
- }
367
-
368
- function tem3dEm(chaves) {
369
- for (var i = 0; i < chaves.length; i++) {
370
- if (chaves[i].giroX || chaves[i].giroY || chaves[i].prof) return true;
371
- }
372
- return false;
373
- }
374
-
375
- /* As curvas com NOME. `suavizar: false` continua sendo linear; o padrão
376
- continua sendo a curva suave. Os nomes existem porque "elástico" e "salto"
377
- são o que separa motion de slide animado, e pedir cubic-bezier a quem
378
- escreve JSON é pedir chute. */
379
- var CURVAS = {
380
- suave: "cubic-bezier(.4,0,.2,1)",
381
- linear: "linear",
382
- entrada: "cubic-bezier(.4,0,1,1)", // acelera: sai devagar, chega rápido
383
- saida: "cubic-bezier(0,0,.2,1)", // freia: chega devagar
384
- elastico: "cubic-bezier(.34,1.56,.64,1)", // passa do ponto e volta
385
- salto: "cubic-bezier(.68,-.55,.27,1.55)", // recua, dispara, passa e assenta
386
- degrau: "steps(6, end)", // quadro a quadro, para o "stop motion"
387
- };
388
- function curvaDe(suavizar) {
389
- if (suavizar === false) return CURVAS.linear;
390
- if (typeof suavizar === "string" && CURVAS[suavizar]) return CURVAS[suavizar];
391
- if (typeof suavizar === "string" && /^(cubic-bezier|steps)\(/.test(suavizar)) return suavizar;
392
- return CURVAS.suave;
393
- }
394
-
395
- /* ── identidade da marca ─────────────────────────────────────────────────
396
- "Ele não escolhe as cores dos textos, não escolhe tema, não escolhe uma
397
- imagem de fundo, não coloca uma logo. Ficou horrível."
398
-
399
- O logo era o mais gritante: não havia NENHUM jeito de pôr um. Dava para
400
- subir a imagem e posicionar à mão, cena por cena — o que ninguém faz num
401
- vídeo de dez cenas, e a IA nunca fazia.
402
-
403
- `doc.marca` resolve de uma vez: o logo aparece em todas as cenas, no
404
- canto, no tamanho certo, sem entrar no fluxo do layout. E `marca.cor`
405
- define a cor de destaque do documento inteiro, que é o que dá identidade
406
- aos números, às barras do gráfico e aos detalhes.
407
-
408
- "marca": { "logo": "/midia/logo.png", "cor": "#ff5a36",
409
- "canto": "inferior-direita", "opacidade": 0.85 }
410
-
411
- `cena.semMarca: true` tira de uma cena específica — a capa costuma querer
412
- o logo grande no meio, não pequeno no canto. */
413
- var CANTOS = {
414
- "superior-esquerda": "top:5.2%;left:5%",
415
- "superior-direita": "top:5.2%;right:5%",
416
- "inferior-esquerda": "bottom:5.2%;left:5%",
417
- "inferior-direita": "bottom:5.2%;right:5%",
418
- };
419
-
420
- function aplicarMarca(no, cena, doc, H) {
421
- var m = doc && doc.marca;
422
- if (!m || typeof m !== "object") return;
423
- // A cor da marca vira a cor de destaque de tudo, a menos que a cena diga
424
- // outra — é ela que pinta número, barra de gráfico e detalhe.
425
- if (m.cor && !cena.tom) no.style.setProperty("--c-tom", m.cor);
426
- if (!m.logo || cena.semMarca) return;
427
-
428
- var caixa = document.createElement("div");
429
- caixa.className = "marca-dagua";
430
- caixa.style.cssText = "position:absolute;z-index:6;pointer-events:none;"
431
- + (CANTOS[m.canto] || CANTOS["inferior-direita"])
432
- + ";opacity:" + (m.opacidade != null ? m.opacidade : 0.85);
433
- var img = document.createElement("img");
434
- img.src = m.logo;
435
- img.alt = "";
436
- // Altura proporcional à tela, resolvida em PIXELS: a caixa é absoluta e
437
- // sem altura própria, e porcentagem dentro dela não resolveria — o logo
438
- // sairia no tamanho original, gigante num caso e sumido no outro.
439
- var alturaPx = Math.round((H || 1080) * ((m.altura || 4.6) / 100));
440
- img.style.cssText = "height:" + alturaPx + "px;width:auto;display:block";
441
- caixa.appendChild(img);
442
- no.appendChild(caixa);
443
- }
444
-
445
- /* ── objetos 3D ──────────────────────────────────────────────────────────
446
- "Quero que tu coloque a função ali no vídeo, na apresentação de slides,
447
- pra fazer objeto 3D com animação, tudo assim, bem top."
448
-
449
- Isto é 3D de verdade — perspectiva, faces, profundidade — feito com
450
- transform 3D do próprio navegador. Sem WebGL e sem biblioteca: o Studio
451
- não tem dependência nenhuma e não vai passar a ter por causa disto. O que
452
- dá para fazer assim cobre o que um vídeo institucional pede: um cubo
453
- girando com o logo nas faces, um cartão com espessura, uma pilha de
454
- camadas em profundidade.
455
-
456
- { "tipo": "objeto3d", "forma": "cubo", "tamanho": 320,
457
- "faces": ["Produto", "Dados", "IA"], "girar": 14 }
458
-
459
- `girar` é a volta completa em segundos; 0 deixa parado no ângulo dado.
460
- `faces` aceita texto ou URL de imagem. E como o elemento é um nó normal,
461
- as chaves de animação valem aqui também: dá para o cubo atravessar a cena
462
- girando. */
463
- var FORMAS_3D = { cubo: 1, caixa: 1, cartao: 1, prisma: 1, painel: 1 };
464
-
465
- function _face3d(conteudo, transform, cor) {
466
- var f = document.createElement("div");
467
- f.className = "face3d";
468
- f.style.transform = transform;
469
- if (cor) f.style.background = cor;
470
- if (/^(https?:|\/)/.test(String(conteudo || ""))) {
471
- var im = document.createElement("img");
472
- im.src = conteudo;
473
- im.alt = "";
474
- f.appendChild(im);
475
- } else if (conteudo) {
476
- f.textContent = conteudo;
477
- }
478
- return f;
479
- }
480
-
481
- function montar3d(no, e) {
482
- var forma = FORMAS_3D[e.forma] ? e.forma : "cubo";
483
- var t = +e.tamanho || 320;
484
- // A caixa e o prisma são o cubo achatado num eixo: uma medida só muda a
485
- // silhueta inteira, e o autor não precisa pensar em seis faces.
486
- var prof = forma === "cartao" || forma === "painel" ? Math.max(6, Math.round(t * 0.045))
487
- : (forma === "caixa" ? Math.round(t * 0.62) : t);
488
- var faces = Array.isArray(e.faces) ? e.faces : [];
489
- var cores = Array.isArray(e.cores) ? e.cores : [];
490
-
491
- no.classList.add("obj3d", "f-" + forma);
492
- no.style.width = _px(t);
493
- no.style.height = _px(forma === "cartao" ? Math.round(t * 0.62) : t);
494
- no.style.perspective = _px(e.perspectiva || t * 3.4);
495
-
496
- var corpo = document.createElement("div");
497
- corpo.className = "corpo3d";
498
- corpo.style.width = _px(t);
499
- corpo.style.height = _px(forma === "cartao" ? Math.round(t * 0.62) : t);
500
-
501
- var meia = prof / 2;
502
- var h = forma === "cartao" ? Math.round(t * 0.62) : t;
503
- var lados = [
504
- ["translateZ(" + meia + "px)", 0],
505
- ["rotateY(180deg) translateZ(" + meia + "px)", 1],
506
- ["rotateY(90deg) translateZ(" + (t / 2) + "px)", 2],
507
- ["rotateY(-90deg) translateZ(" + (t / 2) + "px)", 3],
508
- ["rotateX(90deg) translateZ(" + (h / 2) + "px)", 4],
509
- ["rotateX(-90deg) translateZ(" + (h / 2) + "px)", 5],
510
- ];
511
- // Cartão e painel têm só frente e verso: as bordas viram espessura, e
512
- // desenhar seis faces num objeto de 6px de profundidade só faria sujeira.
513
- var quantas = (forma === "cartao" || forma === "painel") ? 2 : 6;
514
- for (var i = 0; i < quantas; i++) {
515
- var lado = lados[i];
516
- var f = _face3d(faces[lado[1]] != null ? faces[lado[1]] : (faces[0] || ""),
517
- lado[0], cores[lado[1] % (cores.length || 1)]);
518
- // As laterais são estreitas (a profundidade) e o topo/base são baixos.
519
- //
520
- // Só mudar a medida não bastava: a face é `inset: 0`, então encostava na
521
- // borda esquerda em vez de ficar no meio — e a rotação, que gira em
522
- // torno do centro, abria o objeto. O cubo saía com as faces soltas, sem
523
- // fechar. Reposicionar a face no centro antes de girar fecha a caixa.
524
- if (i >= 2 && i < 4) {
525
- f.style.width = _px(prof);
526
- f.style.left = _px(Math.round((t - prof) / 2));
527
- }
528
- if (i >= 4) {
529
- f.style.height = _px(prof);
530
- f.style.top = _px(Math.round((h - prof) / 2));
531
- }
532
- corpo.appendChild(f);
533
- }
534
-
535
- var giro = e.girar == null ? 14 : +e.girar;
536
- var inclina = e.inclinar == null ? -14 : +e.inclinar;
537
- if (giro > 0) {
538
- corpo.style.animation = "giro3d " + giro + "s linear infinite";
539
- corpo.style.setProperty("--inc", inclina + "deg");
540
- } else {
541
- corpo.style.transform = "rotateX(" + inclina + "deg) rotateY(" + (e.angulo || -28) + "deg)";
542
- }
543
- no.appendChild(corpo);
544
- }
545
-
546
- function renderElemento(e, ctx) {
547
- var no = document.createElement("div");
548
- var tipo = e.tipo || "texto";
549
- /* Qual elemento do documento é este nó.
550
-
551
- O editor precisava saber, e antes deduzia pela ordem dos nós na tela.
552
- A dedução estava errada: os layouts de fluxo separam cabeçalho de corpo
553
- e desenham o cabeçalho primeiro, então a ordem na tela NÃO é a ordem do
554
- documento. O efeito era clicar num texto e o editor selecionar outro —
555
- e mexer no elemento errado. O índice vem carimbado daqui. */
556
- if (ctx && ctx.ordem && ctx.ordem.has(e)) no.dataset.i = ctx.ordem.get(e);
557
- /* Tipo inventado ("titulo", "subtitulo"...) caía num default sem estilo:
558
- texto de 16px num canvas de 1920 — invisível — ou div vazio quando o
559
- conteúdo estava noutro campo. O servidor novo recusa; documentos velhos
560
- e outros clientes ganham a leitura generosa: vira texto de verdade. */
561
- if (!TIPOS_CONHECIDOS[tipo]) {
562
- e = { tipo: "texto", papel: /titulo|title/i.test(tipo) ? "titulo" : (e.papel || null),
563
- texto: e.texto || e.conteudo || e.titulo || e.valor || e.legenda || "",
564
- tamanho: e.tamanho, estilo: e.estilo, anim: e.anim, x: e.x, y: e.y, l: e.l, a: e.a };
565
- tipo = "texto";
566
- }
567
- no.className = "el " + tipo;
568
- no.dataset.tipo = tipo;
569
-
570
- /* posição — absoluta só quando o elemento traz coordenadas */
571
- var absoluto = e.x != null || e.y != null;
572
- if (absoluto) {
573
- no.style.position = "absolute";
574
- no.style.left = _px(e.x || 0);
575
- no.style.top = _px(e.y || 0);
576
- if (e.l != null) no.style.width = _px(e.l);
577
- if (e.a != null) {
578
- // em texto a altura é piso, não gaiola: assim nada é cortado nem colide
579
- var elastico = tipo === "texto" || tipo === "lista" || tipo === "citacao" || tipo === "cartoes";
580
- no.style[elastico ? "minHeight" : "height"] = _px(e.a);
581
- }
582
- } else {
583
- no.style.position = "relative";
584
- }
585
- if (e.rotacao) no.style.transform = "rotate(" + e.rotacao + "deg)";
586
- if (e.opacidade != null) no.style.opacity = e.opacidade;
587
- if (e.sombra) no.style.boxShadow = e.sombra === true ? "0 30px 70px -25px rgba(0,0,0,.6)" : e.sombra;
588
- if (e.z != null) no.style.zIndex = e.z;
589
- /* MATERIAL: "vidro" é o liquid glass — translúcido, desfocando o que está
590
- atrás, com bisel de luz em cima e o reflexo diagonal. Vale em QUALQUER
591
- elemento (texto, forma, ícone, imagem, lista, cartões), por isso mora
592
- aqui e não num tipo. O CSS faz o resto por `data-material`. */
593
- /* A profundidade do elemento na cena: quanto ele acompanha a câmera.
594
- >1 vem à frente (anda mais), <1 fica ao fundo (anda menos). É o que
595
- transforma um zoom num plano com camadas. */
596
- if (e.paralaxe != null && isFinite(Number(e.paralaxe))) no.dataset.paralaxe = Number(e.paralaxe);
597
-
598
- if (e.material) {
599
- no.dataset.material = e.material;
600
- if (e.raio != null) no.style.borderRadius = _px(e.raio);
601
- }
602
-
603
- /* animação */
604
- var an = e.anim || ctx.animPadrao;
605
- if (an && an !== "nenhuma" && !(e.chaves && e.chaves.length > 1)) {
606
- no.setAttribute("data-an", typeof an === "string" ? an : (an.entrada || "subir"));
607
- no.style.setProperty("--atraso", (typeof an === "object" && an.atraso != null ? an.atraso : ctx.i * 0.09 + 0.1));
608
- }
609
-
610
- /* ── chaves de animação ────────────────────────────────────────────────
611
- Antes um elemento só sabia ENTRAR: escolhia-se um efeito de entrada e
612
- acabava ali. Não dava para fazer nada percorrer a cena, crescer devagar
613
- ou sumir no fim — que é o que separa animação de slide com efeito.
614
-
615
- Uma chave é um instante: `{t: 0.5, x: 200, escala: 1.2}` diz onde o
616
- elemento está na metade da cena. Duas ou mais viram uma animação CSS de
617
- verdade, gerada aqui, com a duração da cena. O navegador interpola;
618
- nós só descrevemos os pontos.
619
-
620
- Quem tem chaves não recebe animação de entrada: as duas brigariam pelo
621
- mesmo transform, e a chave é a mais específica das duas. */
622
- if (ctx.animar && e.chaves && e.chaves.length > 1) {
623
- aplicarChaves(no, e, ctx);
624
- }
625
-
626
- switch (tipo) {
627
- case "texto": {
628
- var papel = PAPEIS[e.papel] || PAPEIS.corpo;
629
- no.classList.add("txt");
630
- if (papel.classe) no.classList.add(papel.classe);
631
- // tamanho explícito manda; sem ele, o padrão cresce no formato vertical
632
- no.style.fontSize = _px(e.tamanho || Math.round((papel.tamanho || 25) * (ctx.escalaTipo || 1)));
633
- if (e.peso || papel.peso) no.style.fontWeight = e.peso || papel.peso;
634
- if (papel.altura) no.style.lineHeight = papel.altura;
635
- no.style.color = e.cor || papel.cor || "";
636
- /* LEGENDA DE FALA É SEMPRE CENTRALIZADA, e o `alinhar` do modelo não
637
- manda nela. Legenda de vídeo alinhada à esquerda não existe em lugar
638
- nenhum — ela é lida em movimento, e o olho volta para o meio da tela.
639
- Bastava o modelo escrever `alinhar: "esquerda"` uma vez para a legenda
640
- sair torta, e ele escrevia. */
641
- if (e.alinhar && e.papel !== "fala") {
642
- no.style.textAlign = e.alinhar;
643
- no.style.alignItems = e.alinhar === "centro" ? "center" : (e.alinhar === "direita" ? "flex-end" : "flex-start");
644
- }
645
- if (e.fonte === "mono") no.style.fontFamily = "var(--f-mono)";
646
- if (e.fonte === "display") no.style.fontFamily = "var(--f-display)";
647
- if (e.maiusculas) no.style.textTransform = "uppercase";
648
- if (e.espacamento != null) no.style.letterSpacing = e.espacamento + "em";
649
- if (e.altura != null) no.style.lineHeight = e.altura;
650
- if (e.estilo) no.setAttribute("data-estilo", e.estilo);
651
-
652
- /* A LEGENDA SE QUEBRA SOZINHA.
653
- "A IA não consegue formatar bem" — e não vai conseguir: ela escreve a
654
- frase, não a diagramação. Quem sabe quantos caracteres cabem numa
655
- linha de 1080p é este código, e é ele que quebra.
656
-
657
- Sem isto a legenda saía como parágrafo: cinco linhas subindo pela
658
- tela, tapando o rosto de quem fala. */
659
- var corpoTexto = _rico(e.papel === "fala" ? quebrarLegenda(e.texto) : e.texto);
660
- if (e.papel === "fala" && !e.tamanho) {
661
- /* Legenda longa ENCOLHE em vez de crescer para cima. Três linhas com o
662
- corpo de uma linha comem metade do quadro, e o rosto de quem fala
663
- fica atrás delas.
664
-
665
- Não se corta palavra: perder o que foi dito é pior que letra menor.
666
- O piso de 62% é onde a letra ainda se lê num celular. */
667
- var nLinhas = String(quebrarLegenda(e.texto)).split("\n").length;
668
- var base = (PAPEIS.fala.tamanho || 52) * (ctx.escalaTipo || 1);
669
- var fator = Math.max(0.62, 1 - (nLinhas - 1) * 0.10);
670
- if (nLinhas > 1) no.style.fontSize = _px(Math.round(base * fator));
671
- }
672
-
673
- // marcador e fita precisam envolver cada linha para o fundo acompanhar
674
- if (e.estilo === "marcador" || e.estilo === "fita") {
675
- corpoTexto = corpoTexto.split("\n").map(function (l) {
676
- return l.trim() ? '<span class="linha">' + l + "</span>" : "";
677
- }).join("\n");
678
- }
679
-
680
- // palavra a palavra: cada palavra vira um span com atraso próprio
681
- var animAtual = e.anim || ctx.animPadrao;
682
- if (animAtual === "palavra") corpoTexto = _porPalavra(corpoTexto);
683
-
684
- no.innerHTML = corpoTexto;
685
- break;
686
- }
687
-
688
- case "lista": {
689
- if (e.numerada) no.classList.add("numerada");
690
- if (e.marcador) no.setAttribute("data-marcador", e.marcador);
691
- (e.itens || []).forEach(function (it, k) {
692
- var d = document.createElement("div");
693
- d.className = "item";
694
- if (e.tamanho) d.style.fontSize = _px(e.tamanho);
695
- d.innerHTML = _rico(it);
696
- if (e.revelar) {
697
- d.setAttribute("data-an", "subir");
698
- d.style.setProperty("--atraso", (e.atrasoBase != null ? e.atrasoBase : 0.2) + k * (e.intervalo || 0.14));
699
- }
700
- no.appendChild(d);
701
- });
702
- break;
703
- }
704
-
705
- case "cartoes": {
706
- var n = (e.itens || []).length;
707
- no.style.gridTemplateColumns = "repeat(" + Math.min(n || 1, e.colunas || (n > 4 ? 3 : n)) + ", 1fr)";
708
- (e.itens || []).forEach(function (it) {
709
- var c = document.createElement("div");
710
- c.className = "cartao";
711
- var t = typeof it === "string" ? it.split("|") : [it.titulo, it.texto];
712
- c.innerHTML = "<b>" + _rico((t[0] || "").trim()) + "</b>" +
713
- (t[1] ? "<span>" + _rico(String(t[1]).trim()) + "</span>" : "");
714
- no.appendChild(c);
715
- });
716
- break;
717
- }
718
-
719
- /* ── selo ────────────────────────────────────────────────────────────
720
- A pílula pequena, em versalete, que diz onde a cena está: "AVANÇADO",
721
- "MÓDULO 2 DE 5", "RESULTADO 4". Custa quase nada e é o que separa uma
722
- cena que parece produzida de uma cena que parece um slide.
723
-
724
- Antes disso a saída era um `texto` com papel "chapeu", que é só letra
725
- menor — sem a pílula, sem a marcação de estado. */
726
- case "selo": {
727
- no.textContent = "";
728
- if (e.icone) {
729
- var ic = document.createElement("span");
730
- ic.className = "selo-icone";
731
- ic.textContent = e.icone;
732
- no.appendChild(ic);
733
- }
734
- var rot = document.createElement("span");
735
- rot.textContent = e.texto || e.rotulo || "";
736
- no.appendChild(rot);
737
- if (e.estilo) no.setAttribute("data-estilo", e.estilo); // "vivo" | "solto"
738
- break;
739
- }
740
-
741
- /* ── progresso ───────────────────────────────────────────────────────
742
- "4 de 15", com os pontinhos. Diz ao espectador que existe um caminho e
743
- onde ele está nele — é o que faz um vídeo de dez cenas não parecer dez
744
- cenas soltas. */
745
- case "progresso": {
746
- var total = Math.max(1, Math.min(40, Number(e.total) || 1));
747
- var atual = Math.max(1, Math.min(total, Number(e.atual) || 1));
748
- if (e.numero !== false) {
749
- var num = document.createElement("b");
750
- num.textContent = atual + "/" + total;
751
- no.appendChild(num);
752
- }
753
- var pontos = document.createElement("span");
754
- pontos.className = "pontos";
755
- for (var pi = 1; pi <= total; pi++) {
756
- var pt = document.createElement("i");
757
- if (pi === atual) pt.className = "aqui";
758
- else if (pi < atual) pt.className = "feito";
759
- pontos.appendChild(pt);
760
- }
761
- no.appendChild(pontos);
762
- break;
763
- }
764
-
765
- /* ── avatar ──────────────────────────────────────────────────────────
766
- A foto redonda com o anel aceso. É como se apresenta uma PESSOA — e
767
- vídeo com rosto retém mais que vídeo sem. Antes só existia `imagem`,
768
- que é retângulo. */
769
- case "avatar": {
770
- var d = e.tamanho || 260;
771
- no.style.width = _px(d);
772
- no.style.height = _px(d);
773
- if (e.src) {
774
- var foto = document.createElement("img");
775
- foto.src = e.src;
776
- foto.alt = e.nome || "";
777
- no.appendChild(foto);
778
- } else {
779
- // Sem foto, as iniciais — melhor que um buraco cinza.
780
- var ini = document.createElement("b");
781
- // Em px, calculado do diâmetro. Em `%` o navegador mede contra a
782
- // fonte do PAI, não contra a caixa: as iniciais saíam com 8px dentro
783
- // de um círculo de 280 — um pontinho no meio do nada.
784
- ini.style.fontSize = _px(Math.round(d * 0.34));
785
- ini.textContent = String(e.nome || "?").trim().split(/\s+/)
786
- .slice(0, 2).map(function (w) { return w[0]; }).join("").toUpperCase();
787
- no.appendChild(ini);
788
- }
789
- break;
790
- }
791
-
792
- /* ── dispositivo ─────────────────────────────────────────────────────
793
- O celular, o notebook ou a janela de navegador com conteúdo DENTRO.
794
- É o enquadramento que faz uma tela parecer uma tela em vez de um
795
- retângulo de texto — e o vídeo de referência usa isso o tempo todo,
796
- com a conversa acontecendo dentro do celular.
797
-
798
- O conteúdo é `dentro`: uma lista de elementos comuns, renderizados
799
- recursivamente. Assim qualquer coisa cabe ali — conversa, lista,
800
- imagem — sem inventar um vocabulário só para o miolo. */
801
- case "dispositivo": {
802
- var forma = /^(celular|notebook|navegador|tablet)$/.test(e.forma || "")
803
- ? e.forma : "celular";
804
- no.setAttribute("data-forma", forma);
805
- if (e.altura) no.style.height = _px(e.altura);
806
- if (e.largura) no.style.width = _px(e.largura);
807
-
808
- var moldura = document.createElement("div");
809
- moldura.className = "moldura";
810
-
811
- if (forma === "navegador") {
812
- var barra = document.createElement("div");
813
- barra.className = "barra-nav";
814
- barra.innerHTML = "<i></i><i></i><i></i>"
815
- + (e.endereco ? '<span class="url">' + _fugir(e.endereco) + "</span>" : "");
816
- moldura.appendChild(barra);
817
- }
818
-
819
- var tela = document.createElement("div");
820
- tela.className = "tela";
821
- if (e.imagem) {
822
- tela.style.backgroundImage = "url('" + e.imagem + "')";
823
- tela.style.backgroundSize = "cover";
824
- tela.style.backgroundPosition = "center";
825
- }
826
- (e.dentro || []).forEach(function (filho) {
827
- if (!filho || !TIPOS_CONHECIDOS[filho.tipo]) return;
828
- ctx.i++;
829
- tela.appendChild(renderElemento(filho, ctx));
830
- });
831
- moldura.appendChild(tela);
832
- no.appendChild(moldura);
833
-
834
- if (forma === "notebook") {
835
- var base = document.createElement("div");
836
- base.className = "base";
837
- no.appendChild(base);
838
- }
839
- break;
840
- }
841
-
842
- /* ── conversa ────────────────────────────────────────────────────────
843
- Balões de pergunta e resposta. É a forma mais direta de mostrar como
844
- se usa uma ferramenta de IA — e é o que o vídeo de referência faz
845
- dentro do celular. `de: "eu"` alinha à direita; qualquer outra coisa
846
- vai para a esquerda, com destaque. */
847
- case "conversa": {
848
- (e.itens || []).forEach(function (fala, iFala) {
849
- var b = document.createElement("div");
850
- var quem = typeof fala === "string" ? (iFala % 2 ? "ela" : "eu") : (fala.de || "eu");
851
- b.className = "balao " + (quem === "eu" ? "meu" : "dela");
852
- b.style.setProperty("--atraso", (0.12 + iFala * 0.28).toFixed(2));
853
- b.textContent = typeof fala === "string" ? fala : (fala.texto || "");
854
- no.appendChild(b);
855
- });
856
- break;
857
- }
858
-
859
- /* ── passos ──────────────────────────────────────────────────────────
860
- Os cartões de etapa com UM aceso. Diferente de `cartoes`: aqui existe
861
- ordem e existe "onde estamos agora", e é isso que se quer mostrar. */
862
- case "passos": {
863
- var itens = e.itens || [];
864
- var vivo = Number(e.atual) || 0; // 1-based; 0 = nenhum
865
- no.style.gridTemplateColumns = "repeat(" + Math.max(1, itens.length) + ", 1fr)";
866
- itens.forEach(function (it, k) {
867
- var p = document.createElement("div");
868
- p.className = "passo" + (k + 1 === vivo ? " agora" : (k + 1 < vivo ? " feito" : ""));
869
- var partes = typeof it === "string" ? it.split("|") : [it.titulo, it.texto];
870
- var cab = document.createElement("b");
871
- cab.textContent = (partes[0] || "").trim();
872
- p.appendChild(cab);
873
- if (partes[1]) {
874
- var sub = document.createElement("span");
875
- sub.textContent = String(partes[1]).trim();
876
- p.appendChild(sub);
877
- }
878
- no.appendChild(p);
879
- });
880
- break;
881
- }
882
-
883
- /* ── opcoes ──────────────────────────────────────────────────────────
884
- A pergunta com alternativas e uma certa. Faz o espectador PENSAR antes
885
- de a resposta aparecer — e uma pausa de pensamento é o que segura
886
- alguém num vídeo. `certa` é o índice (1-based) da alternativa boa. */
887
- case "opcoes": {
888
- if (e.pergunta) {
889
- var q = document.createElement("b");
890
- q.className = "pergunta";
891
- q.textContent = e.pergunta;
892
- no.appendChild(q);
893
- }
894
- var certa = Number(e.certa) || 0;
895
- (e.itens || []).forEach(function (it, k) {
896
- var o = document.createElement("div");
897
- o.className = "opcao" + (k + 1 === certa ? " certa" : "");
898
- o.textContent = typeof it === "string" ? it : (it.texto || "");
899
- o.style.setProperty("--atraso", (0.1 + k * 0.18).toFixed(2));
900
- no.appendChild(o);
901
- });
902
- break;
903
- }
904
-
905
- case "numeros": {
906
- var m = (e.itens || []).length;
907
- no.style.gridTemplateColumns = "repeat(" + Math.min(m || 1, e.colunas || m) + ", 1fr)";
908
- if (e.tamanho) no.style.setProperty("--tam-num", _px(e.tamanho));
909
- (e.itens || []).forEach(function (it) {
910
- var c = document.createElement("div");
911
- c.className = "num";
912
- var t = typeof it === "string" ? it.split("|") : [it.valor, it.rotulo];
913
- var valor = (t[0] || "").toString().trim();
914
- var b = document.createElement("b");
915
- b.textContent = valor;
916
- // conta do zero até o número quando a cena entra (o player dispara)
917
- if (e.contar) {
918
- var alvo = parseFloat(valor.replace(/[^\d,.-]/g, "").replace(",", "."));
919
- if (!isNaN(alvo)) {
920
- b.setAttribute("data-contar", alvo);
921
- b.setAttribute("data-molde", valor);
922
- }
923
- }
924
- c.appendChild(b);
925
- var s = document.createElement("span");
926
- s.textContent = (t[1] || "").toString().trim();
927
- c.appendChild(s);
928
- no.appendChild(c);
929
- });
930
- break;
931
- }
932
-
933
- case "tabela": {
934
- var linhas = e.linhas || [];
935
- var h = "<table>";
936
- linhas.forEach(function (l, i) {
937
- h += "<tr>";
938
- l.forEach(function (c) { h += i === 0 ? "<th>" + _fugir(c) + "</th>" : "<td>" + _rico(c) + "</td>"; });
939
- h += "</tr>";
940
- });
941
- no.innerHTML = h + "</table>";
942
- break;
943
- }
944
-
945
- case "codigo":
946
- no.textContent = e.texto || "";
947
- if (e.tamanho) no.style.fontSize = _px(e.tamanho);
948
- break;
949
-
950
- case "citacao":
951
- no.innerHTML = '<div class="aspas">“' + _rico(e.texto) + '”</div>' +
952
- (e.autor ? '<div class="autor">' + _fugir(e.autor) + "</div>" : "");
953
- break;
954
-
955
- case "forma": {
956
- var f = e.forma || "retangulo";
957
- // Forma de vidro: o `preenchimento` vira a TINTA sob o vidro (fraca),
958
- // e o vidro em si é do CSS. Pintar o fundo inteiro aqui cobriria o
959
- // desfoque — e um vidro opaco é um retângulo.
960
- if (e.material === "vidro") {
961
- if (e.preenchimento) no.style.setProperty("--tinta", e.preenchimento);
962
- } else {
963
- no.style.background = e.preenchimento || "var(--c-tom)";
964
- }
965
- if (e.borda) no.style.border = (e.borda.largura || 2) + "px solid " + (e.borda.cor || "var(--c-linha)");
966
- if (f === "circulo") no.style.borderRadius = "999px";
967
- // Sem `raio`, o vidro fica com o 28px da folha — escrever 0px inline
968
- // aqui vencia o CSS e o painel de vidro saía de canto reto, contra o
969
- // "padrão 28" que a spec promete. Forma comum segue quadrada.
970
- else if (e.raio != null) no.style.borderRadius = _px(e.raio);
971
- else if (e.material !== "vidro") no.style.borderRadius = "0px";
972
- if (f === "triangulo") { no.style.clipPath = "polygon(50% 0,100% 100%,0 100%)"; }
973
- if (f === "linha") { no.style.height = _px(e.espessura || 3); no.style.borderRadius = "999px"; }
974
- if (f === "seta") {
975
- no.style.background = "none";
976
- no.innerHTML = '<svg viewBox="0 0 100 24" preserveAspectRatio="none" style="width:100%;height:100%">' +
977
- '<path d="M0 12h88M78 4l10 8-10 8" fill="none" stroke="' + (e.preenchimento || "var(--c-tom)") +
978
- '" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"/></svg>';
979
- }
980
- break;
981
- }
982
-
983
- case "imagem": {
984
- var img = document.createElement("img");
985
- img.src = e.src || "";
986
- img.alt = e.alt || "";
987
- img.style.objectFit = e.ajuste === "conter" ? "contain" : "cover";
988
- if (e.posicao) img.style.objectPosition = e.posicao;
989
- if (e.filtro) img.style.filter = e.filtro === "cinza" ? "grayscale(1)"
990
- : e.filtro === "sepia" ? "sepia(.7)"
991
- : e.filtro === "contraste" ? "contrast(1.35) saturate(1.15)"
992
- : e.filtro === "desbotado" ? "saturate(.55) brightness(1.08)"
993
- : e.filtro;
994
- no.style.borderRadius = _px(e.raio != null ? e.raio : 18);
995
- if (e.borda) no.style.border = (e.borda.largura || 2) + "px solid " + (e.borda.cor || "var(--c-linha)");
996
- if (e.movimento) {
997
- no.setAttribute("data-mov", e.movimento);
998
- if (e.duracaoMov) img.style.animationDuration = e.duracaoMov + "s";
999
- }
1000
- no.appendChild(img);
1001
- break;
1002
- }
1003
-
1004
- /* ── clipe: um pedaço de vídeo do próprio usuário ────────────────────
1005
- "Eu gravo o vídeo, e aí ele vai editar: cortar as pausas demoradas,
1006
- legendar, colocar efeito, transição, uma música que combine."
1007
-
1008
- Nada disso era possível — o estúdio não tinha como pôr o vídeo de
1009
- alguém dentro de uma cena. Um `clipe` é um trecho: o arquivo, de onde
1010
- começa e onde termina. Cortar as pausas vira uma lista de clipes do
1011
- MESMO arquivo, com os pedaços mudos fora.
1012
-
1013
- `de` e `ate` são segundos no arquivo original. `mudo` deixa só a
1014
- imagem (para quando a trilha manda na cena), e `volume` mistura a voz
1015
- do vídeo com o resto. */
1016
- case "clipe": {
1017
- var vid = document.createElement("video");
1018
- vid.src = e.src || "";
1019
- vid.preload = "auto";
1020
- vid.playsInline = true;
1021
- vid.muted = !!e.mudo;
1022
- if (e.volume != null) vid.volume = Math.max(0, Math.min(1, e.volume));
1023
- vid.style.width = "100%";
1024
- vid.style.height = "100%";
1025
- vid.style.objectFit = e.ajuste === "conter" ? "contain" : "cover";
1026
- if (e.posicao) vid.style.objectPosition = e.posicao;
1027
- if (e.filtro) vid.style.filter = e.filtro === "cinza" ? "grayscale(1)"
1028
- : e.filtro === "sepia" ? "sepia(.7)"
1029
- : e.filtro === "contraste" ? "contrast(1.35) saturate(1.15)"
1030
- : e.filtro === "desbotado" ? "saturate(.55) brightness(1.08)"
1031
- : e.filtro;
1032
- // O trecho fica gravado no nó: quem toca (player) e quem exporta
1033
- // (video.js) leem daqui em vez de reabrir o documento.
1034
- if (typeof e.de === "number") vid.dataset.de = e.de;
1035
- if (typeof e.ate === "number") vid.dataset.ate = e.ate;
1036
- no.style.borderRadius = _px(e.raio != null ? e.raio : 0);
1037
-
1038
- /* ── SÓ A VOZ, COM A TELA MONTADA POR CIMA ────────────────────────
1039
- *
1040
- * "Não deixa opção de manter só a voz com uma tela de exemplos."
1041
- *
1042
- * Faltava mesmo, e não era teimosia do agente: o vocabulário tinha
1043
- * `mudo` (imagem sem som) e NÃO tinha o contrário. Sem isto, usar a
1044
- * gravação como narração — a voz da pessoa por cima de telas montadas
1045
- * pelo estúdio — era impossível de escrever, por mais que se pedisse.
1046
- *
1047
- * O <video> continua no lugar, carregando e tocando: é ele que leva o
1048
- * áudio para o grafo da trilha (e daí para o MP4) e é o `de`/`ate` dele
1049
- * que dá a duração da cena. O que muda é que ele não aparece — some da
1050
- * tela e some do quadro exportado, que é o `data-som` lido no
1051
- * `video.js`. Um <video> com `display:none` também continuaria tocando,
1052
- * mas alguns navegadores param de decodificar o que não está no
1053
- * layout, e sem quadro decodificado o player o trata como não-pronto. */
1054
- if (e.apenas === "som" || e.som === true) {
1055
- vid.dataset.som = "1";
1056
- no.style.position = "absolute";
1057
- no.style.left = "0"; no.style.top = "0";
1058
- no.style.width = "1px"; no.style.height = "1px";
1059
- no.style.opacity = "0";
1060
- no.style.pointerEvents = "none";
1061
- no.style.overflow = "hidden";
1062
- }
1063
- no.appendChild(vid);
1064
- break;
1065
- }
1066
-
1067
- /* ── O CARTÃO ────────────────────────────────────────────────────────
1068
- Um produto desenhado, não um retângulo com o nome dele escrito dentro.
1069
- O que faz o olho ler "cartão" são quatro detalhes pequenos e nenhum
1070
- deles é o formato: o CHIP, o símbolo de aproximação, a numeração
1071
- mascarada e o pé com nome e validade. Sem eles o desenho é uma caixa;
1072
- com eles, qualquer pessoa reconhece em um quadro de vídeo.
1073
-
1074
- A cor sai da marca (`--c-tom`) quando ninguém diz outra, para o cartão
1075
- pertencer à peça em vez de ser mais um objeto colorido nela. */
1076
- case "cartao": {
1077
- var cc = e.cor || "var(--c-tom)";
1078
- no.style.setProperty("--cc", cc);
1079
- if (e.largura) no.style.width = _px(e.largura);
1080
- if (e.movimento) no.setAttribute("data-mov", e.movimento);
1081
- if (e.face === "verso") no.setAttribute("data-face", "verso");
1082
-
1083
- var corpo = document.createElement("div");
1084
- corpo.className = "cartao-corpo";
1085
-
1086
- var topo = document.createElement("div");
1087
- topo.className = "cartao-topo";
1088
- var marca = document.createElement("span");
1089
- marca.className = "cartao-marca";
1090
- marca.textContent = e.nome || "";
1091
- var vari = document.createElement("span");
1092
- vari.className = "cartao-variante";
1093
- vari.textContent = e.variante || "";
1094
- topo.appendChild(marca); topo.appendChild(vari);
1095
-
1096
- var meio = document.createElement("div");
1097
- meio.className = "cartao-meio";
1098
- var chip = document.createElement("i");
1099
- chip.className = "cartao-chip";
1100
- // O símbolo de aproximação: três arcos. Em SVG porque em texto ele
1101
- // depende de uma fonte que a máquina pode não ter.
1102
- var onda = document.createElement("span");
1103
- onda.className = "cartao-onda";
1104
- onda.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true">'
1105
- + '<path d="M8 5a10 10 0 0 1 0 14M13 8a5.6 5.6 0 0 1 0 8M17.6 2.4a15 15 0 0 1 0 19.2"'
1106
- + ' fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
1107
- meio.appendChild(chip); meio.appendChild(onda);
1108
-
1109
- var num = document.createElement("div");
1110
- num.className = "cartao-numero";
1111
- // Os quatro últimos dígitos e mais nada: é o que aparece num cartão de
1112
- // verdade em foto de campanha, e é o que não vira dado de ninguém.
1113
- num.textContent = "•••• •••• •••• " + String(e.numero || "4821").slice(-4);
1114
-
1115
- var pe = document.createElement("div");
1116
- pe.className = "cartao-pe";
1117
- var quem = document.createElement("span");
1118
- quem.textContent = e.titular || "SEU NOME AQUI";
1119
- var val = document.createElement("span");
1120
- val.textContent = e.validade || "12/29";
1121
- pe.appendChild(quem); pe.appendChild(val);
1122
-
1123
- var brilho = document.createElement("i");
1124
- brilho.className = "cartao-brilho";
1125
-
1126
- corpo.appendChild(brilho);
1127
- corpo.appendChild(topo);
1128
- corpo.appendChild(meio);
1129
- corpo.appendChild(num);
1130
- corpo.appendChild(pe);
1131
- no.appendChild(corpo);
1132
- break;
1133
- }
1134
-
1135
- /* ── A TELA DO APP ───────────────────────────────────────────────────
1136
- O `dispositivo` já desenhava o aparelho; o que ele mostrava DENTRO era
1137
- texto centralizado no meio do vidro, que não se parece com aplicativo
1138
- nenhum. O que faz uma tela parecer um app é a ARRUMAÇÃO: cabeçalho em
1139
- cima, um número grande, linhas com o nome à esquerda e o valor à
1140
- direita separadas por fio, e um botão cheio embaixo.
1141
-
1142
- Vale sozinho na cena e vale dentro do `dispositivo` — é o mesmo
1143
- elemento, e por isso a tela do celular e a do print da campanha não
1144
- divergem. */
1145
- case "app": {
1146
- if (e.escuro) no.setAttribute("data-escuro", "1");
1147
- if (e.topo) {
1148
- var cab = document.createElement("div");
1149
- cab.className = "app-topo";
1150
- cab.innerHTML = "<span>" + _fugir(e.topo) + "</span><i></i>";
1151
- no.appendChild(cab);
1152
- }
1153
- if (e.valor) {
1154
- var caixaV = document.createElement("div");
1155
- caixaV.className = "app-valor";
1156
- if (e.rotulo) {
1157
- var rot = document.createElement("small");
1158
- rot.textContent = e.rotulo;
1159
- caixaV.appendChild(rot);
1160
- }
1161
- var vv = document.createElement("strong");
1162
- vv.textContent = e.valor;
1163
- caixaV.appendChild(vv);
1164
- no.appendChild(caixaV);
1165
- }
1166
- var linhas = e.linhas || e.itens || [];
1167
- if (linhas.length) {
1168
- var lista = document.createElement("div");
1169
- lista.className = "app-linhas";
1170
- linhas.slice(0, 8).forEach(function (li) {
1171
- /* Aceita ["nome", "valor"] e "nome|valor" — o segundo é a forma que
1172
- todo o resto da spec usa (lista, cartoes, passos), e quem escreve
1173
- o documento vai chutar a que já conhece. */
1174
- var nome = li, valor = "";
1175
- if (Array.isArray(li)) { nome = li[0]; valor = li[1] || ""; }
1176
- else if (typeof li === "string" && li.indexOf("|") > -1) {
1177
- var par = li.split("|"); nome = par[0]; valor = par.slice(1).join("|");
1178
- }
1179
- var linha = document.createElement("div");
1180
- linha.className = "app-linha";
1181
- linha.innerHTML = "<span>" + _fugir(String(nome).trim()) + "</span>"
1182
- + "<b>" + _fugir(String(valor).trim()) + "</b>";
1183
- lista.appendChild(linha);
1184
- });
1185
- no.appendChild(lista);
1186
- }
1187
- if (e.botao) {
1188
- var bt = document.createElement("div");
1189
- bt.className = "app-botao";
1190
- bt.textContent = e.botao;
1191
- no.appendChild(bt);
1192
- }
1193
- break;
1194
- }
1195
-
1196
- /* ── A VITRINE ───────────────────────────────────────────────────────
1197
- A linha de produtos, com um escolhido. O `cartoes` genérico resolvia
1198
- isso com três caixinhas de vidro do tamanho de um selo; aqui os itens
1199
- são CARTÕES, o atual vem à frente e os outros recuam — que é o desenho
1200
- de toda tela de "escolha o seu". */
1201
- case "vitrine": {
1202
- var itens = (e.itens || []).slice(0, 5);
1203
- var atual = e.atual != null ? Number(e.atual) : 0;
1204
- if (e.titulo) {
1205
- var tt = document.createElement("div");
1206
- tt.className = "vitrine-titulo";
1207
- tt.textContent = e.titulo;
1208
- no.appendChild(tt);
1209
- }
1210
- var fila = document.createElement("div");
1211
- fila.className = "vitrine-fila";
1212
- itens.forEach(function (it, i) {
1213
- var dado = typeof it === "string"
1214
- ? { nome: it.split("|")[0], detalhe: it.split("|").slice(1).join("|") }
1215
- : (it || {});
1216
- var mini = renderElemento({
1217
- tipo: "cartao", nome: e.nome || dado.marca || "",
1218
- variante: dado.nome || dado.variante || "",
1219
- cor: dado.cor || e.cor, numero: dado.numero,
1220
- titular: " ", validade: " ",
1221
- }, ctx);
1222
- var col = document.createElement("div");
1223
- col.className = "vitrine-item" + (i === atual ? " atual" : "");
1224
- col.appendChild(mini);
1225
- if (dado.detalhe) {
1226
- var d = document.createElement("small");
1227
- d.textContent = dado.detalhe;
1228
- col.appendChild(d);
1229
- }
1230
- fila.appendChild(col);
1231
- });
1232
- no.appendChild(fila);
1233
- break;
1234
- }
1235
-
1236
- /* ── A ASSINATURA ────────────────────────────────────────────────────
1237
- O fim do anúncio. Toda peça de marca fecha assim — o logo sozinho,
1238
- grande, sobre a cor da casa, e nada mais na tela. Sem este elemento a
1239
- última cena do vídeo era mais uma frase, e a peça acabava sem assinar
1240
- quem falou. */
1241
- case "assinatura": {
1242
- no.setAttribute("data-sobre", /^(marca|claro|escuro)$/.test(e.sobre || "") ? e.sobre : "marca");
1243
- if (e.logo) {
1244
- var img2 = document.createElement("img");
1245
- img2.className = "assinatura-logo";
1246
- img2.src = e.logo; img2.alt = e.nome || "";
1247
- no.appendChild(img2);
1248
- } else if (e.nome) {
1249
- var nm = document.createElement("div");
1250
- nm.className = "assinatura-nome";
1251
- nm.textContent = e.nome;
1252
- no.appendChild(nm);
1253
- }
1254
- if (e.frase) {
1255
- var fr = document.createElement("div");
1256
- fr.className = "assinatura-frase";
1257
- fr.textContent = e.frase;
1258
- no.appendChild(fr);
1259
- }
1260
- break;
1261
- }
1262
-
1263
- case "icone": {
1264
- var t = e.tamanho || 90;
1265
- no.style.width = _px(e.l || t);
1266
- no.style.height = _px(e.a || t);
1267
- no.style.color = e.cor || "var(--c-tom)";
1268
- /* ÍCONE COMO OBJETO: `estilo: "app"` é o azulejo de aplicativo —
1269
- gradiente da cor, brilho em cima, sombra embaixo, o traço branco —
1270
- e `estilo: "vidro"` é o mesmo azulejo em liquid glass. Pedido do
1271
- dono ("icon, liquid glass, bem motion"): um ícone de traço sozinho
1272
- é sinal; num azulejo vira coisa. `fundo` pinta o azulejo; sem ele, a
1273
- cor do tema. */
1274
- if (e.estilo === "app" || e.estilo === "vidro") {
1275
- no.dataset.estilo = e.estilo;
1276
- no.style.setProperty("--azulejo", e.fundo || e.cor || "var(--c-tom)");
1277
- if (e.estilo === "app") no.style.color = e.corTraco || "#fff";
1278
- }
1279
- // `caminho`: o ícone desenhado pelo modelo (path SVG 24×24) — ver
1280
- // icones.js. Sem ele, um da lista pelo nome.
1281
- no.innerHTML = e.caminho
1282
- ? svgCaminho(e.caminho, { preenchido: !!e.preenchido, traco: e.traco })
1283
- : svgIcone(e.nome || "estrela");
1284
- var s = no.querySelector("svg");
1285
- if (s) s.setAttribute("stroke-width", e.traco || 1.5);
1286
- break;
1287
- }
1288
-
1289
- case "grafico": {
1290
- var gl = e.l || 900, ga = e.a || 480;
1291
- if (!absoluto) { no.style.width = _px(gl); no.style.height = _px(ga); }
1292
- no.innerHTML = svgGrafico(e, gl, ga);
1293
- break;
1294
- }
1295
-
1296
- case "objeto3d": {
1297
- montar3d(no, e);
1298
- break;
1299
- }
1300
-
1301
- case "no": {
1302
- if (e.destaque) no.classList.add("destaque");
1303
- if (e.formato === "circulo") no.classList.add("circulo");
1304
- if (e.cor) no.style.background = e.cor;
1305
- if (e.corTexto) no.style.color = e.corTexto;
1306
- if (e.tamanho) no.style.fontSize = _px(e.tamanho);
1307
- no.innerHTML = _rico(e.texto);
1308
- break;
1309
- }
1310
-
1311
- default:
1312
- no.innerHTML = _rico(e.texto || "");
1313
- }
1314
-
1315
- return no;
1316
- }
1317
-
1318
- /* ------------------------------------------------------- posição automática */
1319
-
1320
- /* Dá x/y aos nós a partir dos conectores. O autor manda só o grafo. */
1321
- function posicionarAuto(elementos, W, H, modo) {
1322
- var nos = elementos.filter(function (e) { return e.id && e.tipo !== "conector"; });
1323
- if (!nos.length) return;
1324
-
1325
- var links = elementos.filter(function (e) { return e.tipo === "conector" && e.de && e.para; });
1326
- var porId = {};
1327
- nos.forEach(function (n) { porId[n.id] = n; });
1328
-
1329
- var entra = {}, sai = {};
1330
- nos.forEach(function (n) { entra[n.id] = []; sai[n.id] = []; });
1331
- links.forEach(function (c) {
1332
- if (!porId[c.de] || !porId[c.para]) return;
1333
- sai[c.de].push(c.para);
1334
- entra[c.para].push(c.de);
1335
- });
1336
-
1337
- /* nível = maior distância desde uma raiz, para a seta nunca voltar */
1338
- var nivel = {}, visto = {};
1339
- var fila = nos.filter(function (n) { return !entra[n.id].length; }).map(function (n) { return n.id; });
1340
- if (!fila.length) fila = [nos[0].id];
1341
- fila.forEach(function (id) { nivel[id] = 0; });
1342
-
1343
- var guarda = 0;
1344
- while (fila.length && guarda++ < 4000) {
1345
- var id = fila.shift();
1346
- (sai[id] || []).forEach(function (destino) {
1347
- var novo = nivel[id] + 1;
1348
- if (nivel[destino] == null || novo > nivel[destino]) {
1349
- nivel[destino] = novo;
1350
- fila.push(destino);
1351
- }
1352
- });
1353
- }
1354
- nos.forEach(function (n) { if (nivel[n.id] == null) nivel[n.id] = 0; });
1355
-
1356
- var colunas = {};
1357
- nos.forEach(function (n) {
1358
- (colunas[nivel[n.id]] = colunas[nivel[n.id]] || []).push(n);
1359
- });
1360
- var chaves = Object.keys(colunas).map(Number).sort(function (a, b) { return a - b; });
1361
-
1362
- var vertical = modo === "vertical" || modo === "arvore";
1363
- var margem = Math.round(W * 0.07);
1364
-
1365
- // tamanho padrão coerente com o espaço disponível
1366
- var maxPorColuna = Math.max.apply(null, chaves.map(function (k) { return colunas[k].length; }));
1367
- var lPadrao = vertical
1368
- ? Math.min(340, Math.floor((W - margem * 2 - (maxPorColuna - 1) * 60) / maxPorColuna))
1369
- : Math.min(340, Math.floor((W - margem * 2 - (chaves.length - 1) * 90) / Math.max(1, chaves.length)));
1370
- var aPadrao = vertical
1371
- ? Math.min(150, Math.floor((H - margem * 2 - (chaves.length - 1) * 70) / Math.max(1, chaves.length)))
1372
- : 150;
1373
- lPadrao = Math.max(150, lPadrao);
1374
- aPadrao = Math.max(90, aPadrao);
1375
-
1376
- var passoEixo = vertical
1377
- ? (H - margem * 2 - aPadrao) / Math.max(1, chaves.length - 1)
1378
- : (W - margem * 2 - lPadrao) / Math.max(1, chaves.length - 1);
1379
-
1380
- chaves.forEach(function (k, iCol) {
1381
- var grupo = colunas[k];
1382
- var vao = vertical ? W : H;
1383
- var tam = vertical ? lPadrao : aPadrao;
1384
- var espaco = (vao - margem * 2 - tam) / Math.max(1, grupo.length - 1);
1385
-
1386
- grupo.forEach(function (n, i) {
1387
- if (n.l == null) n.l = lPadrao;
1388
- if (n.a == null) n.a = aPadrao;
1389
- var eixo = margem + (chaves.length > 1 ? passoEixo * iCol : (vao === W ? 0 : 0));
1390
- var cruz = grupo.length > 1 ? margem + espaco * i
1391
- : Math.round((vao - (vertical ? n.l : n.a)) / 2);
1392
- if (vertical) {
1393
- if (n.x == null) n.x = Math.round(cruz);
1394
- if (n.y == null) n.y = Math.round(margem + (chaves.length > 1 ? passoEixo * iCol : (H - n.a) / 2));
1395
- } else {
1396
- if (n.x == null) n.x = Math.round(margem + (chaves.length > 1 ? passoEixo * iCol : (W - n.l) / 2));
1397
- if (n.y == null) n.y = Math.round(cruz);
1398
- }
1399
- });
1400
- });
1401
- }
1402
-
1403
- /* -------------------------------------------------------------- conectores */
1404
-
1405
- function desenharConectores(cena, elementos, W, H) {
1406
- var links = elementos.filter(function (e) { return e.tipo === "conector"; });
1407
- if (!links.length) return;
1408
-
1409
- var caixas = {};
1410
- elementos.forEach(function (e) {
1411
- if (e.id) caixas[e.id] = { x: e.x || 0, y: e.y || 0, l: e.l || 200, a: e.a || 100 };
1412
- });
1413
-
1414
- var ns = "http://www.w3.org/2000/svg";
1415
- var svg = document.createElementNS(ns, "svg");
1416
- svg.setAttribute("class", "conectores");
1417
- svg.setAttribute("viewBox", "0 0 " + W + " " + H);
1418
-
1419
- var def = document.createElementNS(ns, "defs");
1420
- def.innerHTML = '<marker id="pta" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">' +
1421
- '<path d="M0 0 L10 5 L0 10 z" fill="var(--c-fraco)"/></marker>';
1422
- svg.appendChild(def);
1423
-
1424
- links.forEach(function (c) {
1425
- var a = caixas[c.de], b = caixas[c.para];
1426
- if (!a || !b) return;
1427
- var ac = { x: a.x + a.l / 2, y: a.y + a.a / 2 }, bc = { x: b.x + b.l / 2, y: b.y + b.a / 2 };
1428
- var horizontal = Math.abs(bc.x - ac.x) >= Math.abs(bc.y - ac.y);
1429
- var p1 = horizontal
1430
- ? { x: bc.x > ac.x ? a.x + a.l : a.x, y: ac.y }
1431
- : { x: ac.x, y: bc.y > ac.y ? a.y + a.a : a.y };
1432
- var p2 = horizontal
1433
- ? { x: bc.x > ac.x ? b.x : b.x + b.l, y: bc.y }
1434
- : { x: bc.x, y: bc.y > ac.y ? b.y : b.y + b.a };
1435
-
1436
- var d;
1437
- if (c.estilo === "reta") {
1438
- d = "M" + p1.x + " " + p1.y + " L" + p2.x + " " + p2.y;
1439
- } else if (c.estilo === "cotovelo") {
1440
- d = horizontal
1441
- ? "M" + p1.x + " " + p1.y + " H" + (p1.x + p2.x) / 2 + " V" + p2.y + " H" + p2.x
1442
- : "M" + p1.x + " " + p1.y + " V" + (p1.y + p2.y) / 2 + " H" + p2.x + " V" + p2.y;
1443
- } else {
1444
- d = horizontal
1445
- ? "M" + p1.x + " " + p1.y + " C" + (p1.x + p2.x) / 2 + " " + p1.y + " " + (p1.x + p2.x) / 2 + " " + p2.y + " " + p2.x + " " + p2.y
1446
- : "M" + p1.x + " " + p1.y + " C" + p1.x + " " + (p1.y + p2.y) / 2 + " " + p2.x + " " + (p1.y + p2.y) / 2 + " " + p2.x + " " + p2.y;
1447
- }
1448
-
1449
- var path = document.createElementNS(ns, "path");
1450
- path.setAttribute("d", d);
1451
- path.setAttribute("fill", "none");
1452
- path.setAttribute("stroke", c.cor || "var(--c-fraco)");
1453
- path.setAttribute("stroke-width", c.espessura || 3);
1454
- if (c.tracejado) path.setAttribute("stroke-dasharray", "10 8");
1455
- if (c.seta !== false) path.setAttribute("marker-end", "url(#pta)");
1456
- svg.appendChild(path);
1457
-
1458
- if (c.texto) {
1459
- var tx = document.createElementNS(ns, "text");
1460
- tx.setAttribute("x", (p1.x + p2.x) / 2);
1461
- tx.setAttribute("y", (p1.y + p2.y) / 2 - 12);
1462
- tx.setAttribute("text-anchor", "middle");
1463
- tx.setAttribute("fill", "var(--c-fraco)");
1464
- tx.setAttribute("font-size", "22");
1465
- tx.textContent = c.texto;
1466
- svg.appendChild(tx);
1467
- }
1468
- });
1469
-
1470
- cena.appendChild(svg);
1471
- }
1472
-
1473
- /* ------------------------------------------------------------------ cena */
1474
-
1475
- function renderCenaBase(cena, doc, op) {
1476
- op = op || {};
1477
- cena = cena || {};
1478
- var W = doc.largura || 1920, H = doc.altura || 1080;
1479
-
1480
- var no = document.createElement("div");
1481
- no.className = "cena" + (op.animar ? " animar" : "");
1482
- no.style.width = W + "px";
1483
- no.style.height = H + "px";
1484
- no.setAttribute("data-tema", cena.tema || doc.tema || "meia-noite");
1485
- if (cena.acabamento || doc.acabamento) no.setAttribute("data-acabamento", cena.acabamento || doc.acabamento);
1486
- if (op.animar && (cena.transicao || doc.transicao)) {
1487
- no.setAttribute("data-transicao", cena.transicao || doc.transicao);
1488
- }
1489
- if (cena.tom) no.style.setProperty("--c-tom", cena.tom);
1490
- if (cena.tom2) no.style.setProperty("--c-tom2", cena.tom2);
1491
- aplicarFundo(no, cena.fundo);
1492
- aplicarMarca(no, cena, doc, H);
1493
-
1494
- var layout = cena.layout || (op.layoutPadrao || "titulo");
1495
- var elementos = (cena.elementos || []).slice();
1496
- var margem = Math.round(W * 0.068);
1497
-
1498
- /* A gravação do usuário é o CHÃO da cena, não um bloco no meio do texto.
1499
- Entrando pelo fluxo, ela virava uma caixinha de 190px espremida embaixo
1500
- do título — e no vídeo exportado sobrava uma tarja de imagem cercada de
1501
- fundo. Quem grava o próprio rosto falando quer a imagem ocupando o
1502
- quadro, com a legenda POR CIMA.
1503
-
1504
- Um clipe com `x`/`y` continua sendo elemento posicionado: aí a pessoa
1505
- pediu a caixinha de propósito (canto da tela, quadro dentro do quadro). */
1506
- var deChao = [];
1507
- var falas = [];
1508
- elementos = elementos.filter(function (e) {
1509
- if (!e || e.x != null || e.y != null) return true;
1510
- if (e.tipo === "clipe") { deChao.push(e); return false; }
1511
- /* A ASSINATURA É A CENA INTEIRA, e por isso entra pelo chão e não pelo
1512
- fluxo. Pelo fluxo ela vira uma caixinha colorida no meio da tela, com a
1513
- margem do layout em volta — que é o oposto de assinar: o logo tem de
1514
- encostar nas bordas do quadro, como em qualquer fim de comercial. */
1515
- if (e.tipo === "assinatura") { deChao.push(e); return false; }
1516
- // Legenda de fala mora no terço de baixo, como toda legenda de vídeo.
1517
- // Pelo fluxo ela iria para o meio da tela, em cima do rosto de quem fala.
1518
- if (e.tipo === "texto" && e.papel === "fala") { falas.push(e); return false; }
1519
- return true;
1520
- });
1521
-
1522
- var fluxo = document.createElement("div");
1523
- fluxo.className = "fluxo";
1524
- fluxo.setAttribute("data-layout", layout);
1525
- fluxo.style.padding = margem + "px " + Math.round(W * 0.075) + "px";
1526
- no.appendChild(fluxo);
1527
-
1528
- // tela vertical pede tipografia maior: os padrões são calibrados em 1920x1080
1529
- var escalaTipo = H > W ? 1.35 : 1;
1530
- no.style.setProperty("--esc", escalaTipo);
1531
- // A altura do quadro, para o CSS dimensionar o que precisa ocupar a cena
1532
- // (o aparelho, hoje) em proporção e não em pixels calibrados num formato só.
1533
- no.style.setProperty("--alto", H + "px");
1534
- /* A UNIDADE DA CENA: 1% da largura do quadro.
1535
-
1536
- Sem ela, tudo o que o CSS escreve em `em` cai na fonte herdada — que numa
1537
- cena é a do navegador, ~14px — e o desenho colapsa sem erro nenhum. Foi
1538
- medido num quadro de 1080x1920: a linha do extrato saiu com 7,84px e o
1539
- nome da assinatura com 30,8px, num quadro de 1920 de altura. Na tela
1540
- aquilo não é "pequeno": é invisível.
1541
-
1542
- O resto do sistema resolve isso mandando PIXEL do documento (`tamanho`).
1543
- Peça que não tem `tamanho` — a tela do app, a vitrine, a assinatura — se
1544
- mede por aqui, e assim acompanha o formato em vez de ser calibrada num só. */
1545
- no.style.setProperty("--u", (W / 100) + "px");
1546
- var ctx = {
1547
- i: 0, escalaTipo: escalaTipo,
1548
- animPadrao: op.animar ? (cena.anim || "subir") : null,
1549
- // as chaves precisam saber quanto dura a cena para virar segundos
1550
- animar: !!op.animar,
1551
- duracao: cena.duracao || 4,
1552
- raiz: no,
1553
- // objeto do elemento -> posição dele em cena.elementos, tirada ANTES de
1554
- // qualquer separação por layout
1555
- ordem: (function () {
1556
- var m = new Map();
1557
- (cena.elementos || []).forEach(function (e, i) { m.set(e, i); });
1558
- return m;
1559
- })(),
1560
- };
1561
-
1562
- deChao.forEach(function (e) {
1563
- ctx.i++;
1564
- var el = renderElemento(e, ctx);
1565
- el.style.position = "absolute";
1566
- /* `inset: 0` sozinho não estica: renderElemento já deixou uma `height`
1567
- inline, e com altura declarada o `bottom` é ignorado (a regra do
1568
- over-constrained do CSS). A camada saía com 60px de altura — uma tarja
1569
- de imagem no topo, com fundo do tema no resto. Largura e altura ditas
1570
- na cara não têm essa conversa. */
1571
- el.style.left = "0"; el.style.top = "0"; el.style.right = ""; el.style.bottom = "";
1572
- el.style.width = "100%"; el.style.height = "100%";
1573
- el.style.zIndex = "0";
1574
- // Sem animação de entrada por padrão: deslizar o quadro inteiro da
1575
- // gravação não é efeito, é defeito. Quem pedir `anim` continua tendo.
1576
- if (!e.anim) { el.removeAttribute("data-an"); el.style.animation = "none"; }
1577
- no.insertBefore(el, fluxo); // atrás do texto, na frente do fundo
1578
- });
1579
- if (deChao.length) {
1580
- fluxo.style.position = "relative";
1581
- fluxo.style.zIndex = "1";
1582
- }
1583
-
1584
- if (falas.length) {
1585
- var faixa = document.createElement("div");
1586
- faixa.className = "faixa-fala";
1587
- faixa.style.position = "absolute";
1588
- faixa.style.left = "0"; faixa.style.right = "0";
1589
- faixa.style.bottom = Math.round(H * 0.085) + "px";
1590
- faixa.style.zIndex = "2"; // acima do texto e da gravação
1591
- faixa.style.padding = "0 " + Math.round(W * 0.08) + "px";
1592
- falas.forEach(function (e) {
1593
- ctx.i++;
1594
- faixa.appendChild(renderElemento(e, ctx));
1595
- });
1596
- no.appendChild(faixa);
1597
- }
1598
-
1599
- if (layout === "livre") {
1600
- fluxo.style.padding = "0";
1601
- if (cena.auto) posicionarAuto(elementos, W, H, cena.auto);
1602
- elementos.forEach(function (e) {
1603
- if (e.tipo === "conector") return;
1604
- ctx.i++;
1605
- var el = renderElemento(e, ctx);
1606
- el.style.position = "absolute";
1607
- if (e.x == null) el.style.left = "0px";
1608
- if (e.y == null) el.style.top = "0px";
1609
- fluxo.appendChild(el);
1610
- });
1611
- desenharConectores(no, elementos, W, H);
1612
- return no;
1613
- }
1614
-
1615
- /* layouts de fluxo — cabeçalho em cima, corpo distribuído */
1616
- var cabecalho = [], corpo = [], flutuantes = [], imagem = null;
1617
- elementos.forEach(function (e) {
1618
- if (e.tipo === "conector") return;
1619
- if (e.x != null || e.y != null) { flutuantes.push(e); return; }
1620
- /* O aparelho também vai para o lado, não só a imagem. É o enquadramento
1621
- do vídeo de referência: texto de um lado, celular com a conversa do
1622
- outro. Sem isto o celular caía na coluna de texto e o outro lado ficava
1623
- vazio — metade do quadro em branco. */
1624
- if ((layout === "imagem-direita" || layout === "imagem-esquerda")
1625
- && (e.tipo === "imagem" || e.tipo === "dispositivo") && !imagem) { imagem = e; return; }
1626
- /* Selo, progresso e foto são material de CABEÇALHO: vêm antes do título,
1627
- como sobrancelha da cena. Caindo no corpo, apareciam DEPOIS do título —
1628
- a pílula "Módulo 2 de 5" embaixo do nome do módulo, e o rosto da pessoa
1629
- embaixo do nome dela. Dentro do grupo a ordem é a do documento, então
1630
- quem escreveu decide o resto. */
1631
- if (e.papel === "chapeu" || e.papel === "titulo"
1632
- || e.tipo === "selo" || e.tipo === "progresso" || e.tipo === "avatar") {
1633
- cabecalho.push(e);
1634
- } else corpo.push(e);
1635
- });
1636
-
1637
- function solta(lista, alvo) {
1638
- lista.forEach(function (e) { ctx.i++; alvo.appendChild(renderElemento(e, ctx)); });
1639
- }
1640
-
1641
- if (layout === "imagem-direita" || layout === "imagem-esquerda") {
1642
- fluxo.style.padding = "0";
1643
- var lado = document.createElement("div");
1644
- lado.className = "lado-texto";
1645
- lado.style.padding = margem + "px " + Math.round(W * 0.055) + "px";
1646
- solta(cabecalho, lado); solta(corpo, lado);
1647
- var box = document.createElement("div");
1648
- box.className = "lado-imagem";
1649
- if (imagem && imagem.tipo === "dispositivo") {
1650
- // O aparelho tem moldura e sombra próprias: encostar nas bordas do
1651
- // quadro comeria as duas. Ele entra com respiro.
1652
- box.classList.add("com-aparelho");
1653
- box.style.padding = margem + "px " + Math.round(W * 0.04) + "px";
1654
- ctx.i++;
1655
- box.appendChild(renderElemento(imagem, ctx));
1656
- } else if (imagem) {
1657
- var im = renderElemento({ tipo: "imagem", src: imagem.src, ajuste: imagem.ajuste, raio: 0 }, ctx);
1658
- im.style.position = "absolute";
1659
- im.style.inset = "0";
1660
- box.appendChild(im);
1661
- }
1662
- if (layout === "imagem-esquerda") { fluxo.appendChild(box); fluxo.appendChild(lado); }
1663
- else { fluxo.appendChild(lado); fluxo.appendChild(box); }
1664
- } else if ((layout === "duas-colunas" || layout === "tres-colunas") && corpo.length > 1) {
1665
- solta(cabecalho, fluxo);
1666
- var cols = document.createElement("div");
1667
- cols.className = "colunas";
1668
- cols.style.gridTemplateColumns = "repeat(" + (layout === "duas-colunas" ? 2 : 3) + ", 1fr)";
1669
- solta(corpo, cols);
1670
- fluxo.appendChild(cols);
1671
- comporCabecaCorpo(fluxo, layout, cabecalho.length, 1);
1672
- } else {
1673
- // um bloco só (cartoes, numeros…) já se distribui sozinho: não engaiolar
1674
- solta(cabecalho, fluxo);
1675
- solta(corpo, fluxo);
1676
- comporCabecaCorpo(fluxo, layout, cabecalho.length, corpo.length);
1677
- }
1678
-
1679
- flutuantes.forEach(function (e) {
1680
- ctx.i++;
1681
- var el = renderElemento(e, ctx);
1682
- el.style.position = "absolute";
1683
- no.appendChild(el);
1684
- });
1685
-
1686
- /* O RODAPÉ DA APRESENTAÇÃO: nome do deck à esquerda, "03 / 08" à direita.
1687
- É o que ancora a composição no quadro — sem ele, uma cena de título e
1688
- lista é um bloco flutuando no meio do preto, e todo deck parece o mesmo
1689
- deck. Quem manda é quem chama (o player, quando a peça é prisma): o
1690
- vídeo não tem rodapé, e a capa e as seções também não — são as cenas
1691
- que se sustentam sozinhas. */
1692
- if (op.rodape && layout !== "capa" && layout !== "secao") {
1693
- var rodape = document.createElement("div");
1694
- rodape.className = "rodape";
1695
- rodape.style.left = Math.round(W * 0.075) + "px";
1696
- rodape.style.right = Math.round(W * 0.075) + "px";
1697
- rodape.style.bottom = Math.round(H * 0.048) + "px";
1698
- var esq = document.createElement("span");
1699
- esq.textContent = op.rodape.titulo || "";
1700
- var dir = document.createElement("span");
1701
- if (op.rodape.indice && op.rodape.total) {
1702
- dir.textContent = String(op.rodape.indice).padStart(2, "0") + " / " + String(op.rodape.total).padStart(2, "0");
1703
- }
1704
- rodape.appendChild(esq);
1705
- rodape.appendChild(dir);
1706
- no.appendChild(rodape);
1707
- }
1708
-
1709
- desenharConectores(no, elementos, W, H);
1710
- return no;
1711
- }
1712
-
1713
- /* A COMPOSIÇÃO DE SLIDE: cabeça em cima, corpo no meio do que sobra.
1714
-
1715
- Tudo centrado verticalmente — que era o padrão para todo layout — deixa um
1716
- título com três linhas de lista ocupando 30% do quadro, com 350px de nada
1717
- em cima e 350px embaixo: a cara de "slide genérico" que o dono apontou. A
1718
- composição de quem faz deck de verdade ancora o título no alto e dá o resto
1719
- do quadro ao conteúdo, que se centra ali.
1720
-
1721
- Só quando há as duas metades (cabeça E corpo) e o layout não é o dos que se
1722
- sustentam centrados (capa, centro, citação, seção): uma cena de vídeo com um
1723
- título sozinho continua no meio, onde um título sozinho fica bem. O corpo é
1724
- marcado no primeiro e no último filho (`margin: auto` no CSS), e o DOM
1725
- continua chapado — nada de embrulho novo em volta dos elementos. */
1726
- function comporCabecaCorpo(fluxo, layout, nCabeca, nCorpo) {
1727
- if (!nCabeca || !nCorpo) return;
1728
- if (layout === "capa" || layout === "centro" || layout === "citacao" || layout === "secao") return;
1729
- var filhos = fluxo.children;
1730
- if (filhos.length <= nCabeca) return;
1731
- fluxo.setAttribute("data-composicao", "cabeca-corpo");
1732
- filhos[nCabeca].classList.add("corpo-inicio");
1733
- filhos[filhos.length - 1].classList.add("corpo-fim");
1734
- }
1735
-
1736
-
1737
- /* ══ A CÂMERA DA CENA ══════════════════════════════════════════════════════
1738
- *
1739
- * `chaves` move UM elemento. O que faltava era mover o QUADRO: a cena inteira
1740
- * se aproximando enquanto o texto entra, a panorâmica que atravessa a arte, o
1741
- * leve giro que tira a cara de página parada. É a câmera do After Effects, e
1742
- * sem ela todo movimento nasce preso ao elemento — o vídeo parece um slide com
1743
- * animação, que é exatamente a reclamação.
1744
- *
1745
- * COMO FUNCIONA: o conteúdo da cena vai para dentro de um `.palco`, e a câmera
1746
- * anima o palco com a mesma máquina de chaves dos elementos (mesma curva,
1747
- * mesma interpolação, mesma conta de duração). O FUNDO fica onde está, na raiz
1748
- * da cena: é o que dá profundidade de graça — o quadro anda sobre o fundo em
1749
- * vez de arrastar tudo junto, que é como um zoom de câmera se comporta.
1750
- *
1751
- * PARALAXE: um elemento com `paralaxe` anda MAIS (>1, perto da câmera) ou
1752
- * MENOS (<1, ao longe) que o palco. Como ele já pode ter as próprias chaves, a
1753
- * diferença vai num embrulho em volta dele — duas animações no mesmo nó
1754
- * disputariam a mesma propriedade `transform`, e a segunda ganharia calada.
1755
- */
1756
-
1757
- // Movimentos com NOME, porque quem escreve o documento não quer pensar em
1758
- // chave: são os quatro que aparecem em toda peça de motion.
1759
- var CAMERAS = {
1760
- aproximar: [{ t: 0, escala: 1 }, { t: 1, escala: 1.12 }],
1761
- afastar: [{ t: 0, escala: 1.12 }, { t: 1, escala: 1 }],
1762
- "pan-direita": [{ t: 0, x: -70 }, { t: 1, x: 70 }],
1763
- "pan-esquerda": [{ t: 0, x: 70 }, { t: 1, x: -70 }],
1764
- subir: [{ t: 0, y: 60 }, { t: 1, y: -60 }],
1765
- descer: [{ t: 0, y: -60 }, { t: 1, y: 60 }],
1766
- // O "empurrão" é o zoom com um grau de giro: é o que dá mão humana ao quadro.
1767
- empurrar: [{ t: 0, escala: 1.02, giro: -0.6 }, { t: 1, escala: 1.14, giro: 0.3 }],
1768
- balanco: [{ t: 0, x: -18, giro: -0.5 }, { t: 0.5, x: 12, giro: 0.4 }, { t: 1, x: -6, giro: -0.2 }],
1769
- };
1770
-
1771
- function chavesDaCamera(camera) {
1772
- if (!camera) return null;
1773
- if (typeof camera === "string") return CAMERAS[camera] ? CAMERAS[camera].slice() : null;
1774
- if (Array.isArray(camera)) return camera.slice();
1775
- if (camera.chaves && camera.chaves.length) return camera.chaves.slice();
1776
- if (camera.movimento && CAMERAS[camera.movimento]) return CAMERAS[camera.movimento].slice();
1777
- return null;
1778
- }
1779
-
1780
- /* A chave do elemento de paralaxe: a da câmera, menos a que o palco já deu.
1781
- Fator 0 devolve nada (anda com o palco, que é o padrão). */
1782
- function chavesDeParalaxe(chaves, fator) {
1783
- var f = fator - 1;
1784
- if (!f) return null;
1785
- return chaves.map(function (k) {
1786
- var nova = { t: _numero(k.t, 0) };
1787
- if (typeof k.x === "number") nova.x = k.x * f;
1788
- if (typeof k.y === "number") nova.y = k.y * f;
1789
- if (typeof k.escala === "number") nova.escala = 1 + (k.escala - 1) * f;
1790
- return nova;
1791
- });
1792
- }
1793
-
1794
- function aplicarCamera(no, cena, op) {
1795
- var chaves = chavesDaCamera(cena.camera);
1796
- if (!chaves || !chaves.length || !op.animar) return no;
1797
-
1798
- var palco = document.createElement("div");
1799
- palco.className = "palco";
1800
- while (no.firstChild) palco.appendChild(no.firstChild);
1801
- no.appendChild(palco);
1802
-
1803
- var ctx = { raiz: no, duracao: cena.duracao || 4, animar: true };
1804
- aplicarChaves(palco, {
1805
- chaves: chaves,
1806
- suavizar: (cena.camera && cena.camera.suavizar) || "suave",
1807
- }, ctx);
1808
-
1809
- /* PARALAXE — e o cuidado que decide se isto funciona ou quebra o layout.
1810
- · Elemento SEM animação própria recebe a diferença direto: é o caso
1811
- comum, e assim nada muda no DOM.
1812
- · Elemento que JÁ anima (tem chaves suas) precisa de um embrulho —
1813
- duas animações no mesmo nó disputam `transform` e a segunda ganha
1814
- calada. Embrulhar só é seguro quando ele está posicionado em
1815
- absoluto; no fluxo, o embrulho viraria um filho a mais no flex e
1816
- mudaria o espaçamento da cena. Nesse caso a paralaxe é ignorada, e
1817
- vale a animação do elemento, que é a mais específica. */
1818
- var comProfundidade = palco.querySelectorAll("[data-paralaxe]");
1819
- Array.prototype.forEach.call(comProfundidade, function (el) {
1820
- var fator = parseFloat(el.getAttribute("data-paralaxe"));
1821
- if (!isFinite(fator) || fator === 1) return;
1822
- var extras = chavesDeParalaxe(chaves, fator);
1823
- if (!extras) return;
1824
- var suave = (cena.camera && cena.camera.suavizar) || "suave";
1825
-
1826
- if (!el.style.animationName) {
1827
- aplicarChaves(el, { chaves: extras, suavizar: suave }, ctx);
1828
- return;
1829
- }
1830
- if (el.style.position !== "absolute") return;
1831
-
1832
- var capa = document.createElement("div");
1833
- capa.className = "paralaxe";
1834
- capa.style.position = "absolute";
1835
- ["left", "top", "right", "bottom", "width", "height"].forEach(function (k) {
1836
- capa.style[k] = el.style[k];
1837
- el.style[k] = "";
1838
- });
1839
- el.style.position = "relative";
1840
- el.parentNode.insertBefore(capa, el);
1841
- capa.appendChild(el);
1842
- aplicarChaves(capa, { chaves: extras, suavizar: suave }, ctx);
1843
- });
1844
- return no;
1845
- }
1846
-
1847
- function renderCena(cena, doc, op) {
1848
- var no = renderCenaBase(cena, doc, op);
1849
- return aplicarCamera(no, cena || {}, op || {});
1850
- }
1851
-
1852
- if (typeof module !== "undefined" && module.exports) module.exports = { renderCena: renderCena, CAMERAS: CAMERAS };