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.
- package/README.md +41 -37
- package/bin/primocode.js +599 -327
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +545 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
package/studio/web/js/render.js
DELETED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 };
|