primocode 8.12.0 → 8.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.12.0",
3
+ "version": "8.13.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -185,6 +185,33 @@ Na cena, `transicao` define como ela **entra**:
185
185
 
186
186
  Vale o mesmo conselho das animações: varie. Tudo em `fade` parece slide, não vídeo.
187
187
 
188
+ ## Chaves de animação
189
+
190
+ `anim` faz o elemento **entrar** e parar por ali. Para ele se mexer ao longo da
191
+ cena — atravessar, crescer devagar, sumir no fim — use `chaves`: uma lista de
192
+ instantes, com `t` de 0 (começo da cena) a 1 (fim).
193
+
194
+ ```json
195
+ { "tipo": "texto", "texto": "Atravessa a tela",
196
+ "chaves": [
197
+ { "t": 0, "x": -400, "opacidade": 0 },
198
+ { "t": 0.25, "x": 0, "opacidade": 1 },
199
+ { "t": 0.75, "x": 0, "opacidade": 1 },
200
+ { "t": 1, "x": 400, "opacidade": 0 }
201
+ ] }
202
+ ```
203
+
204
+ Campos de uma chave, todos opcionais: `x` e `y` (**deslocamento** em pixels a
205
+ partir de onde o elemento está — não é posição absoluta), `escala` (1 = tamanho
206
+ normal), `giro` (graus), `opacidade` (0 a 1) e `desfoque` (pixels).
207
+
208
+ No elemento: `"repetir": true` roda sem parar, e `"suavizar": false` dá
209
+ movimento constante em vez de acelerar e frear.
210
+
211
+ Duas chaves já bastam. Quem tem `chaves` ignora `anim` — as duas disputariam o
212
+ mesmo movimento, e a chave é mais específica. Use em **um ou dois** elementos
213
+ por vídeo: tudo se mexendo ao mesmo tempo cansa e esconde a mensagem.
214
+
188
215
  ## Acabamento
189
216
 
190
217
  Textura por cima da cena inteira — é o que tira a cara de template:
@@ -381,9 +381,18 @@ input.titulo-arq:focus { outline: none; border-color: var(--primo); background:
381
381
  }
382
382
  .grupo { border-bottom: 1px solid var(--linha); padding: 14px; }
383
383
  .grupo > .rotulo { display: block; margin-bottom: 11px; }
384
- .linha-campo { display: grid; grid-template-columns: 62px 1fr; align-items: center; gap: 8px; margin-bottom: 8px; }
384
+ /* Rótulo em cima, controle embaixo.
385
+
386
+ A coluna do rótulo tinha 62px fixos. "Acabamento" não cabia e saía
387
+ cortado; "Cor do fundo" e "Tema da cena" quebravam em duas linhas e
388
+ desalinhavam a altura de tudo. Era metade da sensação de "estranho" no
389
+ painel. Empilhado, o nome cabe inteiro e o controle usa a largura toda. */
390
+ .linha-campo { display: grid; gap: 4px; margin-bottom: 11px; }
385
391
  .linha-campo:last-child { margin-bottom: 0; }
