primocode 8.35.0 → 8.37.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.
@@ -4,6 +4,60 @@
4
4
  Dois modos: layout nomeado (o estúdio posiciona) ou "livre" (x/y/l/a).
5
5
  ========================================================================== */
6
6
 
7
+ /* ── A QUEBRA DA LEGENDA ─────────────────────────────────────────────────
8
+ * Legenda de vídeo tem regra de ofício, e ela não é opinião: linha curta o
9
+ * bastante para o olho pegar de relance (~38 caracteres em 1080p), no máximo
10
+ * três linhas, e a quebra caindo em espaço — nunca no meio da palavra.
11
+ *
12
+ * E as linhas saem EQUILIBRADAS. Encher a primeira até o talo e deixar duas
13
+ * palavras na segunda é o que faz a legenda parecer torta mesmo estando
14
+ * centralizada — foi essa a reclamação.
15
+ */
16
+ var LEGENDA_LARGURA = 38;
17
+
18
+ /** Quebra gulosa numa largura dada: enche a linha até não caber mais. */
19
+ function envolver(palavras, largura) {
20
+ var linhas = [];
21
+ var atual = "";
22
+ for (var i = 0; i < palavras.length; i++) {
23
+ var tenta = atual ? atual + " " + palavras[i] : palavras[i];
24
+ if (atual && tenta.length > largura) { linhas.push(atual); atual = palavras[i]; }
25
+ else atual = tenta;
26
+ }
27
+ if (atual) linhas.push(atual);
28
+ return linhas;
29
+ }
30
+
31
+ function quebrarLegenda(texto) {
32
+ var t = String(texto == null ? "" : texto).replace(/\s+/g, " ").trim();
33
+ if (!t) return "";
34
+ if (t.length <= LEGENDA_LARGURA) return t;
35
+
36
+ var palavras = t.split(" ");
37
+ var quantas = envolver(palavras, LEGENDA_LARGURA).length;
38
+
39
+ /* O EQUILÍBRIO.
40
+ *
41
+ * A quebra gulosa enche a primeira linha até o talo e larga duas palavras na
42
+ * última — é isso que faz a legenda parecer torta mesmo estando
43
+ * centralizada, e foi essa a reclamação.
44
+ *
45
+ * A primeira tentativa foi dividir o comprimento pelo número de linhas e
46
+ * cortar no alvo. Não serve: o resto se acumula na última, que estourava a
47
+ * largura (41 caracteres numa régua de 38).
48
+ *
49
+ * O que serve é apertar a régua: procurar a MENOR largura que ainda cabe no
50
+ * mesmo número de linhas. Aí nenhuma linha passa do teto — a régua só
51
+ * encolheu — e todas ficam parecidas, porque foi o aperto que as igualou.
52
+ */
53
+ var melhor = LEGENDA_LARGURA;
54
+ for (var w = LEGENDA_LARGURA - 1; w >= 12; w--) {
55
+ if (envolver(palavras, w).length > quantas) break;
56
+ melhor = w;
57
+ }
58
+ return envolver(palavras, melhor).join("\n");
59
+ }
60
+
7
61
  var PAPEIS = {
8
62
  chapeu: { classe: "chapeu" },
9
63
  titulo: { tamanho: 84, peso: 660, altura: 1.06 },
@@ -61,6 +115,13 @@ function aplicarFundo(no, fundo) {
61
115
  // Era metade do "os vídeos ficaram todos iguais".
62
116
  if (/^(malha|brilho|grade|pontos|suave|vinheta|aurora|nevoa|ondas|raios|xadrez)$/.test(fundo)) {
63
117
  no.setAttribute("data-fundo", fundo);
118
+ } else if (fundo === "marca") {
119
+ /* A cor da casa, chapada e sem efeito nenhum. Num anúncio de marca é o
120
+ fundo mais usado de todos — e escrevê-lo como `"#21C25E"` obrigaria a
121
+ repetir o hexadecimal em cada cena, que é como uma delas acaba com a
122
+ cor de ontem depois de a marca mudar de tom. */
123
+ no.style.background = "var(--c-tom)";
124
+ no.setAttribute("data-fundo", "marca");
64
125
  } else no.style.background = fundo;
65
126
  return;
66
127
  }
@@ -160,11 +221,30 @@ function svgGrafico(e, l, a) {
160
221
 
161
222
  /* -------------------------------------------------------------- elementos */
162
223
 
224
+ /* O KIT DE ANÚNCIO — os quatro últimos da lista abaixo.
225
+ *
226
+ * Nasceram de um vídeo de referência mandado pelo dono — um comercial de
227
+ * fintech — com uma frase só: "ainda não tá bom, tá genérico. Quero nesse
228
+ * nível."
229
+ *
230
+ * Medindo a peça de hoje contra aquela, o que faltava não era tema nem cor:
231
+ * tema claro e `marca.cor` já existiam e resolvem o mundo da marca. O que não
232
+ * existia era o PRODUTO. Um anúncio de cartão mostra o cartão, mostra o app
233
+ * funcionando dentro do celular, mostra a linha de produtos e assina no fim.
234
+ * Com o vocabulário antigo, "o cartão" era um retângulo verde com a palavra
235
+ * "cartão" escrita dentro — e é exatamente isso que se lê como genérico.
236
+ *
237
+ * ── A LISTA ABAIXO É LIDA POR UM TESTE, e por isso não tem comentário dentro.
238
+ * O `vocabulario.test.js` recorta este objeto com uma expressão regular e
239
+ * pega as palavras: qualquer texto entre as chaves vira "tipo que o servidor
240
+ * precisa aceitar", e o teste passa a cobrar `fintech`, `genérico` e `dono`.
241
+ * Comentário sobre os tipos mora AQUI FORA. */
163
242
  var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
164
243
  citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1,
165
244
  objeto3d: 1, clipe: 1,
166
245
  selo: 1, progresso: 1, avatar: 1, dispositivo: 1, conversa: 1, passos: 1,
167
- opcoes: 1 };
246
+ opcoes: 1,
247
+ cartao: 1, app: 1, vitrine: 1, assinatura: 1 };
168
248
  /* Os sete últimos são o vocabulário do vídeo de referência: pílula de estado,
169
249
  trilha de progresso, foto de pessoa, celular/notebook com conteúdo dentro,
170
250
  balões de conversa, etapas com uma acesa, e pergunta com alternativas.
@@ -481,7 +561,15 @@ function renderElemento(e, ctx) {
481
561
  if (e.peso || papel.peso) no.style.fontWeight = e.peso || papel.peso;
482
562
  if (papel.altura) no.style.lineHeight = papel.altura;
483
563
  no.style.color = e.cor || papel.cor || "";
484
- if (e.alinhar) { no.style.textAlign = e.alinhar; no.style.alignItems = e.alinhar === "centro" ? "center" : (e.alinhar === "direita" ? "flex-end" : "flex-start"); }
564
+ /* LEGENDA DE FALA É SEMPRE CENTRALIZADA, e o `alinhar` do modelo não
565
+ manda nela. Legenda de vídeo alinhada à esquerda não existe em lugar
566
+ nenhum — ela é lida em movimento, e o olho volta para o meio da tela.
567
+ Bastava o modelo escrever `alinhar: "esquerda"` uma vez para a legenda
568
+ sair torta, e ele escrevia. */
569
+ if (e.alinhar && e.papel !== "fala") {
570
+ no.style.textAlign = e.alinhar;
571
+ no.style.alignItems = e.alinhar === "centro" ? "center" : (e.alinhar === "direita" ? "flex-end" : "flex-start");
572
+ }
485
573
  if (e.fonte === "mono") no.style.fontFamily = "var(--f-mono)";
