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.
@@ -13,6 +13,220 @@
13
13
  var ARQ = null, doc = null;
14
14
  var iCena = 0, iAba = 0, sel = null; // sel = índice do elemento/bloco
15
15
  var escala = 1, historico = [], futuro = [], tSalvar = null, sujo = false;
16
+ var chaveSel = 0; // chave de animação em edição
17
+
18
+ /* ------------------------------------------------------------- vocabulário
19
+ Os menus mostravam o valor cru do documento: "grao", "kb-aproxima", e uma
20
+ opção vazia no topo que não dizia nada. Quem abria o editor via três
21
+ caixas em branco e não tinha como adivinhar o que havia dentro. Cada
22
+ opção agora é [valor, nome em português], e o vazio virou uma frase.
23
+
24
+ Os valores NÃO mudam — são os mesmos que o render.css entende. O que
25
+ muda é só o que a pessoa lê. */
26
+ var OPC = {
27
+ transicao: [["", "Nenhuma (corte seco)"], ["fade", "Suave"], ["deslizar", "Deslizar para o lado"],
28
+ ["subir", "Subir"], ["zoom", "Aproximar"], ["limpar", "Varrer"],
29
+ ["persiana", "Persiana"], ["flash", "Flash"]],
30
+ fundo: [["", "Sem fundo"], ["malha", "Malha"], ["aurora", "Aurora"], ["raios", "Raios"],
31
+ ["brilho", "Brilho"], ["ondas", "Ondas"], ["grade", "Grade"], ["pontos", "Pontos"],
32
+ ["vinheta", "Vinheta"], ["ruido", "Ruído"]],
33
+ acabamento: [["", "Nenhum"], ["grao", "Granulado"], ["vinheta", "Vinheta"],
34
+ ["scanlines", "Linhas de TV"], ["luz", "Vazamento de luz"], ["brilho", "Brilho"],
35
+ ["moldura", "Moldura"]],
36
+ animacao: [["", "Nenhuma"], ["subir", "Subir"], ["surgir", "Surgir"], ["esq", "Da esquerda"],
37
+ ["dir", "Da direita"], ["zoom", "Aproximar"], ["desfoque", "Sair do desfoque"],
38
+ ["cair", "Cair"], ["girar", "Girar"], ["saltar", "Saltar"], ["revelar", "Revelar"],
39
+ ["cortina", "Cortina"], ["elastico", "Elástico"]],
40
+ kenburns: [["", "Parado"], ["aproxima", "Aproximar"], ["afasta", "Afastar"],
41
+ ["esquerda", "Correr para a esquerda"], ["direita", "Correr para a direita"],
42
+ ["cima", "Subir devagar"]],
43
+ };
44
+
45
+ /* Menu com nome legível. Aceita "valor" ou ["valor", "Nome"]. */
46
+ function menu(opcoes, atual, aoMudar) {
47
+ var s = el("select", "campo");
48
+ opcoes.forEach(function (o) {
49
+ var valor = Array.isArray(o) ? o[0] : o;
50
+ var nome = Array.isArray(o) ? o[1] : String(o);
51
+ var op = el("option", "", nome);
52
+ op.value = valor;
53
+ if (String(valor) === String(atual == null ? "" : atual)) op.selected = true;
54
+ s.appendChild(op);
55
+ });
56
+ s.onchange = function () { aoMudar(s.value); };
57
+ return s;
58
+ }
59
+
60
+ /* ---------------------------------------------------------------- arrastar
61
+ Reordenar não existia. As camadas e as cenas só respondiam a clique, e
62
+ quem tentava arrastar — que é o gesto óbvio — não acontecia nada. Este
63
+ é o mesmo comportamento para as duas listas: pega, mostra onde vai cair,
64
+ solta, reordena e salva. */
65
+ function arrastavel(no, indice, mover) {
66
+ no.draggable = true;
67
+ no.addEventListener("dragstart", function (ev) {
68
+ ev.dataTransfer.effectAllowed = "move";
69
+ ev.dataTransfer.setData("text/plain", String(indice));
70
+ no.classList.add("arrastando");
71
+ });
72
+ no.addEventListener("dragend", function () {
73
+ no.classList.remove("arrastando");
74
+ Array.prototype.forEach.call(document.querySelectorAll(".alvo-solta"),
75
+ function (n) { n.classList.remove("alvo-solta"); });
76
+ });
77
+ no.addEventListener("dragover", function (ev) {
78
+ ev.preventDefault();
79
+ ev.dataTransfer.dropEffect = "move";
80
+ no.classList.add("alvo-solta");
81
+ });
82
+ no.addEventListener("dragleave", function () { no.classList.remove("alvo-solta"); });
83
+ no.addEventListener("drop", function (ev) {
84
+ ev.preventDefault();
85
+ ev.stopPropagation();
86
+ no.classList.remove("alvo-solta");
87
+ var de = parseInt(ev.dataTransfer.getData("text/plain"), 10);
88
+ if (isNaN(de) || de === indice) return;
89
+ mover(de, indice);
90
+ });
91
+ }
92
+
93
+ /* Tira o item de uma posição e põe na outra, na mesma lista. */
94
+ function reordenar(lista, de, para) {
95
+ var item = lista.splice(de, 1)[0];
96
+ lista.splice(para, 0, item);
97
+ }
98
+
99
+ function nomeDaOpcao(opcoes, valor) {
100
+ for (var i = 0; i < opcoes.length; i++) {
101
+ if (String(opcoes[i][0]) === String(valor)) return opcoes[i][1];
102
+ }
103
+ return String(valor);
104
+ }
105
+
106
+ /* ----------------------------------------------------------------- modelos
107
+ Cena pronta, com texto de exemplo, para pôr e trocar as palavras.
108
+
109
+ "Nova cena" dava uma cena VAZIA, e cena vazia é a tela em branco: quem
110
+ abre não sabe por onde começar e o resultado sai desalinhado do resto.
111
+ Estes modelos são os mesmos arranjos que a spec ensina à IA — a pessoa
112
+ agora alcança os mesmos, com um clique. */
113
+ var MODELOS = [
114
+ { id: "capa", nome: "Abertura", para: ["corte", "prisma", "tela"],
115
+ cena: { layout: "capa", duracao: 4, fundo: "malha", transicao: "fade", elementos: [
116
+ { tipo: "texto", papel: "chapeu", texto: "NOME DA EMPRESA", an: "surgir" },
117
+ { tipo: "texto", papel: "titulo", texto: "O título que abre", an: "subir" },
118
+ { tipo: "texto", papel: "apoio", texto: "Uma frase que explica do que se trata.", an: "subir" }] } },
119
+ { id: "titulo-lista", nome: "Título e tópicos", para: ["corte", "prisma"],
120
+ cena: { layout: "titulo", duracao: 6, transicao: "deslizar", elementos: [
121
+ { tipo: "texto", papel: "titulo", texto: "Uma ideia por cena" },
122
+ { tipo: "lista", itens: ["Primeiro ponto", "Segundo ponto", "Terceiro ponto"],
123
+ revelar: true, intervalo: 0.9 }] } },
124
+ { id: "numeros", nome: "Números que impressionam", para: ["corte", "prisma", "tela"],
125
+ cena: { layout: "centro", duracao: 5, fundo: "brilho", transicao: "zoom", elementos: [
126
+ { tipo: "numeros", itens: [
127
+ { valor: "3x", rotulo: "mais rápido" },
128
+ { valor: "40%", rotulo: "menos custo" },
129
+ { valor: "24h", rotulo: "no ar" }], an: "saltar" }] } },
130
+ { id: "citacao", nome: "Citação", para: ["corte", "prisma", "tela"],
131
+ cena: { layout: "citacao", duracao: 5, tema: "papel", transicao: "limpar", elementos: [
132
+ { tipo: "citacao", texto: "A frase que alguém disse e vale repetir.",
133
+ autor: "Quem disse", an: "desfoque" }] } },
134
+ { id: "cartoes", nome: "Três cartões", para: ["corte", "prisma"],
135
+ cena: { layout: "titulo", duracao: 6, transicao: "subir", elementos: [
136
+ { tipo: "texto", papel: "titulo", texto: "O que fazemos" },
137
+ { tipo: "cartoes", itens: [
138
+ { titulo: "Primeiro", texto: "Uma linha explicando." },
139
+ { titulo: "Segundo", texto: "Uma linha explicando." },
140
+ { titulo: "Terceiro", texto: "Uma linha explicando." }], revelar: true }] } },
141
+ { id: "secao", nome: "Divisor de seção", para: ["corte", "prisma"],
142
+ cena: { layout: "secao", duracao: 2.5, fundo: "aurora", transicao: "flash", elementos: [
143
+ { tipo: "texto", papel: "titulo", texto: "Parte dois", an: "zoom" }] } },
144
+ { id: "foto", nome: "Foto com frase", para: ["corte", "prisma", "tela"],
145
+ cena: { layout: "capa", duracao: 5, acabamento: "vinheta", transicao: "fade",
146
+ fundo: "vinheta", kenburns: "aproxima", elementos: [
147
+ { tipo: "texto", papel: "titulo", texto: "A frase por cima da imagem", an: "subir" }] } },
148
+ { id: "grafico", nome: "Gráfico", para: ["corte", "prisma"],
149
+ cena: { layout: "titulo", duracao: 6, elementos: [
150
+ { tipo: "texto", papel: "titulo", texto: "Como evoluiu" },
151
+ { tipo: "grafico", grafico: "barras",
152
+ dados: [["Jan", 30], ["Fev", 52], ["Mar", 71], ["Abr", 96]] }] } },
153
+ { id: "fecho", nome: "Fecho / chamada", para: ["corte", "prisma", "tela"],
154
+ cena: { layout: "centro", duracao: 4, fundo: "raios", transicao: "zoom", elementos: [
155
+ { tipo: "texto", papel: "titulo", texto: "Vamos conversar", an: "zoom" },
156
+ { tipo: "texto", papel: "apoio", texto: "seu@email.com", an: "surgir" }] } },
157
+ ];
158
+
159
+ function modelosDaFerramenta() {
160
+ return MODELOS.filter(function (m) { return m.para.indexOf(SLUG) !== -1; });
161
+ }
162
+
163
+ /* A galeria: cada modelo desenhado de verdade, com o renderizador do
164
+ produto. Miniatura falsa mentiria sobre o resultado. */
165
+ function abrirModelos() {
166
+ var fundo = el("div", "folha on");
167
+ var caixa = el("div", "caixa larga");
168
+ caixa.appendChild(el("h3", "", "Escolha um começo"));
169
+ caixa.appendChild(el("p", "sub",
170
+ "Cada um entra como cena nova, com texto de exemplo para você trocar."));
171
+
172
+ var grade = el("div", "modelos");
173
+ var W = doc.largura || 1920, H = doc.altura || 1080;
174
+ var pendentes = [];
175
+
176
+ // Primeiro a cena em branco: quem quer começar do zero não deve ter de
177
+ // procurar.
178
+ var vazio = el("button", "modelo");
179
+ var cxV = el("div", "caixa-mini vazia", svgIcone("mais"));
180
+ vazio.appendChild(cxV);
181
+ vazio.appendChild(el("span", "", "Cena em branco"));
182
+ vazio.onclick = function () { fechar(); novaCena(null); };
183
+ grade.appendChild(vazio);
184
+
185
+ modelosDaFerramenta().forEach(function (m) {
186
+ var b = el("button", "modelo");
187
+ var cx = el("div", "caixa-mini");
188
+ var mini = renderCena(m.cena, doc, {});
189
+ cx.appendChild(mini);
190
+ b.appendChild(cx);
191
+ b.appendChild(el("span", "", m.nome));
192
+ b.onclick = function () { fechar(); novaCena(m); };
193
+ grade.appendChild(b);
194
+ pendentes.push([cx, mini]);
195
+ });
196
+ caixa.appendChild(grade);
197
+
198
+ var rodape = el("div", "rodape");
199
+ var bFechar = el("button", "btn", "Fechar");
200
+ bFechar.onclick = fechar;
201
+ rodape.appendChild(bFechar);
202
+ caixa.appendChild(rodape);
203
+ fundo.appendChild(caixa);
204
+ document.body.appendChild(fundo);
205
+
206
+ // A escala só dá para calcular depois que o nó está no documento e tem
207
+ // largura de verdade.
208
+ pendentes.forEach(function (p) {
209
+ var larg = p[0].clientWidth || 220;
210
+ p[0].style.height = Math.round(larg * H / W) + "px";
211
+ p[1].style.transform = "scale(" + (larg / W) + ")";
212
+ });
213
+
214
+ function fechar() { fundo.remove(); document.removeEventListener("keydown", esc); }
215
+ function esc(ev) { if (ev.key === "Escape") fechar(); }
216
+ document.addEventListener("keydown", esc);
217
+ fundo.onclick = function (ev) { if (ev.target === fundo) fechar(); };
218
+ }
219
+
220
+ function novaCena(modelo) {
221
+ var base = modelo && modelo.cena
222
+ ? JSON.parse(JSON.stringify(modelo.cena))
223
+ : { layout: F.layoutPadrao, duracao: 4, elementos: [] };
224
+ if (!F.tempo) delete base.duracao;
225
+ mudar(function () {
226
+ doc.cenas.splice(iCena + 1, 0, base);
227
+ iCena++; sel = null;
228
+ });
229
+ }
16
230
 