386
- .linha-campo > span { font-size: 11.5px; color: var(--fraco2); }
392
+ .linha-campo > span {
393
+ font-size: 10.5px; color: var(--fraco2);
394
+ letter-spacing: .04em; text-transform: uppercase;
395
+ }
387
396
  .dois { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
388
397
  .tres { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
389
398
  .opcoes { display: flex; gap: 4px; }
@@ -721,3 +730,125 @@ body:has(.transporte) .aviso { bottom: 80px; }
721
730
  .topo { gap: 6px; padding: 0 8px; }
722
731
  .marca em { display: none; }
723
732
  }
733
+
734
+ /* ══════════════════════════════════════════════════════════════════════════
735
+ Reordenar, modelos e ações de camada
736
+
737
+ Três coisas que faltavam e faziam o editor parecer travado: arrastar não
738
+ dava retorno nenhum, camada não tinha ação à mão, e "nova cena" abria uma
739
+ tela em branco. O visual aqui é de propósito discreto — a peça que a
740
+ pessoa está fazendo é que tem de chamar atenção, não a ferramenta.
741
+ ═════════════════════════════════════════════════════════════════════════ */
742
+
743
+ /* enquanto arrasta: o item sai de cena, o alvo se anuncia */
744
+ .arrastando { opacity: .35; }
745
+ .camada.alvo-solta, .miniatura.alvo-solta, .clipe.alvo-solta {
746
+ outline: 2px solid var(--primo);
747
+ outline-offset: 1px;
748
+ }
749
+ .camada, .miniatura, .clipe { -webkit-user-drag: element; }
750
+ .camada:active { cursor: grabbing; }
751
+
752
+ /* ações que aparecem ao passar o mouse na camada */
753
+ .camada { position: relative; }
754
+ .camada-acoes {
755
+ display: none; gap: 1px; margin-left: auto; flex: 0 0 auto;
756
+ }
757
+ .camada:hover .camada-acoes, .camada.ativa .camada-acoes { display: flex; }
758
+ .camada-acoes button {
759
+ background: none; border: 0; padding: 3px;
760
+ border-radius: 5px; cursor: pointer; color: var(--fraco2);
761
+ display: grid; place-items: center;
762
+ }
763
+ .camada-acoes button:hover { background: var(--sup3, rgba(255,255,255,.08)); color: var(--texto); }
764
+ .camada-acoes button:disabled { opacity: .25; cursor: default; }
765
+ .camada-acoes button:disabled:hover { background: none; color: var(--fraco2); }
766
+ .camada-acoes svg { width: 12px; height: 12px; opacity: 1; }
767
+
768
+ /* a transição marcada no próprio clipe da linha do tempo */
769
+ .clipe .tr-marca {
770
+ position: absolute; left: 4px; top: 4px;
771
+ background: rgba(6, 6, 8, .72);
772
+ color: #cbd5e1;
773
+ font-size: 9.5px; letter-spacing: .02em;
774
+ padding: 2px 5px; border-radius: 4px;
775
+ pointer-events: none;
776
+ max-width: calc(100% - 8px);
777
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
778
+ }
779
+
780
+ /* o "+" no fim da tira */
781
+ .clipe-mais {
782
+ flex: 0 0 auto;
783
+ width: 40px; height: 62px;
784
+ border: 1px dashed var(--linha2);
785
+ border-radius: 8px;
786
+ background: none; color: var(--fraco2);
787
+ cursor: pointer;
788
+ display: grid; place-items: center;
789
+ }
790
+ .clipe-mais:hover { border-color: var(--primo); color: var(--primo); }
791
+ .clipe-mais svg { width: 15px; height: 15px; }
792
+
793
+ /* galeria de modelos */
794
+ .folha .caixa.larga { width: min(1040px, 100%); }
795
+ .folha .sub { color: var(--fraco); font-size: 12.5px; margin: -8px 0 18px; }
796
+ .modelos {
797
+ display: grid;
798
+ grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
799
+ gap: 14px;
800
+ }
801
+ .modelo {
802
+ background: none; border: 0; padding: 0;
803
+ cursor: pointer; text-align: left;
804
+ display: grid; gap: 7px;
805
+ }
806
+ .modelo .caixa-mini {
807
+ position: relative; width: 100%; aspect-ratio: 16/9;
808
+ overflow: hidden; border-radius: 9px;
809
+ border: 1px solid var(--linha);
810
+ background: var(--sup2);
811
+ transition: border-color .12s, transform .12s;
812
+ }
813
+ .modelo .caixa-mini .cena { position: absolute; top: 0; left: 0; transform-origin: top left; }
814
+ .modelo .caixa-mini.vazia { display: grid; place-items: center; border-style: dashed; }
815
+ .modelo .caixa-mini.vazia svg { width: 22px; height: 22px; color: var(--fraco2); }
816
+ .modelo:hover .caixa-mini { border-color: var(--primo); transform: translateY(-2px); }
817
+ .modelo span { font-size: 12.5px; color: var(--fraco); }
818
+ .modelo:hover span { color: var(--texto); }
819
+
820
+ /* seletor de cor compacto: amostras numa faixa, hex ao lado */
821
+ .cor-faixa { display: grid; grid-template-columns: 1fr 62px; gap: 6px; align-items: center; }
822
+ .cor-faixa .cores { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; }
823
+ .cor-faixa .cores::-webkit-scrollbar { height: 0; }
824
+ .cor-faixa .cores button { width: 19px; height: 19px; flex: 0 0 auto; }
825
+ .cor-faixa .cores button.ativa { outline: 2px solid var(--texto); outline-offset: 1px; }
826
+ .cor-faixa .sem-cor {
827
+ background: repeating-linear-gradient(45deg, var(--sup2) 0 4px, var(--linha2) 4px 8px);
828
+ }
829
+ .campo.hex { font-size: 11px; padding-left: 6px; padding-right: 6px; }
830
+
831
+ /* chaves de animação: a régua mostra QUANDO cada uma acontece */
832
+ .regua-chaves { position: relative; height: 26px; margin: 2px 0 2px; }
833
+ .trilho-chaves {
834
+ position: absolute; left: 5px; right: 5px; top: 11px; height: 3px;
835
+ background: var(--sup3); border-radius: 2px;
836
+ }
837
+ .trilho-chaves .pino {
838
+ position: absolute; top: 50%; width: 11px; height: 11px;
839
+ margin: -5.5px 0 0 -5.5px; padding: 0;
840
+ border-radius: 50%; border: 2px solid var(--fundo);
841
+ background: var(--fraco); cursor: pointer;
842
+ }
843
+ .trilho-chaves .pino:hover { background: var(--texto); }
844
+ .trilho-chaves .pino.ativo { background: var(--primo); transform: scale(1.25); }
845
+ .sub-regua { font-size: 10.5px; color: var(--fraco2); margin-bottom: 10px; }
846
+ .opcoes button.ativo { border-color: var(--primo); color: var(--primo); background: var(--primo-suave); }
847
+ .presets { display: grid; gap: 5px; margin-top: 9px; }
848
+ .presets button {
849
+ height: 30px; padding: 0 10px; text-align: left;
850
+ border: 1px solid var(--linha); border-radius: 8px;
851
+ background: var(--sup2); color: var(--fraco);
852
+ font: 500 12px/1 var(--sans); cursor: pointer;
853
+ }
854
+ .presets button:hover { background: var(--sup3); color: var(--texto); border-color: var(--primo); }
@@ -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
  }
