primocode 8.12.0 → 8.14.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.
@@ -2,7 +2,10 @@
2
2
  ferramentas.js — o que cada ferramenta oferece na interface.
3
3
  ========================================================================== */
4
4
 
5
- var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta"];
5
+ // Os dez que o render.css define. A lista tinha seis: quatro temas existiam
6
+ // no CSS, a IA podia usá-los, e o editor não os oferecia a ninguém.
7
+ var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta",
8
+ "retro", "oceano", "doce", "mono"];
6
9
 
7
10
  var PALETA = ["#ff5a36", "#ffb03a", "#f4e04d", "#2fbf7f", "#1f8fe0", "#8b5cf6",
8
11
  "#f472b6", "#ffffff", "#9aa4b2", "#111318"];
@@ -47,8 +47,16 @@ function _porPalavra(html) {
47
47
  function aplicarFundo(no, fundo) {
48
48
  if (!fundo) return;
49
49
  if (typeof fundo === "string") {
50
- if (/^(malha|brilho|grade|pontos|suave|vinheta)$/.test(fundo)) no.setAttribute("data-fundo", fundo);
51
- else no.style.background = fundo;
50
+ // Os ONZE que o render.css define.
51
+ //
52
+ // A lista aqui tinha seis. Os outros cinco — aurora, raios, ondas,
53
+ // nevoa, xadrez — caíam no `else` e viravam `background: "aurora"`:
54
+ // CSS inválido, fundo NENHUM. E são justamente os que a spec recomenda
55
+ // para vídeo, então toda cena que pedia aurora ou raios saía chapada.
56
+ // Era metade do "os vídeos ficaram todos iguais".
57
+ if (/^(malha|brilho|grade|pontos|suave|vinheta|aurora|nevoa|ondas|raios|xadrez)$/.test(fundo)) {
58
+ no.setAttribute("data-fundo", fundo);
59
+ } else no.style.background = fundo;
52
60
  return;
53
61
  }
54
62
  if (fundo.tipo === "gradiente") {
@@ -70,13 +78,23 @@ function aplicarFundo(no, fundo) {
70
78
  /* --------------------------------------------------------------- gráficos */
71
79
 
72
80
  function svgGrafico(e, l, a) {
73
- var dados = (e.dados || []).map(function (d) {
81
+ /* Os dados podem chegar como "dados" ou como "itens", e o tipo como
82
+ "grafico", "estilo" ou "tipo".
83
+
84
+ O nome certo é `dados` + `grafico`, e é o que a spec ensina. Mas quem
85
+ escreve um gráfico — a IA ou uma pessoa — chuta `itens`, porque é o
86
+ campo de TODOS os outros elementos da mesma spec (lista, cartoes,
87
+ numeros usam `itens`). Com o nome trocado esta função devolvia string
88
+ vazia: o gráfico simplesmente não existia na tela, sem erro nenhum.
89
+ Aceitar as duas grafias custa uma linha e evita a falha muda. */
90
+ var bruto = e.dados || e.itens || [];
91
+ var dados = bruto.map(function (d) {
74
92
  return Array.isArray(d) ? { r: d[0], v: +d[1] || 0 } : { r: d.rotulo, v: +d.valor || 0 };
75
93
  });
76
94
  if (!dados.length) return "";
77
95
  var cores = e.cores || ["var(--c-tom)", "var(--c-tom2)", "#7c9cff", "#2fbf7f", "#f472b6", "#facc15"];
78
96
  var max = Math.max.apply(null, dados.map(function (d) { return d.v; })) || 1;
79
- var tipo = e.grafico || "barras";
97
+ var tipo = e.grafico || e.estilo || e.tipoGrafico || "barras";
80
98
  var pad = { e: 8, d: 8, t: 14, b: 42 };
81
99
  var iw = l - pad.e - pad.d, ih = a - pad.t - pad.b;
82
100
  var p = ['<svg viewBox="0 0 ' + l + " " + a + '" width="' + l + '" height="' + a + '">'];
@@ -138,11 +156,233 @@ function svgGrafico(e, l, a) {
138
156
  /* -------------------------------------------------------------- elementos */
139
157
 
140
158
  var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
141
- citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1 };
159
+ citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1,
160
+ objeto3d: 1 };
161
+
162
+ /* ── chaves de animação ──────────────────────────────────────────────────
163
+ Transforma a lista de instantes de um elemento numa regra @keyframes de
164
+ verdade e prende ela no elemento.
165
+
166
+ Os nomes precisam ser únicos por página: o mesmo documento aparece várias
167
+ vezes ao mesmo tempo (palco, miniaturas do painel, clipes da tira), e
168
+ dois @keyframes de mesmo nome com conteúdo diferente se atropelariam —
169
+ a última regra injetada venceria em TODAS as cópias. */
170
+ var _nChave = 0;
171
+
172
+ function _numero(v, padrao) {
173
+ return typeof v === "number" && isFinite(v) ? v : padrao;
174
+ }
175
+
176
+ function aplicarChaves(no, e, ctx) {
177
+ var chaves = e.chaves.slice().sort(function (a, b) {
178
+ return _numero(a.t, 0) - _numero(b.t, 0);
179
+ });
180
+
181
+ var nome = "ch" + (++_nChave);
182
+ var passos = chaves.map(function (k) {
183
+ var pct = Math.max(0, Math.min(1, _numero(k.t, 0))) * 100;
184
+ var mov = [];
185
+ // x e y de uma chave são DESLOCAMENTO a partir de onde o elemento está.
186
+ // Assim a chave continua valendo se a pessoa arrastar o elemento no
187
+ // palco depois — o contrário obrigaria a refazer todas as chaves.
188
+ var dx = _numero(k.x, 0), dy = _numero(k.y, 0);
189
+ if (dx || dy) mov.push("translate(" + dx + "px," + dy + "px)");
190
+ var esc = _numero(k.escala, 1);
191
+ if (esc !== 1) mov.push("scale(" + esc + ")");
192
+ var giro = _numero(k.giro, 0);
193
+ if (giro) mov.push("rotate(" + giro + "deg)");
194
+
195
+ var linhas = ["transform:" + (mov.length ? mov.join(" ") : "none")];
196
+ var op = _numero(k.opacidade, 1);
197
+ linhas.push("opacity:" + op);
198
+ if (typeof k.desfoque === "number" && k.desfoque > 0) {
199
+ linhas.push("filter:blur(" + k.desfoque + "px)");
200
+ }
201
+ return pct.toFixed(3) + "%{" + linhas.join(";") + "}";
202
+ }).join("");
203
+
204
+ // A regra vai na RAIZ da cena, não dentro do elemento.
205
+ //
206
+ // Presa no próprio elemento ela era apagada segundos depois: quem desenha
207
+ // o conteúdo (texto, lista, número) escreve o innerHTML e leva junto tudo
208
+ // que estivesse ali. O elemento continuava com `animation-name: ch1`
209
+ // apontando para uma regra que não existia mais — a animação simplesmente
210
+ // não acontecia, sem erro nenhum no console. A cena só recebe filhos, e
211
+ // por isso a regra sobrevive lá.
212
+ var folha = document.createElement("style");
213
+ folha.textContent = "@keyframes " + nome + "{" + passos + "}";
214
+ (ctx.raiz || no).appendChild(folha);
215
+
216
+ var dur = _numero(ctx.duracao, 4);
217
+ no.style.animationName = nome;
218
+ no.style.animationDuration = dur + "s";
219
+ no.style.animationTimingFunction = e.suavizar === false ? "linear" : "cubic-bezier(.4,0,.2,1)";
220
+ no.style.animationFillMode = "both";
221
+ no.style.animationIterationCount = e.repetir ? "infinite" : "1";
222
+ }
223
+
224
+ /* ── identidade da marca ─────────────────────────────────────────────────
225
+ "Ele não escolhe as cores dos textos, não escolhe tema, não escolhe uma
226
+ imagem de fundo, não coloca uma logo. Ficou horrível."
227
+
228
+ O logo era o mais gritante: não havia NENHUM jeito de pôr um. Dava para
229
+ subir a imagem e posicionar à mão, cena por cena — o que ninguém faz num
230
+ vídeo de dez cenas, e a IA nunca fazia.
231
+
232
+ `doc.marca` resolve de uma vez: o logo aparece em todas as cenas, no
233
+ canto, no tamanho certo, sem entrar no fluxo do layout. E `marca.cor`
234
+ define a cor de destaque do documento inteiro, que é o que dá identidade
235
+ aos números, às barras do gráfico e aos detalhes.
236
+
237
+ "marca": { "logo": "/midia/logo.png", "cor": "#ff5a36",
238
+ "canto": "inferior-direita", "opacidade": 0.85 }
239
+
240
+ `cena.semMarca: true` tira de uma cena específica — a capa costuma querer
241
+ o logo grande no meio, não pequeno no canto. */
242
+ var CANTOS = {
243
+ "superior-esquerda": "top:5.2%;left:5%",
244
+ "superior-direita": "top:5.2%;right:5%",
245
+ "inferior-esquerda": "bottom:5.2%;left:5%",
246
+ "inferior-direita": "bottom:5.2%;right:5%",
247
+ };
248
+
249
+ function aplicarMarca(no, cena, doc, H) {
250
+ var m = doc && doc.marca;
251
+ if (!m || typeof m !== "object") return;
252
+ // A cor da marca vira a cor de destaque de tudo, a menos que a cena diga
253
+ // outra — é ela que pinta número, barra de gráfico e detalhe.
254
+ if (m.cor && !cena.tom) no.style.setProperty("--c-tom", m.cor);
255
+ if (!m.logo || cena.semMarca) return;
256
+
257
+ var caixa = document.createElement("div");
258
+ caixa.className = "marca-dagua";
259
+ caixa.style.cssText = "position:absolute;z-index:6;pointer-events:none;"
260
+ + (CANTOS[m.canto] || CANTOS["inferior-direita"])
261
+ + ";opacity:" + (m.opacidade != null ? m.opacidade : 0.85);
262
+ var img = document.createElement("img");
263
+ img.src = m.logo;
264
+ img.alt = "";
265
+ // Altura proporcional à tela, resolvida em PIXELS: a caixa é absoluta e
266
+ // sem altura própria, e porcentagem dentro dela não resolveria — o logo
267
+ // sairia no tamanho original, gigante num caso e sumido no outro.
268
+ var alturaPx = Math.round((H || 1080) * ((m.altura || 4.6) / 100));
269
+ img.style.cssText = "height:" + alturaPx + "px;width:auto;display:block";
270
+ caixa.appendChild(img);
271
+ no.appendChild(caixa);
272
+ }
273
+
274
+ /* ── objetos 3D ──────────────────────────────────────────────────────────
275
+ "Quero que tu coloque a função ali no vídeo, na apresentação de slides,
276
+ pra fazer objeto 3D com animação, tudo assim, bem top."
277
+
278
+ Isto é 3D de verdade — perspectiva, faces, profundidade — feito com
279
+ transform 3D do próprio navegador. Sem WebGL e sem biblioteca: o Studio
280
+ não tem dependência nenhuma e não vai passar a ter por causa disto. O que
281
+ dá para fazer assim cobre o que um vídeo institucional pede: um cubo
282
+ girando com o logo nas faces, um cartão com espessura, uma pilha de
283
+ camadas em profundidade.
284
+
285
+ { "tipo": "objeto3d", "forma": "cubo", "tamanho": 320,
286
+ "faces": ["Produto", "Dados", "IA"], "girar": 14 }
287
+
288
+ `girar` é a volta completa em segundos; 0 deixa parado no ângulo dado.
289
+ `faces` aceita texto ou URL de imagem. E como o elemento é um nó normal,
290
+ as chaves de animação valem aqui também: dá para o cubo atravessar a cena
291
+ girando. */
292
+ var FORMAS_3D = { cubo: 1, caixa: 1, cartao: 1, prisma: 1, painel: 1 };
293
+
294
+ function _face3d(conteudo, transform, cor) {
295
+ var f = document.createElement("div");
296
+ f.className = "face3d";
297
+ f.style.transform = transform;
298
+ if (cor) f.style.background = cor;
299
+ if (/^(https?:|\/)/.test(String(conteudo || ""))) {
300
+ var im = document.createElement("img");
301
+ im.src = conteudo;
302
+ im.alt = "";
303
+ f.appendChild(im);
304
+ } else if (conteudo) {
305
+ f.textContent = conteudo;
306
+ }
307
+ return f;
308
+ }
309
+
310
+ function montar3d(no, e) {
311
+ var forma = FORMAS_3D[e.forma] ? e.forma : "cubo";
312
+ var t = +e.tamanho || 320;
313
+ // A caixa e o prisma são o cubo achatado num eixo: uma medida só muda a
314
+ // silhueta inteira, e o autor não precisa pensar em seis faces.
315
+ var prof = forma === "cartao" || forma === "painel" ? Math.max(6, Math.round(t * 0.045))
316
+ : (forma === "caixa" ? Math.round(t * 0.62) : t);
317
+ var faces = Array.isArray(e.faces) ? e.faces : [];
318
+ var cores = Array.isArray(e.cores) ? e.cores : [];
319
+
320
+ no.classList.add("obj3d", "f-" + forma);
321
+ no.style.width = _px(t);
322
+ no.style.height = _px(forma === "cartao" ? Math.round(t * 0.62) : t);
323
+ no.style.perspective = _px(e.perspectiva || t * 3.4);
324
+
325
+ var corpo = document.createElement("div");
326
+ corpo.className = "corpo3d";
327
+ corpo.style.width = _px(t);
328
+ corpo.style.height = _px(forma === "cartao" ? Math.round(t * 0.62) : t);
329
+
330
+ var meia = prof / 2;
331
+ var h = forma === "cartao" ? Math.round(t * 0.62) : t;
332
+ var lados = [
333
+ ["translateZ(" + meia + "px)", 0],
334
+ ["rotateY(180deg) translateZ(" + meia + "px)", 1],
335
+ ["rotateY(90deg) translateZ(" + (t / 2) + "px)", 2],
336
+ ["rotateY(-90deg) translateZ(" + (t / 2) + "px)", 3],
337
+ ["rotateX(90deg) translateZ(" + (h / 2) + "px)", 4],
338
+ ["rotateX(-90deg) translateZ(" + (h / 2) + "px)", 5],
339
+ ];
340
+ // Cartão e painel têm só frente e verso: as bordas viram espessura, e
341
+ // desenhar seis faces num objeto de 6px de profundidade só faria sujeira.
342
+ var quantas = (forma === "cartao" || forma === "painel") ? 2 : 6;
343
+ for (var i = 0; i < quantas; i++) {
344
+ var lado = lados[i];
345
+ var f = _face3d(faces[lado[1]] != null ? faces[lado[1]] : (faces[0] || ""),
346
+ lado[0], cores[lado[1] % (cores.length || 1)]);
347
+ // As laterais são estreitas (a profundidade) e o topo/base são baixos.
348
+ //
349
+ // Só mudar a medida não bastava: a face é `inset: 0`, então encostava na
350
+ // borda esquerda em vez de ficar no meio — e a rotação, que gira em
351
+ // torno do centro, abria o objeto. O cubo saía com as faces soltas, sem
352
+ // fechar. Reposicionar a face no centro antes de girar fecha a caixa.
353
+ if (i >= 2 && i < 4) {
354
+ f.style.width = _px(prof);
355
+ f.style.left = _px(Math.round((t - prof) / 2));
356
+ }
357
+ if (i >= 4) {
358
+ f.style.height = _px(prof);
359
+ f.style.top = _px(Math.round((h - prof) / 2));
360
+ }
361
+ corpo.appendChild(f);
362
+ }
363
+
364
+ var giro = e.girar == null ? 14 : +e.girar;
365
+ var inclina = e.inclinar == null ? -14 : +e.inclinar;
366
+ if (giro > 0) {
367
+ corpo.style.animation = "giro3d " + giro + "s linear infinite";
368
+ corpo.style.setProperty("--inc", inclina + "deg");
369
+ } else {
370
+ corpo.style.transform = "rotateX(" + inclina + "deg) rotateY(" + (e.angulo || -28) + "deg)";
371
+ }
372
+ no.appendChild(corpo);
373
+ }
142
374
 
143
375
  function renderElemento(e, ctx) {
144
376
  var no = document.createElement("div");
145
377
  var tipo = e.tipo || "texto";
378
+ /* Qual elemento do documento é este nó.
379
+
380
+ O editor precisava saber, e antes deduzia pela ordem dos nós na tela.
381
+ A dedução estava errada: os layouts de fluxo separam cabeçalho de corpo
382
+ e desenham o cabeçalho primeiro, então a ordem na tela NÃO é a ordem do
383
+ documento. O efeito era clicar num texto e o editor selecionar outro —
384
+ e mexer no elemento errado. O índice vem carimbado daqui. */
385
+ if (ctx && ctx.ordem && ctx.ordem.has(e)) no.dataset.i = ctx.ordem.get(e);
146
386
  /* Tipo inventado ("titulo", "subtitulo"...) caía num default sem estilo:
147
387
  texto de 16px num canvas de 1920 — invisível — ou div vazio quando o
148
388
  conteúdo estava noutro campo. O servidor novo recusa; documentos velhos
@@ -178,11 +418,27 @@ function renderElemento(e, ctx) {
178
418
 
179
419
  /* animação */
180
420
  var an = e.anim || ctx.animPadrao;
181
- if (an && an !== "nenhuma") {
421
+ if (an && an !== "nenhuma" && !(e.chaves && e.chaves.length > 1)) {
182
422
  no.setAttribute("data-an", typeof an === "string" ? an : (an.entrada || "subir"));
183
423
  no.style.setProperty("--atraso", (typeof an === "object" && an.atraso != null ? an.atraso : ctx.i * 0.09 + 0.1));
184
424
  }
185
425
 
426
+ /* ── chaves de animação ────────────────────────────────────────────────
427
+ Antes um elemento só sabia ENTRAR: escolhia-se um efeito de entrada e
428
+ acabava ali. Não dava para fazer nada percorrer a cena, crescer devagar
429
+ ou sumir no fim — que é o que separa animação de slide com efeito.
430
+
431
+ Uma chave é um instante: `{t: 0.5, x: 200, escala: 1.2}` diz onde o
432
+ elemento está na metade da cena. Duas ou mais viram uma animação CSS de
433
+ verdade, gerada aqui, com a duração da cena. O navegador interpola;
434
+ nós só descrevemos os pontos.
435
+
436
+ Quem tem chaves não recebe animação de entrada: as duas brigariam pelo
437
+ mesmo transform, e a chave é a mais específica das duas. */
438
+ if (ctx.animar && e.chaves && e.chaves.length > 1) {
439
+ aplicarChaves(no, e, ctx);
440
+ }
441
+
186
442
  switch (tipo) {
187
443
  case "texto": {
188
444
  var papel = PAPEIS[e.papel] || PAPEIS.corpo;
@@ -355,6 +611,11 @@ function renderElemento(e, ctx) {
355
611
  break;
356
612
  }
357
613
 
614
+ case "objeto3d": {
615
+ montar3d(no, e);
616
+ break;
617
+ }
618
+
358
619
  case "no": {
359
620
  if (e.destaque) no.classList.add("destaque");
360
621
  if (e.formato === "circulo") no.classList.add("circulo");
@@ -546,6 +807,7 @@ function renderCena(cena, doc, op) {
546
807
  if (cena.tom) no.style.setProperty("--c-tom", cena.tom);
547
808
  if (cena.tom2) no.style.setProperty("--c-tom2", cena.tom2);
548
809
  aplicarFundo(no, cena.fundo);
810
+ aplicarMarca(no, cena, doc, H);
549
811
 
550
812
  var layout = cena.layout || (op.layoutPadrao || "titulo");
551
813
  var elementos = (cena.elementos || []).slice();
@@ -560,7 +822,21 @@ function renderCena(cena, doc, op) {
560
822
  // tela vertical pede tipografia maior: os padrões são calibrados em 1920x1080
561
823
  var escalaTipo = H > W ? 1.35 : 1;
562
824
  no.style.setProperty("--esc", escalaTipo);
563
- var ctx = { i: 0, escalaTipo: escalaTipo, animPadrao: op.animar ? (cena.anim || "subir") : null };
825
+ var ctx = {
826
+ i: 0, escalaTipo: escalaTipo,
827
+ animPadrao: op.animar ? (cena.anim || "subir") : null,
828
+ // as chaves precisam saber quanto dura a cena para virar segundos
829
+ animar: !!op.animar,
830
+ duracao: cena.duracao || 4,
831
+ raiz: no,
832
+ // objeto do elemento -> posição dele em cena.elementos, tirada ANTES de
833
+ // qualquer separação por layout
834
+ ordem: (function () {
835
+ var m = new Map();
836
+ (cena.elementos || []).forEach(function (e, i) { m.set(e, i); });
837
+ return m;
838
+ })(),
839
+ };
564
840
 
565
841
  if (layout === "livre") {
566
842
  fluxo.style.padding = "0";
@@ -582,3 +582,50 @@
582
582
  .cena .el { animation: none !important; opacity: 1 !important; }
583
583
  .folha-doc { padding: 0; width: auto; min-height: 0; }
584
584
  }
585
+
586
+ /* ══════════════════════════════════════════════════════════════════════════
587
+ Objetos 3D
588
+
589
+ Perspectiva de verdade, com transform 3D do navegador. Sem WebGL e sem
590
+ biblioteca — o Studio não tem dependência e não vai passar a ter por isto.
591
+ ═════════════════════════════════════════════════════════════════════════ */
592
+ .el.obj3d { display: grid; place-items: center; }
593
+ .corpo3d {
594
+ position: relative;
595
+ transform-style: preserve-3d;
596
+ transform: rotateX(-14deg) rotateY(-28deg);
597
+ }
598
+ .face3d {
599
+ /* Sem isto a borda e o padding SOMAM ao tamanho pedido: cada face fica
600
+ maior do que a medida usada no translateZ, e o cubo não fecha — as
601
+ faces ficam soltas, com fresta. O render.css é usado sozinho no HTML
602
+ exportado, então não dá para contar com o reset do core.css. */
603
+ box-sizing: border-box;
604
+ position: absolute; inset: 0;
605
+ display: grid; place-items: center;
606
+ background: var(--c-tom, #ff5a36);
607
+ color: #fff;
608
+ font: 700 clamp(14px, 2.2vw, 40px)/1.15 var(--f-display, inherit);
609
+ text-align: center;
610
+ padding: 6%;
611
+ border: 1px solid rgba(255, 255, 255, .16);
612
+ border-radius: 6px;
613
+ box-shadow: inset 0 0 90px rgba(0, 0, 0, .34);
614
+ backface-visibility: visible;
615
+ overflow: hidden;
616
+ }
617
+ .face3d img { width: 100%; height: 100%; object-fit: cover; display: block; }
618
+ /* cada face pega uma luz diferente, senão o cubo vira uma mancha só */
619
+ .face3d:nth-child(2) { filter: brightness(.78); }
620
+ .face3d:nth-child(3) { filter: brightness(.88); }
621
+ .face3d:nth-child(4) { filter: brightness(.7); }
622
+ .face3d:nth-child(5) { filter: brightness(1.1); }
623
+ .face3d:nth-child(6) { filter: brightness(.6); }
624
+
625
+ @keyframes giro3d {
626
+ from { transform: rotateX(var(--inc, -14deg)) rotateY(0deg); }
627
+ to { transform: rotateX(var(--inc, -14deg)) rotateY(360deg); }
628
+ }
629
+
630
+ /* painel: sem moldura, é uma superfície flutuando em profundidade */
631
+ .el.obj3d.f-painel .face3d { border: 0; box-shadow: 0 40px 90px -30px rgba(0,0,0,.7); }