486
574
  if (e.fonte === "display") no.style.fontFamily = "var(--f-display)";
487
575
  if (e.maiusculas) no.style.textTransform = "uppercase";
@@ -489,7 +577,26 @@ function renderElemento(e, ctx) {
489
577
  if (e.altura != null) no.style.lineHeight = e.altura;
490
578
  if (e.estilo) no.setAttribute("data-estilo", e.estilo);
491
579
 
492
- var corpoTexto = _rico(e.texto);
580
+ /* A LEGENDA SE QUEBRA SOZINHA.
581
+ "A IA não consegue formatar bem" — e não vai conseguir: ela escreve a
582
+ frase, não a diagramação. Quem sabe quantos caracteres cabem numa
583
+ linha de 1080p é este código, e é ele que quebra.
584
+
585
+ Sem isto a legenda saía como parágrafo: cinco linhas subindo pela
586
+ tela, tapando o rosto de quem fala. */
587
+ var corpoTexto = _rico(e.papel === "fala" ? quebrarLegenda(e.texto) : e.texto);
588
+ if (e.papel === "fala" && !e.tamanho) {
589
+ /* Legenda longa ENCOLHE em vez de crescer para cima. Três linhas com o
590
+ corpo de uma linha comem metade do quadro, e o rosto de quem fala
591
+ fica atrás delas.
592
+
593
+ Não se corta palavra: perder o que foi dito é pior que letra menor.
594
+ O piso de 62% é onde a letra ainda se lê num celular. */
595
+ var nLinhas = String(quebrarLegenda(e.texto)).split("\n").length;
596
+ var base = (PAPEIS.fala.tamanho || 52) * (ctx.escalaTipo || 1);
597
+ var fator = Math.max(0.62, 1 - (nLinhas - 1) * 0.10);
598
+ if (nLinhas > 1) no.style.fontSize = _px(Math.round(base * fator));
599
+ }
493
600
 
494
601
  // marcador e fita precisam envolver cada linha para o fundo acompanhar
495
602
  if (e.estilo === "marcador" || e.estilo === "fita") {
@@ -844,10 +951,232 @@ function renderElemento(e, ctx) {
844
951
  if (typeof e.de === "number") vid.dataset.de = e.de;
845
952
  if (typeof e.ate === "number") vid.dataset.ate = e.ate;
846
953
  no.style.borderRadius = _px(e.raio != null ? e.raio : 0);
954
+
955
+ /* ── SÓ A VOZ, COM A TELA MONTADA POR CIMA ────────────────────────
956
+ *
957
+ * "Não deixa opção de manter só a voz com uma tela de exemplos."
958
+ *
959
+ * Faltava mesmo, e não era teimosia do agente: o vocabulário tinha
960
+ * `mudo` (imagem sem som) e NÃO tinha o contrário. Sem isto, usar a
961
+ * gravação como narração — a voz da pessoa por cima de telas montadas
962
+ * pelo estúdio — era impossível de escrever, por mais que se pedisse.
963
+ *
964
+ * O <video> continua no lugar, carregando e tocando: é ele que leva o
965
+ * áudio para o grafo da trilha (e daí para o MP4) e é o `de`/`ate` dele
966
+ * que dá a duração da cena. O que muda é que ele não aparece — some da
967
+ * tela e some do quadro exportado, que é o `data-som` lido no
968
+ * `video.js`. Um <video> com `display:none` também continuaria tocando,
969
+ * mas alguns navegadores param de decodificar o que não está no
970
+ * layout, e sem quadro decodificado o player o trata como não-pronto. */
971
+ if (e.apenas === "som" || e.som === true) {
972
+ vid.dataset.som = "1";
973
+ no.style.position = "absolute";
974
+ no.style.left = "0"; no.style.top = "0";
975
+ no.style.width = "1px"; no.style.height = "1px";
976
+ no.style.opacity = "0";
977
+ no.style.pointerEvents = "none";
978
+ no.style.overflow = "hidden";
979
+ }
847
980
  no.appendChild(vid);
848
981
  break;
849
982
  }
850
983
 
984
+ /* ── O CARTÃO ────────────────────────────────────────────────────────
985
+ Um produto desenhado, não um retângulo com o nome dele escrito dentro.
986
+ O que faz o olho ler "cartão" são quatro detalhes pequenos e nenhum
987
+ deles é o formato: o CHIP, o símbolo de aproximação, a numeração
988
+ mascarada e o pé com nome e validade. Sem eles o desenho é uma caixa;
989
+ com eles, qualquer pessoa reconhece em um quadro de vídeo.
990
+
991
+ A cor sai da marca (`--c-tom`) quando ninguém diz outra, para o cartão
992
+ pertencer à peça em vez de ser mais um objeto colorido nela. */
993
+ case "cartao": {
994
+ var cc = e.cor || "var(--c-tom)";
995
+ no.style.setProperty("--cc", cc);
996
+ if (e.largura) no.style.width = _px(e.largura);
997
+ if (e.movimento) no.setAttribute("data-mov", e.movimento);
998
+ if (e.face === "verso") no.setAttribute("data-face", "verso");
999
+
1000
+ var corpo = document.createElement("div");
1001
+ corpo.className = "cartao-corpo";
1002
+
1003
+ var topo = document.createElement("div");
1004
+ topo.className = "cartao-topo";
1005
+ var marca = document.createElement("span");
1006
+ marca.className = "cartao-marca";
1007
+ marca.textContent = e.nome || "";
1008
+ var vari = document.createElement("span");
1009
+ vari.className = "cartao-variante";
1010
+ vari.textContent = e.variante || "";
1011
+ topo.appendChild(marca); topo.appendChild(vari);
1012
+
1013
+ var meio = document.createElement("div");
1014
+ meio.className = "cartao-meio";
1015
+ var chip = document.createElement("i");
1016
+ chip.className = "cartao-chip";
1017
+ // O símbolo de aproximação: três arcos. Em SVG porque em texto ele
1018
+ // depende de uma fonte que a máquina pode não ter.
1019
+ var onda = document.createElement("span");
1020
+ onda.className = "cartao-onda";
1021
+ onda.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true">'
1022
+ + '<path d="M8 5a10 10 0 0 1 0 14M13 8a5.6 5.6 0 0 1 0 8M17.6 2.4a15 15 0 0 1 0 19.2"'
1023
+ + ' fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
1024
+ meio.appendChild(chip); meio.appendChild(onda);
1025
+
1026
+ var num = document.createElement("div");
1027
+ num.className = "cartao-numero";
1028
+ // Os quatro últimos dígitos e mais nada: é o que aparece num cartão de
1029
+ // verdade em foto de campanha, e é o que não vira dado de ninguém.
1030
+ num.textContent = "•••• •••• •••• " + String(e.numero || "4821").slice(-4);
1031
+
1032
+ var pe = document.createElement("div");
1033
+ pe.className = "cartao-pe";
1034
+ var quem = document.createElement("span");
1035
+ quem.textContent = e.titular || "SEU NOME AQUI";
1036
+ var val = document.createElement("span");
1037
+ val.textContent = e.validade || "12/29";
1038
+ pe.appendChild(quem); pe.appendChild(val);
1039
+
1040
+ var brilho = document.createElement("i");
1041
+ brilho.className = "cartao-brilho";
1042
+
1043
+ corpo.appendChild(brilho);
1044
+ corpo.appendChild(topo);
1045
+ corpo.appendChild(meio);
1046
+ corpo.appendChild(num);
1047
+ corpo.appendChild(pe);
1048
+ no.appendChild(corpo);
1049
+ break;
1050
+ }
1051
+
1052
+ /* ── A TELA DO APP ───────────────────────────────────────────────────
1053
+ O `dispositivo` já desenhava o aparelho; o que ele mostrava DENTRO era
1054
+ texto centralizado no meio do vidro, que não se parece com aplicativo
1055
+ nenhum. O que faz uma tela parecer um app é a ARRUMAÇÃO: cabeçalho em
1056
+ cima, um número grande, linhas com o nome à esquerda e o valor à
1057
+ direita separadas por fio, e um botão cheio embaixo.
1058
+
1059
+ Vale sozinho na cena e vale dentro do `dispositivo` — é o mesmo
1060
+ elemento, e por isso a tela do celular e a do print da campanha não
1061
+ divergem. */
1062
+ case "app": {
1063
+ if (e.escuro) no.setAttribute("data-escuro", "1");
1064
+ if (e.topo) {
1065
+ var cab = document.createElement("div");
1066
+ cab.className = "app-topo";
1067
+ cab.innerHTML = "<span>" + _fugir(e.topo) + "</span><i></i>";
1068
+ no.appendChild(cab);
1069
+ }
1070
+ if (e.valor) {
1071
+ var caixaV = document.createElement("div");
1072
+ caixaV.className = "app-valor";
1073
+ if (e.rotulo) {
1074
+ var rot = document.createElement("small");
1075
+ rot.textContent = e.rotulo;
1076
+ caixaV.appendChild(rot);
1077
+ }
1078
+ var vv = document.createElement("strong");
1079
+ vv.textContent = e.valor;
1080
+ caixaV.appendChild(vv);
1081
+ no.appendChild(caixaV);
1082
+ }
1083
+ var linhas = e.linhas || e.itens || [];
1084
+ if (linhas.length) {
1085
+ var lista = document.createElement("div");
1086
+ lista.className = "app-linhas";
1087
+ linhas.slice(0, 8).forEach(function (li) {
1088
+ /* Aceita ["nome", "valor"] e "nome|valor" — o segundo é a forma que
1089
+ todo o resto da spec usa (lista, cartoes, passos), e quem escreve
1090
+ o documento vai chutar a que já conhece. */
1091
+ var nome = li, valor = "";
1092
+ if (Array.isArray(li)) { nome = li[0]; valor = li[1] || ""; }
1093
+ else if (typeof li === "string" && li.indexOf("|") > -1) {
1094
+ var par = li.split("|"); nome = par[0]; valor = par.slice(1).join("|");
1095
+ }
1096
+ var linha = document.createElement("div");
1097
+ linha.className = "app-linha";
1098
+ linha.innerHTML = "<span>" + _fugir(String(nome).trim()) + "</span>"
1099
+ + "<b>" + _fugir(String(valor).trim()) + "</b>";
1100
+ lista.appendChild(linha);
1101
+ });
1102
+ no.appendChild(lista);
1103
+ }
1104
+ if (e.botao) {
1105
+ var bt = document.createElement("div");
1106
+ bt.className = "app-botao";
1107
+ bt.textContent = e.botao;
1108
+ no.appendChild(bt);
1109
+ }
1110
+ break;
1111
+ }
1112
+
1113
+ /* ── A VITRINE ───────────────────────────────────────────────────────
1114
+ A linha de produtos, com um escolhido. O `cartoes` genérico resolvia
1115
+ isso com três caixinhas de vidro do tamanho de um selo; aqui os itens
1116
+ são CARTÕES, o atual vem à frente e os outros recuam — que é o desenho
1117
+ de toda tela de "escolha o seu". */
1118
+ case "vitrine": {
1119
+ var itens = (e.itens || []).slice(0, 5);
1120
+ var atual = e.atual != null ? Number(e.atual) : 0;
1121
+ if (e.titulo) {
1122
+ var tt = document.createElement("div");
1123
+ tt.className = "vitrine-titulo";
1124
+ tt.textContent = e.titulo;
1125
+ no.appendChild(tt);
1126
+ }
1127
+ var fila = document.createElement("div");
1128
+ fila.className = "vitrine-fila";
1129
+ itens.forEach(function (it, i) {
1130
+ var dado = typeof it === "string"
1131
+ ? { nome: it.split("|")[0], detalhe: it.split("|").slice(1).join("|") }
1132
+ : (it || {});
1133
+ var mini = renderElemento({
1134
+ tipo: "cartao", nome: e.nome || dado.marca || "",
1135
+ variante: dado.nome || dado.variante || "",
1136
+ cor: dado.cor || e.cor, numero: dado.numero,
1137
+ titular: " ", validade: " ",
1138
+ }, ctx);
1139
+ var col = document.createElement("div");
1140
+ col.className = "vitrine-item" + (i === atual ? " atual" : "");
1141
+ col.appendChild(mini);
1142
+ if (dado.detalhe) {
1143
+ var d = document.createElement("small");
1144
+ d.textContent = dado.detalhe;
1145
+ col.appendChild(d);
1146
+ }
1147
+ fila.appendChild(col);
1148
+ });
1149
+ no.appendChild(fila);
1150
+ break;
1151
+ }
1152
+
1153
+ /* ── A ASSINATURA ────────────────────────────────────────────────────
1154
+ O fim do anúncio. Toda peça de marca fecha assim — o logo sozinho,
1155
+ grande, sobre a cor da casa, e nada mais na tela. Sem este elemento a
1156
+ última cena do vídeo era mais uma frase, e a peça acabava sem assinar
1157
+ quem falou. */
1158
+ case "assinatura": {
1159
+ no.setAttribute("data-sobre", /^(marca|claro|escuro)$/.test(e.sobre || "") ? e.sobre : "marca");
1160
+ if (e.logo) {
1161
+ var img2 = document.createElement("img");
1162
+ img2.className = "assinatura-logo";
1163
+ img2.src = e.logo; img2.alt = e.nome || "";
1164
+ no.appendChild(img2);
1165
+ } else if (e.nome) {
1166
+ var nm = document.createElement("div");
1167
+ nm.className = "assinatura-nome";
1168
+ nm.textContent = e.nome;
1169
+ no.appendChild(nm);
1170
+ }
1171
+ if (e.frase) {
1172
+ var fr = document.createElement("div");
1173
+ fr.className = "assinatura-frase";
1174
+ fr.textContent = e.frase;
1175
+ no.appendChild(fr);
1176
+ }
1177
+ break;
1178
+ }
1179
+
851
1180
  case "icone": {
852
1181
  var t = e.tamanho || 90;
853
1182
  no.style.width = _px(e.l || t);
@@ -1081,6 +1410,11 @@ function renderCena(cena, doc, op) {
1081
1410
  elementos = elementos.filter(function (e) {
1082
1411
  if (!e || e.x != null || e.y != null) return true;
1083
1412
  if (e.tipo === "clipe") { deChao.push(e); return false; }
1413
+ /* A ASSINATURA É A CENA INTEIRA, e por isso entra pelo chão e não pelo
1414
+ fluxo. Pelo fluxo ela vira uma caixinha colorida no meio da tela, com a
1415
+ margem do layout em volta — que é o oposto de assinar: o logo tem de
1416
+ encostar nas bordas do quadro, como em qualquer fim de comercial. */
1417
+ if (e.tipo === "assinatura") { deChao.push(e); return false; }
1084
1418
  // Legenda de fala mora no terço de baixo, como toda legenda de vídeo.
1085
1419
  // Pelo fluxo ela iria para o meio da tela, em cima do rosto de quem fala.
1086
1420
  if (e.tipo === "texto" && e.papel === "fala") { falas.push(e); return false; }
@@ -1096,6 +1430,21 @@ function renderCena(cena, doc, op) {
1096
1430
  // tela vertical pede tipografia maior: os padrões são calibrados em 1920x1080
1097
1431
  var escalaTipo = H > W ? 1.35 : 1;
1098
1432
  no.style.setProperty("--esc", escalaTipo);
1433
+ // A altura do quadro, para o CSS dimensionar o que precisa ocupar a cena
1434
+ // (o aparelho, hoje) em proporção e não em pixels calibrados num formato só.
1435
+ no.style.setProperty("--alto", H + "px");
1436
+ /* A UNIDADE DA CENA: 1% da largura do quadro.
1437
+
1438
+ Sem ela, tudo o que o CSS escreve em `em` cai na fonte herdada — que numa
1439
+ cena é a do navegador, ~14px — e o desenho colapsa sem erro nenhum. Foi
1440
+ medido num quadro de 1080x1920: a linha do extrato saiu com 7,84px e o
1441
+ nome da assinatura com 30,8px, num quadro de 1920 de altura. Na tela
1442
+ aquilo não é "pequeno": é invisível.
1443
+
1444
+ O resto do sistema resolve isso mandando PIXEL do documento (`tamanho`).
1445
+ Peça que não tem `tamanho` — a tela do app, a vitrine, a assinatura — se
1446
+ mede por aqui, e assim acompanha o formato em vez de ser calibrada num só. */
1447
+ no.style.setProperty("--u", (W / 100) + "px");
1099
1448
  var ctx = {
1100
1449
  i: 0, escalaTipo: escalaTipo,
1101
1450
  animPadrao: op.animar ? (cena.anim || "subir") : null,
@@ -192,6 +192,10 @@ var VideoExport = (function () {
192
192
  // para desenhar levanta exceção em alguns navegadores e mancha o
193
193
  // canvas em outros.
194
194
  if (v.readyState < 2 || !v.videoWidth) return;
195
+ // Clipe de SÓ ÁUDIO: entra pelo som e não pelo quadro. Ele tem 1px na
196
+ // tela, então sem esta linha o export desenharia uma tira de um pixel
197
+ // esticada por cima da cena montada — visível, e sem causa aparente.
198
+ if (v.dataset && v.dataset.som === "1") return;
195
199
  var r = v.getBoundingClientRect();
196
200
  var dx = (r.left - caixaCena.left) * escX;
197
201
  var dy = (r.top - caixaCena.top) * escY;
@@ -511,8 +511,17 @@
511
511
  }
512
512
 
513
513
  .el.dispositivo[data-forma="celular"] { width: 100%; }
514
+ /* O TETO DO APARELHO ACOMPANHA O QUADRO. Ele era 780px fixos, calibrados em
515
+ 1920x1080 — e num vídeo vertical de 1080x1920 isso deixa o celular ocupando
516
+ um terço da altura, boiando no meio do branco. Num anúncio o aparelho é o
517
+ assunto da cena: ele ocupa a tela. `--alto` é a altura da cena, posta pelo
518
+ renderCena. */
514
519
  .el.dispositivo[data-forma="celular"] .moldura {
515
- height: min(100%, 780px); width: auto; max-width: 100%;
520
+ /* Altura em PIXEL vindo do quadro, sem porcentagem: `min(100%, …)` dentro de
521
+ um pai de altura automática é percentagem circular — o navegador desiste
522
+ dela e o aparelho volta a ser medido pela largura. Medido: 408px de altura
523
+ num quadro de 1920, um quinto da tela. */
524
+ height: calc(var(--alto, 1080px) * .62); width: auto; max-width: 100%;
516
525
  aspect-ratio: 9 / 19;
517
526
  border-radius: 42px; padding: 12px;
518
527
  }
@@ -761,8 +770,14 @@
761
770
  .cena.animar .el[data-an="cortina"] { animation-name: anCortina; animation-duration: .85s; }
762
771
  .cena.animar .el[data-an="elastico"] { animation-name: anElastico; animation-duration: .95s; }
763
772
 
764
- /* palavra a palavra — cada palavra entra sozinha */
765
- .el.txt .pal { display: inline-block; opacity: 0; }
773
+ /* palavra a palavra — cada palavra entra sozinha.
774
+
775
+ A palavra nasce VISÍVEL, e quem a esconde é a animação (com `both`, o
776
+ estado inicial do keyframe já vale antes de ela começar). Com `opacity: 0`
777
+ aqui na base, toda cena com `anim: "palavra"` saía EM BRANCO fora do player
778
+ — na miniatura, na prévia parada do editor, em qualquer render estático.
779
+ Texto que some sem erro nenhum é o defeito mais caro deste arquivo. */
780
+ .el.txt .pal { display: inline-block; }
766
781
  .cena.animar .el[data-an="palavra"] { animation: none; opacity: 1; }
767
782
  .cena.animar .el[data-an="palavra"] .pal {
768
783
  animation: anSubir .58s cubic-bezier(.22, 1, .36, 1) both;
@@ -933,3 +948,212 @@
933
948
  display: grid; place-items: center; min-height: 0; overflow: visible;
934
949
  }
935
950
  .cena .fluxo .lado-imagem.com-aparelho > .el { max-height: 100%; }
951
+
952
+ /* ══ O KIT DE ANÚNCIO ═══════════════════════════════════════════════════════
953
+ Nasceu de um vídeo de referência mandado pelo dono — um comercial de
954
+ fintech — com uma frase só: "ainda não tá bom, tá genérico. Quero nesse
955
+ nível." Medindo a peça de hoje contra aquela, o que faltava não era tema
956
+ nem cor (tema claro e `marca.cor` já existiam): era o PRODUTO.
957
+ ══════════════════════════════════════════════════════════════════════════ */
958
+
959
+ /* ── cartão ───────────────────────────────────────────────────────────────
960
+ A proporção é a de verdade (85,6 × 54 mm = 1,586). Ela importa mais que o
961
+ desenho: um retângulo na proporção errada não é lido como cartão por mais
962
+ chip que tenha. */
963
+ .el.cartao {
964
+ --cc: var(--c-tom);
965
+ width: min(100%, 620px);
966
+ perspective: 1400px;
967
+ }
968
+ .el.cartao .cartao-corpo {
969
+ position: relative;
970
+ aspect-ratio: 1.586;
971
+ border-radius: 6.2%;
972
+ padding: 7% 7.5%;
973
+ display: flex; flex-direction: column; justify-content: space-between;
974
+ color: #fff;
975
+ /* Duas fontes de luz e uma sombra longa: é o que separa "objeto pousado na
976
+ cena" de "retângulo colado por cima". */
977
+ background:
978
+ radial-gradient(120% 140% at 12% 4%, rgba(255, 255, 255, .30), transparent 52%),
979
+ linear-gradient(145deg,
980
+ color-mix(in srgb, var(--cc) 88%, #fff),
981
+ var(--cc) 46%,
982
+ color-mix(in srgb, var(--cc) 72%, #000) 100%);
983
+ box-shadow:
984
+ inset 0 1px 0 rgba(255, 255, 255, .38),
985
+ inset 0 -1px 0 rgba(0, 0, 0, .22),
986
+ 0 2.2% 5% -1% rgba(0, 0, 0, .45);
987
+ transform: rotate(-1.6deg);
988
+ }
989
+ /* O reflexo diagonal. Sem ele o plástico vira papel colorido. */
990
+ .el.cartao .cartao-brilho {
991
+ position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
992
+ background: linear-gradient(102deg, transparent 34%, rgba(255, 255, 255, .20) 46%,
993
+ rgba(255, 255, 255, .05) 54%, transparent 62%);
994
+ }
995
+ .el.cartao .cartao-topo {
996
+ display: flex; align-items: baseline; justify-content: space-between; gap: 1em;
997
+ }
998
+ .el.cartao .cartao-marca {
999
+ font-weight: 800; font-size: 6.4cqw; letter-spacing: -.01em;
1000
+ }
1001
+ .el.cartao .cartao-variante {
1002
+ font-size: 3.4cqw; letter-spacing: .18em; text-transform: uppercase;
1003
+ opacity: .82; font-weight: 600;
1004
+ }
1005
+ .el.cartao .cartao-meio { display: flex; align-items: center; gap: 4%; }
1006
+ /* O chip: dourado, com os contatos. É o detalhe que mais faz o desenho ser
1007
+ reconhecido — some ele e sobra um cartão de visita. */
1008
+ .el.cartao .cartao-chip {
1009
+ width: 13%; aspect-ratio: 1.32; border-radius: 14%;
1010
+ background: linear-gradient(150deg, #f7e3a1, #d9b558 46%, #b8912f);
1011
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .18);
1012
+ position: relative; flex: none;
1013
+ }
1014
+ .el.cartao .cartao-chip::before,
1015
+ .el.cartao .cartao-chip::after {
1016
+ content: ""; position: absolute; left: 12%; right: 12%; height: 1px;
1017
+ background: rgba(0, 0, 0, .28);
1018
+ }
1019
+ .el.cartao .cartao-chip::before { top: 36%; }
1020
+ .el.cartao .cartao-chip::after { top: 64%; }
1021
+ .el.cartao .cartao-onda { width: 7%; opacity: .9; display: block; flex: none; }
1022
+ .el.cartao .cartao-onda svg { width: 100%; height: auto; display: block; }
1023
+ .el.cartao .cartao-numero {
1024
+ font-family: var(--f-mono);
1025
+ font-size: 5.6cqw; letter-spacing: .06em; opacity: .95;
1026
+ }
1027
+ .el.cartao .cartao-pe {
1028
+ display: flex; justify-content: space-between; gap: 1em;
1029
+ font-size: 3.1cqw; letter-spacing: .13em; text-transform: uppercase;
1030
+ opacity: .8; font-weight: 600;
1031
+ }
1032
+ /* As medidas de dentro são `cqw` — por cento da LARGURA DO CARTÃO, não da
1033
+ tela. Sem o contexto de contêiner o mesmo cartão sai com o chip do tamanho
1034
+ de uma unha na vitrine e do tamanho de uma caixa na cena cheia. */
1035
+ .el.cartao .cartao-corpo { container-type: inline-size; }
1036
+
1037
+ .el.cartao[data-face="verso"] .cartao-corpo { transform: rotate(1.6deg); }
1038
+ .el.cartao[data-mov="flutuar"] .cartao-corpo { animation: cartao-flutua 6s ease-in-out infinite; }
1039
+ .el.cartao[data-mov="girar"] .cartao-corpo { animation: cartao-gira 9s ease-in-out infinite; }
1040
+ @keyframes cartao-flutua {
1041
+ 0%, 100% { transform: rotate(-1.6deg) translateY(0); }
1042
+ 50% { transform: rotate(-.6deg) translateY(-2.2%); }
1043
+ }
1044
+ @keyframes cartao-gira {
1045
+ 0%, 100% { transform: rotate(-1.6deg) rotateY(-16deg); }
1046
+ 50% { transform: rotate(-1.6deg) rotateY(16deg); }
1047
+ }
1048
+ @media (prefers-reduced-motion: reduce) {
1049
+ .el.cartao .cartao-corpo { animation: none; }
1050
+ }
1051
+
1052
+ /* ── a tela do app ────────────────────────────────────────────────────────
1053
+ Vale sozinha na cena e vale dentro do `dispositivo`. É o mesmo elemento de
1054
+ propósito: com dois, o print da campanha e a tela do celular divergiriam na
1055
+ primeira mudança. */
1056
+ .el.app {
1057
+ width: 100%;
1058
+ font-size: calc(var(--u, 10px) * 3.4);
1059
+ display: flex; flex-direction: column; gap: 4%;
1060
+ text-align: left;
1061
+ background: #fff; color: #10161d;
1062
+ border-radius: 22px; padding: 7% 6.5%;
1063
+ box-shadow: 0 18px 50px rgba(0, 0, 0, .16);
1064
+ }
1065
+ .el.app[data-escuro] { background: #0e1319; color: #eaf0f6; box-shadow: none; }
1066
+ .el.app .app-topo {
1067
+ display: flex; align-items: center; justify-content: space-between;
1068
+ font-size: .62em; font-weight: 700; opacity: .75;
1069
+ }
1070
+ .el.app .app-topo i {
1071
+ width: 2.2em; height: .34em; border-radius: 999px;
1072
+ background: currentColor; opacity: .25; display: block;
1073
+ }
1074
+ .el.app .app-valor { display: flex; flex-direction: column; gap: .12em; }
1075
+ .el.app .app-valor small { font-size: .5em; opacity: .6; font-weight: 600; }
1076
+ .el.app .app-valor strong {
1077
+ font-size: 1.5em; font-weight: 800; letter-spacing: -.02em;
1078
+ font-variant-numeric: tabular-nums;
1079
+ }
1080
+ .el.app .app-linhas { display: flex; flex-direction: column; }
1081
+ /* O FIO entre as linhas é o que faz o olho ler "extrato". Sem ele são frases
1082
+ empilhadas, que é como a tela saía antes. */
1083
+ .el.app .app-linha {
1084
+ display: flex; align-items: center; justify-content: space-between; gap: 1em;
1085
+ padding: .62em 0; font-size: .56em;
1086
+ border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
1087
+ }
1088
+ .el.app .app-linha:first-child { border-top: 0; }
1089
+ .el.app .app-linha span { opacity: .82; }
1090
+ .el.app .app-linha b { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
1091
+ .el.app .app-botao {
1092
+ margin-top: auto;
1093
+ background: var(--c-tom); color: #fff;
1094
+ border-radius: 999px; padding: .78em 1em;
1095
+ text-align: center; font-weight: 700; font-size: .58em;
1096
+ }
1097
+
1098
+ /* Dentro do aparelho a tela do app OCUPA o vidro: com o padrão do
1099
+ `dispositivo` (conteúdo centrado com folga) ela ficava boiando no meio, e o
1100
+ que se vê num celular de verdade é a tela cheia. */
1101
+ .el.dispositivo .tela { justify-content: flex-start; }
1102
+ .el.dispositivo .tela > .el.app {
1103
+ flex: 1; border-radius: 0; box-shadow: none;
1104
+ margin: -26px -20px; padding: 34px 22px 26px;
1105
+ background: transparent; color: var(--c-texto);
1106
+ }
1107
+
1108
+ /* ── vitrine ──────────────────────────────────────────────────────────────
1109
+ A linha de produtos com um escolhido. O item atual vem à frente; os outros
1110
+ recuam — sem isso são quatro cartões do mesmo tamanho e ninguém sabe qual
1111
+ está sendo oferecido. */
1112
+ .el.vitrine {
1113
+ width: 100%; font-size: calc(var(--u, 10px) * 3.4);
1114
+ display: flex; flex-direction: column; gap: 1.2em;
1115
+ }
1116
+ .el.vitrine .vitrine-titulo {
1117
+ text-align: center; font-size: .62em; font-weight: 700;
1118
+ letter-spacing: .04em; opacity: .82;
1119
+ }
1120
+ .el.vitrine .vitrine-fila {
1121
+ display: flex; align-items: center; justify-content: center; gap: 2.2%;
1122
+ }
1123
+ .el.vitrine .vitrine-item {
1124
+ flex: 1 1 0; min-width: 0;
1125
+ display: flex; flex-direction: column; align-items: center; gap: .5em;
1126
+ /* Recuado, não apagado: a .42 sobre fundo escuro os outros cartões somem, e
1127
+ uma vitrine com um produto só não é vitrine. */
1128
+ opacity: .58; transform: scale(.86);
1129
+ transition: opacity .4s ease, transform .4s ease;
1130
+ }
1131
+ .el.vitrine .vitrine-item.atual { opacity: 1; transform: scale(1.06); z-index: 2; }
1132
+ .el.vitrine .vitrine-item small {
1133
+ font-size: .42em; opacity: .75; text-align: center; font-weight: 600;
1134
+ }
1135
+ .el.vitrine .el.cartao { width: 100%; }
1136
+
1137
+ /* ── assinatura ───────────────────────────────────────────────────────────
1138
+ O fim do anúncio: o logo sozinho, grande, sobre a cor da casa. Ela toma a
1139
+ cena INTEIRA porque é isso que uma assinatura é — se dividisse espaço com
1140
+ outra coisa, seria mais um bloco em vez do ponto final. */
1141
+ .el.assinatura {
1142
+ position: absolute; inset: 0; z-index: 4;
1143
+ font-size: calc(var(--u, 10px) * 3);
1144
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
1145
+ gap: .5em; text-align: center; padding: 8%;
1146
+ }
1147
+ .el.assinatura[data-sobre="marca"] { background: var(--c-tom); color: #fff; }
1148
+ .el.assinatura[data-sobre="claro"] { background: #fff; color: #10161d; }
1149
+ .el.assinatura[data-sobre="escuro"] { background: #080b0e; color: #fff; }
1150
+ .el.assinatura .assinatura-logo { width: min(52%, 520px); height: auto; display: block; }
1151
+ .el.assinatura .assinatura-nome {
1152
+ font-size: 3.6em; font-weight: 800; letter-spacing: -.03em; line-height: 1;
1153
+ }
1154
+ .el.assinatura .assinatura-frase { font-size: .62em; opacity: .82; letter-spacing: .02em; }
1155
+
1156
+ /* Título de anúncio quebra EQUILIBRADO. "Um cartão. Seu jeito." saía em
1157
+ quatro linhas — três palavras e um órfão —, e é assim que uma frase curta
1158
+ vira um bloco desengonçado no meio do quadro. */
1159
+ .el.texto.titulo, .el.texto[data-papel="titulo"] { text-wrap: balance; }