17
231
  /* ------------------------------------------------------------ utilidades */
18
232
 
@@ -236,48 +450,80 @@
236
450
  lista.innerHTML = "";
237
451
 
238
452
  if (F.modelo === "cena") {
239
- $("#painel-rotulo").textContent = SLUG === "corte" ? "Cenas" : (SLUG === "prisma" ? "Slides" : "Pranchas");
240
- var W = doc.largura || 1920, H = doc.altura || 1080;
241
- var minis = [];
242
- (doc.cenas || []).forEach(function (c, i) {
243
- var m = el("div", "miniatura" + (i === iCena ? " ativa" : ""));
244
- var caixa = el("div", "caixa");
245
- var mini = renderCena(c, doc, {});
246
- caixa.appendChild(mini);
247
- m.appendChild(caixa);
248
- m.appendChild(el("div", "num", String(i + 1)));
249
- m.onclick = function () { iCena = i; sel = null; desenhar(); };
250
- lista.appendChild(m);
251
- minis.push([caixa, mini]);
252
- });
253
- // a escala vem da largura real da caixa, que depende do painel
254
- minis.forEach(function (par) {
255
- var larg = par[0].clientWidth || 160;
256
- par[0].style.height = Math.round(larg * H / W) + "px";
257
- par[1].style.transform = "scale(" + (larg / W) + ")";
258
- });
453
+ // No Corte as cenas já vivem na tira do rodapé. Repetir a mesma lista
454
+ // aqui era o pior tipo de duplicação: duas coisas iguais, lado a lado,
455
+ // sem dizer qual manda. Aqui ficam só as camadas — que é o que faltava
456
+ // ter espaço.
457
+ var soCamadas = !!F.tempo;
458
+ if (!soCamadas) {
459
+ $("#painel-rotulo").textContent = SLUG === "prisma" ? "Slides" : "Pranchas";
460
+ var W = doc.largura || 1920, H = doc.altura || 1080;
461
+ var minis = [];
462
+ (doc.cenas || []).forEach(function (c, i) {
463
+ var m = el("div", "miniatura" + (i === iCena ? " ativa" : ""));
464
+ var caixa = el("div", "caixa");
465
+ var mini = renderCena(c, doc, {});
466
+ caixa.appendChild(mini);
467
+ m.appendChild(caixa);
468
+ m.appendChild(el("div", "num", String(i + 1)));
469
+ m.onclick = function () { iCena = i; sel = null; desenhar(); };
470
+ arrastavel(m, i, function (de, para) {
471
+ mudar(function () { reordenar(doc.cenas, de, para); });
472
+ iCena = para; sel = null; desenhar();
473
+ });
474
+ lista.appendChild(m);
475
+ minis.push([caixa, mini]);
476
+ });
477
+ // a escala vem da largura real da caixa, que depende do painel
478
+ minis.forEach(function (par) {
479
+ var larg = par[0].clientWidth || 160;
480
+ par[0].style.height = Math.round(larg * H / W) + "px";
481
+ par[1].style.transform = "scale(" + (larg / W) + ")";
482
+ });
483
+ }
259
484
 
260
- if (sel != null || (cena().elementos || []).length) {
485
+ var els = cena().elementos || [];
486
+ $("#painel-rotulo").textContent = soCamadas ? "Camadas da cena" : $("#painel-rotulo").textContent;
487
+ if (!soCamadas && els.length) {
261
488
  var tit = el("div", "rotulo");
262
- tit.style.cssText = "padding:12px 4px 6px";
489
+ tit.style.cssText = "padding:14px 4px 6px";
263
490
  tit.textContent = "Camadas";
264
491
  lista.appendChild(tit);
265
- (cena().elementos || []).forEach(function (e2, i) {
266
- var c = el("div", "camada" + (i === sel ? " ativa" : ""),
267
- svgIcone(iconeDe(e2)) + "<span>" + rotuloDe(e2) + "</span>");
268
- c.onclick = function () { sel = i; desenhar(); };
269
- lista.appendChild(c);
270
- });
271
492
  }
493
+ if (!els.length) {
494
+ lista.appendChild(el("div", "nada", soCamadas
495
+ ? "Cena vazia. Use a barra da esquerda para pôr texto, lista, número ou imagem."
496
+ : "Nada nesta prancha ainda."));
497
+ }
498
+ // De cima para baixo na lista = da frente para trás na tela, que é como
499
+ // todo editor de camada se comporta. O documento guarda o contrário.
500
+ els.slice().reverse().forEach(function (e2, ordem) {
501
+ var i = els.length - 1 - ordem;
502
+ var c = el("div", "camada" + (i === sel ? " ativa" : ""),
503
+ svgIcone(iconeDe(e2)) + "<span>" + rotuloDe(e2) + "</span>");
504
+ c.appendChild(botoesDaCamada(els, i));
505
+ c.onclick = function () { sel = i; desenhar(); };
506
+ arrastavel(c, i, function (de, para) {
507
+ mudar(function () { reordenar(els, de, para); });
508
+ sel = para; desenhar();
509
+ });
510
+ lista.appendChild(c);
511
+ });
272
512
  return;
273
513
  }
274
514
 
275
515
  if (F.modelo === "documento") {
276
516
  $("#painel-rotulo").textContent = "Blocos";
277
- (doc.blocos || []).forEach(function (b, i) {
517
+ var blocos = doc.blocos || [];
518
+ blocos.forEach(function (b, i) {
278
519
  var c = el("div", "camada" + (i === sel ? " ativa" : ""),
279
520
  svgIcone(iconeDe(b)) + "<span>" + rotuloDe(b) + "</span>");
521
+ c.appendChild(botoesDaCamada(blocos, i));
280
522
  c.onclick = function () { sel = i; desenhar(); };
523
+ arrastavel(c, i, function (de, para) {
524
+ mudar(function () { reordenar(blocos, de, para); });
525
+ sel = para; desenhar();
526
+ });
281
527
  lista.appendChild(c);
282
528
  });
283
529
  return;
@@ -292,6 +538,41 @@
292
538
  });
293
539
  }
294
540
 
541
+ /* Subir, descer, duplicar e apagar direto na camada.
542
+
543
+ Arrastar é o gesto natural, mas não é o único: num item pequeno o
544
+ arrasto erra o alvo, e teclado não arrasta. Os botões fazem o mesmo
545
+ trabalho sem depender de pontaria. */
546
+ function botoesDaCamada(lista, i) {
547
+ var caixa = el("div", "camada-acoes");
548
+ function bt(icone, titulo, fn) {
549
+ var b = el("button", "", svgIcone(icone));
550
+ b.title = titulo;
551
+ b.onclick = function (ev) { ev.stopPropagation(); fn(); };
552
+ caixa.appendChild(b);
553
+ return b;
554
+ }
555
+ bt("subir", "Trazer para a frente", function () {
556
+ if (i >= lista.length - 1) return;
557
+ mudar(function () { reordenar(lista, i, i + 1); });
558
+ sel = i + 1; desenhar();
559
+ }).disabled = i >= lista.length - 1;
560
+ bt("descer", "Mandar para trás", function () {
561
+ if (i <= 0) return;
562
+ mudar(function () { reordenar(lista, i, i - 1); });
563
+ sel = i - 1; desenhar();
564
+ }).disabled = i <= 0;
565
+ bt("duplicar", "Duplicar", function () {
566
+ mudar(function () { lista.splice(i + 1, 0, JSON.parse(JSON.stringify(lista[i]))); });
567
+ sel = i + 1; desenhar();
568
+ });
569
+ bt("lixeira", "Apagar", function () {
570
+ mudar(function () { lista.splice(i, 1); });
571
+ sel = null; desenhar();
572
+ });
573
+ return caixa;
574
+ }
575
+
295
576
  function iconeDe(e) {
296
577
  return { texto: "texto", lista: "lista", cartoes: "cartao", numeros: "numero",
297
578
  tabela: "tabela", codigo: "codigo", citacao: "citacao", forma: "quadrado",
@@ -333,13 +614,20 @@
333
614
  wrap.appendChild(quadro);
334
615
 
335
616
  /* seleção e interação */
336
- var els = Array.prototype.slice.call(cn.querySelectorAll(".el"));
617
+ // Índice do documento -> nó na tela, pelo carimbo que o renderizador
618
+ // deixa em data-i.
619
+ //
620
+ // Antes isto era deduzido pela ordem: o k-ésimo nó era o k-ésimo
621
+ // elemento. A dedução quebra em todo layout de fluxo, porque eles
622
+ // desenham o cabeçalho (chapéu e título) antes do corpo — a ordem na
623
+ // tela não é a do documento. Quem clicava num texto via o editor
624
+ // selecionar outro, e editava o errado. Foi medido: nas três camadas da
625
+ // cena de teste, as três apontavam para o elemento vizinho.
337
626
  var lista = cena().elementos || [];
338
- var mapa = []; // nó -> índice no documento
339
- var k = 0;
340
- lista.forEach(function (e) {
341
- if (e.tipo === "conector") { mapa.push(null); return; }
342
- mapa.push(els[k++] || null);
627
+ var mapa = lista.map(function () { return null; });
628
+ Array.prototype.forEach.call(cn.querySelectorAll(".el[data-i]"), function (no) {
629
+ var i = +no.dataset.i;
630
+ if (i >= 0 && i < mapa.length && !mapa[i]) mapa[i] = no;
343
631
  });
344
632
 
345
633
  mapa.forEach(function (no, i) {
@@ -463,13 +751,8 @@
463
751
  var esp = el("div"); esp.style.flex = "1"; b.appendChild(esp);
464
752
 
465
753
  if (F.modelo === "cena") {
466
- var addC = el("button", "btn", svgIcone("mais") + " Nova cena");
467
- addC.onclick = function () {
468
- mudar(function () {
469
- doc.cenas.splice(iCena + 1, 0, { layout: F.layoutPadrao, elementos: [] });
470
- iCena++; sel = null;
471
- });
472
- };
754
+ var addC = el("button", "btn primo", svgIcone("mais") + " Nova cena");
755
+ addC.onclick = abrirModelos;
473
756
  var dup = el("button", "btn", svgIcone("duplicar"));
474
757
  dup.title = "Duplicar cena";
475
758
  dup.onclick = function () {
@@ -610,12 +893,28 @@
610
893
  caixa.appendChild(mini);
611
894
  clipe.appendChild(caixa);
612
895
  clipe.appendChild(el("div", "dur", dur + "s"));
896
+ // A transição aparece NA tira, entre um clipe e outro, que é onde ela
897
+ // acontece. Escondida num menu do inspetor, ninguém achava.
898
+ if (c.transicao && c.transicao !== "corte") {
899
+ clipe.appendChild(el("div", "tr-marca", nomeDaOpcao(OPC.transicao, c.transicao)));
900
+ }
613
901
  clipe.onclick = function () { iCena = i; sel = null; desenhar(); };
902
+ clipe.ondblclick = function () { Player && Player.tocar && Player.tocar(); };
903
+ arrastavel(clipe, i, function (de, para) {
904
+ mudar(function () { reordenar(doc.cenas, de, para); });
905
+ iCena = para; sel = null; desenhar();
906
+ });
614
907
  t.appendChild(clipe);
615
908
  });
909
+
910
+ var mais = el("button", "clipe-mais", svgIcone("mais"));
911
+ mais.title = "Nova cena";
912
+ mais.onclick = abrirModelos;
913
+ t.appendChild(mais);
914
+
616
915
  var info = el("div");
617
916
  info.style.cssText = "margin-left:12px;font-size:12px;color:var(--fraco2);white-space:nowrap";
618
- info.textContent = "total " + total.toFixed(1) + "s";
917
+ info.textContent = "total " + total.toFixed(1) + "s · arraste para reordenar";
619
918
  t.appendChild(info);
620
919
  }
621
920
 
@@ -790,10 +1089,44 @@
790
1089
  return t;
791
1090
  }
792
1091
 
793
- function selecao(valores, atual, aoMudar) {
1092
+ /* O menu escreve em português.
1093
+
1094
+ Antes ele mostrava o valor cru do documento — "grao", "kb-aproxima",
1095
+ "left" — e uma opção vazia no topo, sem texto nenhum. Três caixas em
1096
+ branco no inspetor, e nada dizendo o que havia dentro delas.
1097
+
1098
+ O dicionário traduz na hora de mostrar; o valor gravado continua o
1099
+ mesmo, porque é ele que o render.css entende. `vazio` é o texto da
1100
+ opção "", que muda com o contexto: em Fundo é "Sem fundo", em
1101
+ Acabamento é "Nenhum". */
1102
+ var NOMES = {
1103
+ malha: "Malha", brilho: "Brilho", grade: "Grade", pontos: "Pontos", suave: "Suave",
1104
+ vinheta: "Vinheta", aurora: "Aurora", raios: "Raios", ondas: "Ondas", ruido: "Ruído",
1105
+ grao: "Granulado", scanlines: "Linhas de TV", luz: "Vazamento de luz", moldura: "Moldura",
1106
+ horizontal: "Na horizontal", vertical: "Na vertical",
1107
+ chapeu: "Chapéu", titulo: "Título", subtitulo: "Subtítulo", apoio: "Apoio",
1108
+ corpo: "Corpo", legenda: "Legenda",
1109
+ left: "À esquerda", center: "Ao centro", right: "À direita",
1110
+ cobrir: "Preencher", conter: "Caber inteira",
1111
+ cinza: "Preto e branco", sepia: "Sépia", contraste: "Contraste", desbotado: "Desbotado",
1112
+ aproximar: "Aproximar", afastar: "Afastar", esquerda: "Para a esquerda",
1113
+ direita: "Para a direita", cima: "Para cima",
1114
+ retangulo: "Retângulo", circulo: "Círculo", triangulo: "Triângulo",
1115
+ linha: "Linha", seta: "Seta",
1116
+ barras: "Barras", area: "Área", pizza: "Pizza", rosca: "Rosca",
1117
+ curva: "Curva", reta: "Reta", cotovelo: "Cotovelo",
1118
+ texto: "Texto", inteiro: "Número inteiro", decimal: "Decimal",
1119
+ moeda: "Dinheiro", percentual: "Porcentagem",
1120
+ "não": "Não", sim: "Sim",
1121
+ 300: "Fino", 400: "Normal", 500: "Médio", 600: "Semi-negrito",
1122
+ 700: "Negrito", 800: "Extra-negrito",
1123
+ };
1124
+
1125
+ function selecao(valores, atual, aoMudar, vazio) {
794
1126
  var s = el("select", "campo");
795
1127
  valores.forEach(function (v) {
796
- var o = el("option", "", String(v));
1128
+ var rotulo = v === "" ? (vazio || "Automático") : (NOMES[v] || String(v));
1129
+ var o = el("option", "", rotulo);
797
1130
  o.value = v;
798
1131
  if (String(v) === String(atual)) o.selected = true;
799
1132
  s.appendChild(o);
@@ -802,18 +1135,32 @@
802
1135
  return s;
803
1136
  }
804
1137
 
1138
+ /* As amostras e o campo hex na MESMA faixa.
1139
+
1140
+ Empilhados, cada cor ocupava três linhas do painel; com dois seletores
1141
+ de cor na mesma tela, sobrava painel só para eles. Agora as amostras
1142
+ rolam numa faixa e o hex fica ao lado — e a amostra escolhida aparece
1143
+ marcada, coisa que antes não dava para saber. */
805
1144
  function seletorCor(valor, aoMudar) {
806
- var caixa = el("div");
807
- caixa.style.cssText = "display:grid;gap:6px";
1145
+ var caixa = el("div", "cor-faixa");
808
1146
  var p = el("div", "cores");
1147
+ var limpar = el("button", "sem-cor");
1148
+ limpar.title = "Sem cor";
1149
+ limpar.onclick = function () { aoMudar(""); };
1150
+ p.appendChild(limpar);
809
1151
  PALETA.forEach(function (c) {
810
1152
  var b = el("button");
811
1153
  b.style.background = c;
1154
+ if (String(valor).toLowerCase() === String(c).toLowerCase()) b.classList.add("ativa");
1155
+ b.title = c;
812
1156
  b.onclick = function () { aoMudar(c); };
813
1157
  p.appendChild(b);
814
1158
  });
815
1159
  caixa.appendChild(p);
816
- caixa.appendChild(inp(valor, aoMudar));
1160
+ var hex = inp(valor, aoMudar);
1161
+ hex.placeholder = "#hex";
1162
+ hex.className = "campo hex";
1163
+ caixa.appendChild(hex);
817
1164
  return caixa;
818
1165
  }
819
1166
 
@@ -837,13 +1184,21 @@
837
1184
  var c = cena();
838
1185
  g0.appendChild(linha("Layout", selecao(F.layouts, c.layout || F.layoutPadrao,
839
1186
  function (v) { mudar(function () { c.layout = v; }); })));
840
- g0.appendChild(linha("Fundo", selecao(["", "malha", "brilho", "grade", "pontos", "suave", "vinheta"],
841
- typeof c.fundo === "string" ? c.fundo : "", function (v) { mudar(function () { if (v) c.fundo = v; else delete c.fundo; }); })));
842
- g0.appendChild(linha("Fundo (extra)", selecao(
843
- ["", "aurora", "raios", "ondas", "nevoa", "xadrez"],
844
- ["aurora", "raios", "ondas", "nevoa", "xadrez"].indexOf(c.fundo) >= 0 ? c.fundo : "",
845
- function (v) { mudar(function () { if (v) c.fundo = v; }); })));
846
- g0.appendChild(linha("Cor fundo", inp(typeof c.fundo === "string" && c.fundo.charAt(0) === "#" ? c.fundo : "",
1187
+ // UM controle para o fundo, não três.
1188
+ //
1189
+ // Havia "Fundo", "Fundo (extra)" e "Cor fundo", lado a lado, e os três
1190
+ // gravavam na MESMA propriedade: escolher num apagava o outro, e dois
1191
+ // deles apareciam vazios porque só um podia estar preenchido por vez.
1192
+ // Era a parte mais confusa do inspetor. Agora é uma lista só, com tudo
1193
+ // que existe, e a cor entra por baixo quando é cor que se quer.
1194
+ var padroes = ["", "malha", "brilho", "grade", "pontos", "suave", "vinheta",
1195
+ "aurora", "raios", "ondas", "nevoa", "xadrez"];
1196
+ var fundoAtual = typeof c.fundo === "string" && c.fundo.charAt(0) !== "#" ? c.fundo : "";
1197
+ g0.appendChild(linha("Fundo", selecao(padroes, fundoAtual, function (v) {
1198
+ mudar(function () { if (v) c.fundo = v; else delete c.fundo; });
1199
+ }, "Sem fundo")));
1200
+ g0.appendChild(linha("Cor do fundo", seletorCor(
1201
+ typeof c.fundo === "string" && c.fundo.charAt(0) === "#" ? c.fundo : "",
847
1202
  function (v) { mudar(function () { if (v) c.fundo = v; else delete c.fundo; }); })));
848
1203
 
849
1204
  var bFoto = el("button", "btn", svgIcone("imagem") + " Foto de fundo");
@@ -855,16 +1210,17 @@
855
1210
  };
856
1211
  g0.appendChild(linha("Imagem", bFoto));
857
1212
 
858
- g0.appendChild(linha("Acabam.", selecao(["", "grao", "vinheta", "scanlines", "luz", "brilho", "moldura"],
859
- c.acabamento || "", function (v) { mudar(function () { if (v) c.acabamento = v; else delete c.acabamento; }); })));
860
- g0.appendChild(linha("Destaque", seletorCor(c.tom || "",
1213
+ g0.appendChild(linha("Acabamento", selecao(["", "grao", "vinheta", "scanlines", "luz", "brilho", "moldura"],
1214
+ c.acabamento || "", function (v) { mudar(function () { if (v) c.acabamento = v; else delete c.acabamento; }); }, "Nenhum")));
1215
+ g0.appendChild(linha("Cor de destaque", seletorCor(c.tom || "",
861
1216
  function (v) { mudar(function () { if (v) c.tom = v; else delete c.tom; }); })));
1217
+ g0.appendChild(linha("Tema da cena", selecao([""].concat(TEMAS), c.tema || "",
1218
+ function (v) { mudar(function () { if (v) c.tema = v; else delete c.tema; }); }, "Igual ao documento")));
862
1219
 
863
1220
  if (F.tempo) {
864
- g0.appendChild(linha("Duração", inp(c.duracao || 4, function (v) { mudar(function () { c.duracao = v || 4; }); }, "number")));
865
- g0.appendChild(linha("Transição", selecao(
866
- ["", "corte", "fade", "deslizar", "subir", "zoom", "limpar", "persiana", "flash"],
867
- c.transicao || "", function (v) { mudar(function () { if (v) c.transicao = v; else delete c.transicao; }); })));
1221
+ g0.appendChild(linha("Duração (s)", inp(c.duracao || 4, function (v) { mudar(function () { c.duracao = v || 4; }); }, "number")));
1222
+ g0.appendChild(linha("Como entra", menu(OPC.transicao, c.transicao || "",
1223
+ function (v) { mudar(function () { if (v) c.transicao = v; else delete c.transicao; }); })));
868
1224
  }
869
1225
  if (SLUG === "traco") {
870
1226
  g0.appendChild(linha("Arranjo", selecao(["", "horizontal", "vertical"], c.auto || "",
@@ -932,12 +1288,10 @@
932
1288
  la.appendChild(inp(e.l, function (v) { edita("l", v); }, "number"));
933
1289
  la.appendChild(inp(e.a, function (v) { edita("a", v); }, "number"));
934
1290
  gp.appendChild(linha("L / A", la));
935
- gp.appendChild(linha("Girar", inp(e.rotacao || 0, function (v) { edita("rotacao", v); }, "number")));
936
- gp.appendChild(linha("Opacid.", inp(e.opacidade != null ? e.opacidade : 1, function (v) { edita("opacidade", v); }, "number")));
937
- gp.appendChild(linha("Entrada", selecao(
938
- ["", "subir", "surgir", "esquerda", "direita", "zoom", "desfoque", "cair",
939
- "girar", "saltar", "revelar", "cortina", "elastico", "palavra", "nenhuma"],
940
- e.anim || "", function (v) { edita("anim", v); })));
1291
+ gp.appendChild(linha("Girar (graus)", inp(e.rotacao || 0, function (v) { edita("rotacao", v); }, "number")));
1292
+ gp.appendChild(linha("Opacidade", inp(e.opacidade != null ? e.opacidade : 1, function (v) { edita("opacidade", v); }, "number")));
1293
+ gp.appendChild(linha("Como entra", menu(OPC.animacao, e.anim || "",
1294
+ function (v) { edita("anim", v); })));
941
1295
  if (e.tipo === "numeros") {
942
1296
  gp.appendChild(linha("Contar", selecao(["não", "sim"], e.contar ? "sim" : "não",
943
1297
  function (v) { edita("contar", v === "sim" ? true : ""); })));
@@ -948,7 +1302,133 @@
948
1302
  dica.style.padding = "10px 16px";
949
1303
  ins.appendChild(dica);
950
1304
  }
1305
+ if (F.tempo) chavesInspetor(ins, e);
1306
+ }
1307
+ }
1308
+
1309
+ /* ─────────────────────────────────────────────────────────── chaves ─────
1310
+ Antes um elemento só sabia ENTRAR: escolhia-se um efeito e acabava ali.
1311
+ Nada percorria a cena, crescia devagar nem sumia no fim.
1312
+
1313
+ Uma chave é um instante: onde o elemento está aos 0%, aos 50%, aos
1314
+ 100% da cena. Duas ou mais viram animação de verdade. A régua embaixo
1315
+ mostra as chaves na linha do tempo da cena — não é enfeite, é o único
1316
+ jeito de ver que existem três chaves e que a do meio está adiantada. */
1317
+ var PRESETS_CHAVE = [
1318
+ { nome: "Atravessar", chaves: [{ t: 0, x: -220, opacidade: 0 }, { t: 0.18, x: 0, opacidade: 1 },
1319
+ { t: 0.82, x: 0, opacidade: 1 }, { t: 1, x: 220, opacidade: 0 }] },
1320
+ { nome: "Crescer devagar", chaves: [{ t: 0, escala: 1 }, { t: 1, escala: 1.18 }] },
1321
+ { nome: "Aparecer e sumir", chaves: [{ t: 0, opacidade: 0 }, { t: 0.15, opacidade: 1 },
1322
+ { t: 0.85, opacidade: 1 }, { t: 1, opacidade: 0 }] },
1323
+ { nome: "Flutuar", chaves: [{ t: 0, y: 0 }, { t: 0.5, y: -22 }, { t: 1, y: 0 }] },
1324
+ { nome: "Sair do desfoque", chaves: [{ t: 0, desfoque: 18, opacidade: 0, escala: 1.06 },
1325
+ { t: 0.35, desfoque: 0, opacidade: 1, escala: 1 }, { t: 1, desfoque: 0, opacidade: 1, escala: 1 }] },
1326
+ ];
1327
+
1328
+ function chavesInspetor(ins, e) {
1329
+ var g = grupo("Animação por chaves");
1330
+ var chaves = e.chaves || [];
1331
+
1332
+ if (!chaves.length) {
1333
+ g.appendChild(el("div", "nada",
1334
+ "Sem chaves: o elemento fica parado depois de entrar."));
1335
+ var pr = el("div", "presets");
1336
+ PRESETS_CHAVE.forEach(function (p) {
1337
+ var b = el("button", "", p.nome);
1338
+ b.onclick = function () {
1339
+ edita("chaves", JSON.parse(JSON.stringify(p.chaves)));
1340
+ };
1341
+ pr.appendChild(b);
1342
+ });
1343
+ g.appendChild(pr);
1344
+ ins.appendChild(g);
1345
+ return;
1346
+ }
1347
+
1348
+ // A régua: cada chave é um pino na duração da cena.
1349
+ var regua = el("div", "regua-chaves");
1350
+ var trilho = el("div", "trilho-chaves");
1351
+ regua.appendChild(trilho);
1352
+ chaves.forEach(function (k, i) {
1353
+ var pino = el("button", "pino" + (i === chaveSel ? " ativo" : ""));
1354
+ pino.style.left = (Math.max(0, Math.min(1, k.t || 0)) * 100) + "%";
1355
+ pino.title = ((k.t || 0) * (cena().duracao || 4)).toFixed(1) + "s";
1356
+ pino.onclick = function () { chaveSel = i; desenharInspetor(); };
1357
+ trilho.appendChild(pino);
1358
+ });
1359
+ g.appendChild(regua);
1360
+ g.appendChild(el("div", "sub-regua",
1361
+ "0s" + " · " + (cena().duracao || 4) + "s no fim"));
1362
+
1363
+ if (chaveSel >= chaves.length) chaveSel = chaves.length - 1;
1364
+ var k = chaves[chaveSel] || chaves[0];
1365
+
1366
+ var nav = el("div", "opcoes");
1367
+ chaves.forEach(function (kk, i) {
1368
+ var b = el("button", i === chaveSel ? "ativo" : "", String(i + 1));
1369
+ b.onclick = function () { chaveSel = i; desenharInspetor(); };
1370
+ nav.appendChild(b);
1371
+ });
1372
+ var bMais = el("button", "", svgIcone("mais"));
1373
+ bMais.title = "Nova chave no fim";
1374
+ bMais.onclick = function () {
1375
+ mudar(function () {
1376
+ var ultima = chaves[chaves.length - 1] || {};
1377
+ var nova = JSON.parse(JSON.stringify(ultima));
1378
+ nova.t = Math.min(1, (ultima.t || 0) + 0.25);
1379
+ chaves.push(nova);
1380
+ chaves.sort(function (a, b2) { return (a.t || 0) - (b2.t || 0); });
1381
+ });
1382
+ chaveSel = chaves.length - 1;
1383
+ desenharInspetor();
1384
+ };
1385
+ nav.appendChild(bMais);
1386
+ g.appendChild(nav);
1387
+
1388
+ function campoChave(rotulo, campo, padrao, passo) {
1389
+ var i = inp(k[campo] != null ? k[campo] : padrao, function (v) {
1390
+ mudar(function () {
1391
+ if (v == null || v === "") delete k[campo];
1392
+ else k[campo] = v;
1393
+ });
1394
+ }, "number");
1395
+ if (passo) i.step = passo;
1396
+ g.appendChild(linha(rotulo, i));
951
1397
  }
1398
+
1399
+ var tSeg = inp(+((k.t || 0) * (cena().duracao || 4)).toFixed(2), function (v) {
1400
+ mudar(function () {
1401
+ k.t = Math.max(0, Math.min(1, (v || 0) / (cena().duracao || 4)));
1402
+ chaves.sort(function (a, b2) { return (a.t || 0) - (b2.t || 0); });
1403
+ });
1404
+ }, "number");
1405
+ tSeg.step = "0.1";
1406
+ g.appendChild(linha("Momento (s)", tSeg));
1407
+
1408
+ campoChave("Deslocar X", "x", 0);
1409
+ campoChave("Deslocar Y", "y", 0);
1410
+ campoChave("Escala", "escala", 1, "0.05");
1411
+ campoChave("Girar (graus)", "giro", 0);
1412
+ campoChave("Opacidade", "opacidade", 1, "0.1");
1413
+ campoChave("Desfoque (px)", "desfoque", 0);
1414
+
1415
+ var acoes = el("div", "opcoes");
1416
+ var bRep = el("button", e.repetir ? "ativo" : "", svgIcone("refazer"));
1417
+ bRep.title = "Repetir sem parar";
1418
+ bRep.onclick = function () { edita("repetir", e.repetir ? "" : true); };
1419
+ var bLin = el("button", e.suavizar === false ? "ativo" : "", svgIcone("linha"));
1420
+ bLin.title = "Movimento constante (sem suavizar)";
1421
+ bLin.onclick = function () { edita("suavizar", e.suavizar === false ? "" : false); };
1422
+ var bDel = el("button", "", svgIcone("lixeira"));
1423
+ bDel.title = "Apagar esta chave";
1424
+ bDel.onclick = function () {
1425
+ mudar(function () { chaves.splice(chaveSel, 1); if (!chaves.length) delete e.chaves; });
1426
+ chaveSel = 0; desenharInspetor();
1427
+ };
1428
+ acoes.appendChild(bRep); acoes.appendChild(bLin); acoes.appendChild(bDel);
1429
+ g.appendChild(acoes);
1430
+
1431
+ ins.appendChild(g);
952
1432
  }
953
1433
 
954
1434
  function camposDoTipo(ins, e) {
@@ -1130,27 +1610,45 @@
1130
1610
 
1131
1611
  /* --- 3. escolha da voz, já ranqueada -------------------------------- */
1132
1612
 
1613
+ // As vozes vêm do servidor, que é quem sintetiza. Cada uma traz a
1614
+ // frequência MEDIDA — quem quer voz de locução escolhe pelo número, não
1615
+ // pelo palpite.
1133
1616
  var voz = doc.voz || {};
1134
- var boas = Narracao.vozes();
1135
1617
  var sel2 = el("select", "campo");
1136
- var opAuto = el("option", "", "melhor disponível");
1137
- opAuto.value = "";
1138
- sel2.appendChild(opAuto);
1139
- boas.forEach(function (v) {
1140
- var o = el("option", "", Narracao.limpo(v.name) + " · " + v.lang);
1141
- o.value = v.name;
1142
- if (v.name === voz.voz) o.selected = true;
1143
- sel2.appendChild(o);
1144
- });
1618
+ function encherVozes(lista) {
1619
+ sel2.innerHTML = "";
1620
+ var opAuto = el("option", "", "Padrão (a mais grave)");
1621
+ opAuto.value = "";
1622
+ sel2.appendChild(opAuto);
1623
+ lista.forEach(function (v) {
1624
+ var o = el("option", "", v.nome + " · " + v.tom
1625
+ + (v.hz ? " · " + v.hz + " Hz" : "")
1626
+ + (v.instalada ? "" : " · baixa ao usar"));
1627
+ o.value = v.id;
1628
+ if (v.id === voz.voz) o.selected = true;
1629
+ sel2.appendChild(o);
1630
+ });
1631
+ }
1632
+ encherVozes(VozNeural.vozes());
1633
+ VozNeural.status().then(function () { encherVozes(VozNeural.vozes()); });
1145
1634
  sel2.onchange = function () {
1146
1635
  mudar(function () { doc.voz = doc.voz || {}; doc.voz.voz = sel2.value; });
1147
1636
  Narracao.falar("Assim que esta voz vai narrar o seu vídeo.",
1148
- { voz: sel2.value, velocidade: voz.velocidade });
1637
+ { voz: sel2.value, grave: voz.grave, velocidade: voz.velocidade });
1149
1638
  };
1150
1639
  gn.appendChild(linha("Voz", sel2));
1151
1640
 
1152
- gn.appendChild(linha("Velocid.", inp(voz.velocidade != null ? voz.velocidade : 0.96,
1153
- function (v) { mudar(function () { doc.voz = doc.voz || {}; doc.voz.velocidade = v || 0.96; }); },
1641
+ var bGrave = el("button", "btn" + (voz.grave ? " primo" : ""),
1642
+ (voz.grave ? "✓ " : "") + "Registro grave, de cinema");
1643
+ bGrave.style.cssText = "width:100%;margin:2px 0 8px";
1644
+ bGrave.title = "Abaixa o tom uns 10% sem mudar a duração";
1645
+ bGrave.onclick = function () {
1646
+ mudar(function () { doc.voz = doc.voz || {}; doc.voz.grave = !voz.grave; });
1647
+ };
1648
+ gn.appendChild(bGrave);
1649
+
1650
+ gn.appendChild(linha("Velocidade", inp(voz.velocidade != null ? voz.velocidade : 1,
1651
+ function (v) { mudar(function () { doc.voz = doc.voz || {}; doc.voz.velocidade = v || 1; }); },
1154
1652
  "number")));
1155
1653
 
1156
1654
  if (F.tempo) {
@@ -1160,24 +1658,41 @@
1160
1658
  gn.appendChild(bTodas);
1161
1659
  }
1162
1660
 
1163
- /* --- 4. diagnóstico honesto ----------------------------------------- */
1164
-
1165
- var d = Narracao.diagnostico();
1166
- if (!Narracao.disponivel()) {
1167
- gn.appendChild(el("div", "nada", "Este navegador não tem síntese de voz."));
1168
- } else if (!d.premium) {
1169
- var dica = el("div", "dica-voz");
1170
- dica.innerHTML =
1171
- "<b>As vozes do sistema estão no modo compacto.</b>" +
1172
- "<p>Só há " + d.boasEmPortugues + " voz útil em português aqui. As vozes " +
1173
- "aprimoradas soam muito melhor e são download gratuito da Apple:</p>" +
1174
- "<p class='caminho'>Ajustes do Sistema → Acessibilidade → Conteúdo Falado → " +
1175
- "Voz do sistema → Gerenciar vozes → Português (Brasil) → baixe as marcadas " +
1176
- "como <b>Premium</b> ou <b>Aprimorada</b></p>" +
1177
- "<p>Depois recarregue esta página. Para qualidade de locução de verdade, " +
1178
- "grave a sua voz no botão acima.</p>";
1179
- gn.appendChild(dica);
1180
- }
1661
+ /* --- 4. o estado real do motor -------------------------------------- */
1662
+
1663
+ // Isto aqui mandava o usuário em "Ajustes do Sistema → Acessibilidade →
1664
+ // ... baixe as vozes da Apple". Era verdade quando a narração dependia
1665
+ // do TTS do sistema. Hoje a síntese é neural e roda no servidor, então o
1666
+ // texto virou instrução para o lugar errado — e num Windows, para um
1667
+ // lugar que nem existe. O que fica é o estado de verdade do motor.
1668
+ var aviso = el("div", "dica-voz");
1669
+ aviso.style.display = "none";
1670
+ gn.appendChild(aviso);
1671
+ VozNeural.status(true).then(function (s) {
1672
+ var p = s.piper || {};
1673
+ if (p.pronta) return;
1674
+ aviso.style.display = "";
1675
+ if (p.baixando && p.baixando.ativo) {
1676
+ aviso.innerHTML = "<b>Preparando a voz neural…</b><p>" + p.baixando.etapa
1677
+ + " · " + p.baixando.pct + "%. Enquanto isso a narração sai por uma voz "
1678
+ + "mais simples; o vídeo não fica mudo.</p>";
1679
+ setTimeout(function () { if (document.body.contains(aviso)) desenharInspetor(); }, 3000);
1680
+ } else if (!p.suportado) {
1681
+ aviso.innerHTML = "<b>Sem voz neural para este sistema.</b>"
1682
+ + "<p>A narração sai pela voz de reserva. Gravar a sua própria voz, no botão "
1683
+ + "acima, continua sendo o melhor resultado.</p>";
1684
+ } else {
1685
+ aviso.innerHTML = "<b>A voz neural ainda não foi baixada.</b>"
1686
+ + "<p>São ~90 MB, uma vez só, e depois funciona sem internet.</p>";
1687
+ var bJa = el("button", "btn primo", "Baixar agora");
1688
+ bJa.style.cssText = "width:100%;margin-top:8px";
1689
+ bJa.onclick = function () {
1690
+ bJa.textContent = "baixando…";
1691
+ VozNeural.instalar(voz.voz).then(function () { desenharInspetor(); });
1692
+ };
1693
+ aviso.appendChild(bJa);
1694
+ }
1695
+ });
1181
1696
 
1182
1697
  ins.appendChild(gn);
1183
1698
  }