@@ -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"];
@@ -70,13 +70,23 @@ function aplicarFundo(no, fundo) {
70
70
  /* --------------------------------------------------------------- gráficos */
71
71
 
72
72
  function svgGrafico(e, l, a) {
73
- var dados = (e.dados || []).map(function (d) {
73
+ /* Os dados podem chegar como "dados" ou como "itens", e o tipo como
74
+ "grafico", "estilo" ou "tipo".
75
+
76
+ O nome certo é `dados` + `grafico`, e é o que a spec ensina. Mas quem
77
+ escreve um gráfico — a IA ou uma pessoa — chuta `itens`, porque é o
78
+ campo de TODOS os outros elementos da mesma spec (lista, cartoes,
79
+ numeros usam `itens`). Com o nome trocado esta função devolvia string
80
+ vazia: o gráfico simplesmente não existia na tela, sem erro nenhum.
81
+ Aceitar as duas grafias custa uma linha e evita a falha muda. */
82
+ var bruto = e.dados || e.itens || [];
83
+ var dados = bruto.map(function (d) {
74
84
  return Array.isArray(d) ? { r: d[0], v: +d[1] || 0 } : { r: d.rotulo, v: +d.valor || 0 };
75
85
  });
76
86
  if (!dados.length) return "";
77
87
  var cores = e.cores || ["var(--c-tom)", "var(--c-tom2)", "#7c9cff", "#2fbf7f", "#f472b6", "#facc15"];
78
88
  var max = Math.max.apply(null, dados.map(function (d) { return d.v; })) || 1;
79
- var tipo = e.grafico || "barras";
89
+ var tipo = e.grafico || e.estilo || e.tipoGrafico || "barras";
80
90
  var pad = { e: 8, d: 8, t: 14, b: 42 };
81
91
  var iw = l - pad.e - pad.d, ih = a - pad.t - pad.b;
82
92
  var p = ['<svg viewBox="0 0 ' + l + " " + a + '" width="' + l + '" height="' + a + '">'];
@@ -140,9 +150,79 @@ function svgGrafico(e, l, a) {
140
150
  var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
141
151
  citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1 };
142
152
 
153
+ /* ── chaves de animação ──────────────────────────────────────────────────
154
+ Transforma a lista de instantes de um elemento numa regra @keyframes de
155
+ verdade e prende ela no elemento.
156
+
157
+ Os nomes precisam ser únicos por página: o mesmo documento aparece várias
158
+ vezes ao mesmo tempo (palco, miniaturas do painel, clipes da tira), e
159
+ dois @keyframes de mesmo nome com conteúdo diferente se atropelariam —
160
+ a última regra injetada venceria em TODAS as cópias. */
161
+ var _nChave = 0;
162
+
163
+ function _numero(v, padrao) {
164
+ return typeof v === "number" && isFinite(v) ? v : padrao;
165
+ }
166
+
167
+ function aplicarChaves(no, e, ctx) {
168
+ var chaves = e.chaves.slice().sort(function (a, b) {
169
+ return _numero(a.t, 0) - _numero(b.t, 0);
170
+ });
171
+
172
+ var nome = "ch" + (++_nChave);
173
+ var passos = chaves.map(function (k) {
174
+ var pct = Math.max(0, Math.min(1, _numero(k.t, 0))) * 100;
175
+ var mov = [];
176
+ // x e y de uma chave são DESLOCAMENTO a partir de onde o elemento está.
177
+ // Assim a chave continua valendo se a pessoa arrastar o elemento no
178
+ // palco depois — o contrário obrigaria a refazer todas as chaves.
179
+ var dx = _numero(k.x, 0), dy = _numero(k.y, 0);
180
+ if (dx || dy) mov.push("translate(" + dx + "px," + dy + "px)");
181
+ var esc = _numero(k.escala, 1);
182
+ if (esc !== 1) mov.push("scale(" + esc + ")");
183
+ var giro = _numero(k.giro, 0);
184
+ if (giro) mov.push("rotate(" + giro + "deg)");
185
+
186
+ var linhas = ["transform:" + (mov.length ? mov.join(" ") : "none")];
187
+ var op = _numero(k.opacidade, 1);
188
+ linhas.push("opacity:" + op);
189
+ if (typeof k.desfoque === "number" && k.desfoque > 0) {
190
+ linhas.push("filter:blur(" + k.desfoque + "px)");
191
+ }
192
+ return pct.toFixed(3) + "%{" + linhas.join(";") + "}";
193
+ }).join("");
194
+
195
+ // A regra vai na RAIZ da cena, não dentro do elemento.
196
+ //
197
+ // Presa no próprio elemento ela era apagada segundos depois: quem desenha
198
+ // o conteúdo (texto, lista, número) escreve o innerHTML e leva junto tudo
199
+ // que estivesse ali. O elemento continuava com `animation-name: ch1`
200
+ // apontando para uma regra que não existia mais — a animação simplesmente
201
+ // não acontecia, sem erro nenhum no console. A cena só recebe filhos, e
202
+ // por isso a regra sobrevive lá.
203
+ var folha = document.createElement("style");
204
+ folha.textContent = "@keyframes " + nome + "{" + passos + "}";
205
+ (ctx.raiz || no).appendChild(folha);
206
+
207
+ var dur = _numero(ctx.duracao, 4);
208
+ no.style.animationName = nome;
209
+ no.style.animationDuration = dur + "s";
210
+ no.style.animationTimingFunction = e.suavizar === false ? "linear" : "cubic-bezier(.4,0,.2,1)";
211
+ no.style.animationFillMode = "both";
212
+ no.style.animationIterationCount = e.repetir ? "infinite" : "1";
213
+ }
214
+
143
215
  function renderElemento(e, ctx) {
144
216
  var no = document.createElement("div");
145
217
  var tipo = e.tipo || "texto";
218
+ /* Qual elemento do documento é este nó.
219
+
220
+ O editor precisava saber, e antes deduzia pela ordem dos nós na tela.
221
+ A dedução estava errada: os layouts de fluxo separam cabeçalho de corpo
222
+ e desenham o cabeçalho primeiro, então a ordem na tela NÃO é a ordem do
223
+ documento. O efeito era clicar num texto e o editor selecionar outro —
224
+ e mexer no elemento errado. O índice vem carimbado daqui. */
225
+ if (ctx && ctx.ordem && ctx.ordem.has(e)) no.dataset.i = ctx.ordem.get(e);
146
226
  /* Tipo inventado ("titulo", "subtitulo"...) caía num default sem estilo:
147
227
  texto de 16px num canvas de 1920 — invisível — ou div vazio quando o
148
228
  conteúdo estava noutro campo. O servidor novo recusa; documentos velhos
@@ -178,11 +258,27 @@ function renderElemento(e, ctx) {
178
258
 
179
259
  /* animação */
180
260
  var an = e.anim || ctx.animPadrao;
181
- if (an && an !== "nenhuma") {
261
+ if (an && an !== "nenhuma" && !(e.chaves && e.chaves.length > 1)) {
182
262
  no.setAttribute("data-an", typeof an === "string" ? an : (an.entrada || "subir"));
183
263
  no.style.setProperty("--atraso", (typeof an === "object" && an.atraso != null ? an.atraso : ctx.i * 0.09 + 0.1));
184
264
  }
185
265
 
266
+ /* ── chaves de animação ────────────────────────────────────────────────
267
+ Antes um elemento só sabia ENTRAR: escolhia-se um efeito de entrada e
268
+ acabava ali. Não dava para fazer nada percorrer a cena, crescer devagar
269
+ ou sumir no fim — que é o que separa animação de slide com efeito.
270
+
271
+ Uma chave é um instante: `{t: 0.5, x: 200, escala: 1.2}` diz onde o
272
+ elemento está na metade da cena. Duas ou mais viram uma animação CSS de
273
+ verdade, gerada aqui, com a duração da cena. O navegador interpola;
274
+ nós só descrevemos os pontos.
275
+
276
+ Quem tem chaves não recebe animação de entrada: as duas brigariam pelo
277
+ mesmo transform, e a chave é a mais específica das duas. */
278
+ if (ctx.animar && e.chaves && e.chaves.length > 1) {
279
+ aplicarChaves(no, e, ctx);
280
+ }
281
+
186
282
  switch (tipo) {
187
283
  case "texto": {
188
284
  var papel = PAPEIS[e.papel] || PAPEIS.corpo;
@@ -560,7 +656,21 @@ function renderCena(cena, doc, op) {
560
656
  // tela vertical pede tipografia maior: os padrões são calibrados em 1920x1080
561
657
  var escalaTipo = H > W ? 1.35 : 1;
562
658
  no.style.setProperty("--esc", escalaTipo);
563
- var ctx = { i: 0, escalaTipo: escalaTipo, animPadrao: op.animar ? (cena.anim || "subir") : null };
659
+ var ctx = {
660
+ i: 0, escalaTipo: escalaTipo,
661
+ animPadrao: op.animar ? (cena.anim || "subir") : null,
662
+ // as chaves precisam saber quanto dura a cena para virar segundos
663
+ animar: !!op.animar,
664
+ duracao: cena.duracao || 4,
665
+ raiz: no,
666
+ // objeto do elemento -> posição dele em cena.elementos, tirada ANTES de
667
+ // qualquer separação por layout
668
+ ordem: (function () {
669
+ var m = new Map();
670
+ (cena.elementos || []).forEach(function (e, i) { m.set(e, i); });
671
+ return m;
672
+ })(),
673
+ };
564
674
 
565
675
  if (layout === "livre") {
566
676
  fluxo.style.padding = "0";