primocode 10.3.0 → 10.4.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.
@@ -25,20 +25,25 @@
25
25
 
26
26
  const variedade = require('./variedade.js');
27
27
 
28
- const ESTILOS = ['hibrido', 'gerativo', 'motion', 'vox', 'marca', 'lancamento', 'impacto', 'cinema', 'composicao', 'fluido'];
28
+ const ESTILOS = ['hibrido', 'gerativo', 'motion', 'vox', 'marca', 'lancamento', 'impacto', 'cinema', 'composicao', 'fluido', 'personagem'];
29
29
  // Nomes que o modelo usa para os mesmos estilos.
30
30
  /* "motion" agora é o GERATIVO ("não pode ter template", 29–30/09); o motion
31
31
  antigo segue pelo nome "classico" (e "limpo", o nome mais antigo dele). */
32
32
  const APELIDOS = { motion: 'gerativo', generativo: 'gerativo', kinetic: 'gerativo', cinetico: 'gerativo', 'cinético': 'gerativo',
33
33
  tipografico: 'gerativo', 'tipográfico': 'gerativo', animacao: 'gerativo', 'animação': 'gerativo',
34
34
  classico: 'motion', 'clássico': 'motion', 'motion-classico': 'motion', motion3d: 'motion',
35
- absurdo: 'hibrido', misto: 'hibrido', mix: 'hibrido', 'híbrido': 'hibrido', limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', minimalista: 'fluido', minimal: 'fluido', clean: 'fluido', apple: 'fluido', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento' };
35
+ absurdo: 'hibrido', misto: 'hibrido', mix: 'hibrido', 'híbrido': 'hibrido', limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', minimalista: 'fluido', minimal: 'fluido', clean: 'fluido', apple: 'fluido', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento',
36
+ personagens: 'personagem', boneco: 'personagem', bonecos: 'personagem', conversa: 'personagem', dialogo: 'personagem', 'diálogo': 'personagem', podcast: 'personagem', gravado: 'personagem' };
36
37
 
37
38
  const PEDE_FLUIDO = /\bfluid[oa]\b|minimalista|\bminimal\b|\bclean\b|\bapple\b/i;
38
39
  const PEDE_MOTION = /\bmotion\b|anima[çc][ãa]o|animad[oa]/i;
39
40
  const PEDE_VOX = /\bvox\b|explica|explainer|document[aá]rio|editorial|hist[oó]ria d[aoe]s?\b/i;
40
41
  const PEDE_MARCA = /\bbrand|identidade visual|revela[çc][ãa]o d[ae] marca|\blogo(tipo|marca)?\b|vinheta de marca/i;
41
42
  const PEDE_LANCAMENTO = /lan[çc]amento|\blaunch|\bsaas\b|produto novo|apresenta(ndo|r|ção d[oe]) (o |a )?(app|produto|plataforma|ferramenta)/i;
43
+ /* "Uma animação em 3D do Claude falando com o usuário... como se fosse gravado"
44
+ (10/10): personagem, boneco, avatar, conversa ou podcast em 3D vira o vídeo
45
+ GRAVADO em 3D (personagem.js). O 3D de produto continua no gerativo. */
46
+ const PEDE_PERSONAGEM = /personage(m|ns)|\bbonec[oa]s?\b|\bavatar|mascote falando|\bpodcast\b|entrevista|(conversa|di[aá]logo|falando|explicando)[^.]{0,60}\b3\s?d\b|\b3\s?d\b[^.]{0,60}(conversa|di[aá]logo|falando|explicando|pessoa|gente)/i;
42
47
  const PEDE_IMPACTO = /an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i;
43
48
 
44
49
  const normal = (e) => {
@@ -73,7 +78,9 @@ function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = fa
73
78
  const doRoteiro = normal(roteiro);
74
79
  /* Pedir MOTION (ou animação) é pedir o gerativo — antes do vox, da marca e
75
80
  do lançamento, que são leiaute fixo ("não pode ter template", 30/09). */
76
- if (doRoteiro && !(doRoteiro !== 'gerativo' && doRoteiro !== 'fluido' && PEDE_MOTION.test(pedido))) return doRoteiro;
81
+ // O personagem também é animação: "animação 3D do Claude falando" não pode cair no gerativo.
82
+ if (doRoteiro && !(!['gerativo', 'fluido', 'personagem'].includes(doRoteiro) && PEDE_MOTION.test(pedido))) return doRoteiro;
83
+ if (PEDE_PERSONAGEM.test(pedido)) return 'personagem';
77
84
  // O fluido é um jeito de motion: "motion minimalista" é o fluido, não o gerativo.
78
85
  if (PEDE_FLUIDO.test(pedido)) return 'fluido';
79
86
  if (PEDE_MOTION.test(pedido)) return 'gerativo';
@@ -88,6 +95,6 @@ function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = fa
88
95
  }
89
96
 
90
97
  /** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
91
- const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido']);
98
+ const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido', 'personagem']);
92
99
 
93
- module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM };
100
+ module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM, PEDE_PERSONAGEM };
@@ -42,6 +42,7 @@
42
42
  const { misturarCor, ehCor } = require('./morfar.js');
43
43
  const { volumesDaTrilha } = require('./ritmo.js');
44
44
  const { falaDe } = require('./composicao.js');
45
+ const { limitesNaCamera, MARGEM } = require('./margem.js');
45
46
 
46
47
  const FPS = 30;
47
48
  const QUADRO = 1 / FPS;
@@ -1255,6 +1256,23 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1255
1256
  } else camera.push({ t: r3(b.ate), x: 0, y: 0, zoom: r3(z1 + 0.03), rot: 0, e: 'camera' });
1256
1257
  });
1257
1258
 
1259
+ /* ── a área segura de cada batida (margem.js) ──────────────────────
1260
+ O bloco parado não pode encostar na borda DA TELA — e a tela vê o
1261
+ mundo pela câmera: com zoom 1,1 e a batida 110 px mais perto da lente,
1262
+ a margem de 8,5% do mundo virava 3% na tela ("pensam" colado na borda,
1263
+ 10/10). Aqui o limite da tela vira limite do mundo, batida a batida; o
1264
+ componente encaixa o bloco MEDIDO dentro dele. O ícone vive mais perto
1265
+ da lente (translateZ 150), então tem o limite dele. */
1266
+ const D = Math.round(Math.max(W, H) * 0.85);
1267
+ batidas.forEach((b) => {
1268
+ const assenta = Math.min(b.de + 0.55, (b.de + b.ate) / 2);
1269
+ const fimCalmo = Math.max(assenta + 0.1, b.ate - 0.32);
1270
+ const op = { W, H, camera, de: assenta, ate: fimCalmo, D };
1271
+ b.seguro = limitesNaCamera({ ...op, z: 110 });
1272
+ b.seguroIcone = limitesNaCamera({ ...op, z: 260 });
1273
+ b.vao = Math.round(Math.min(W, H) * MARGEM.vao);
1274
+ });
1275
+
1258
1276
  // ── o produto em 3D: mora nas batidas que o hospedam ─────────────────
1259
1277
  const produtos = batidas.filter((b) => b.produto3d).map((b) => ({ ...b.produto3d, de: b.de, ate: b.ate, modelo: roteiro.modelo3d,
1260
1278
  cor: roteiro.cor3d || '#1b1c20', corAcento: P.acento, ...(roteiro.variacao3d || {}) }));
@@ -47,6 +47,7 @@ const assets = require('./assets.js');
47
47
  const video = require('./video.js');
48
48
  const batida = require('./batida.js');
49
49
  const revisor = require('./revisor.js');
50
+ const margem = require('./margem.js');
50
51
  const assinatura = require('./assinatura.js');
51
52
  const identidade = require('./identidade.js');
52
53
  const slides = require('./slides.js');
@@ -64,6 +65,7 @@ const vox = require('./vox.js');
64
65
  const kokoro = require('./kokoro.js');
65
66
  const lancamento = require('./lancamento.js');
66
67
  const glifos = require('./glifos.js');
68
+ const personagem = require('./personagem.js');
67
69
  const { escolherEstilo, LEVES } = require('./estilo.js');
68
70
  const sonsLeves = require('./sons-leves.js');
69
71
  const ende = require('./ende.js');
@@ -277,7 +279,12 @@ function roteiroValido(r) {
277
279
  if (!Array.isArray(partes) || partes.length < 2) return null;
278
280
  const CINEMA = ['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'];
279
281
  const NOVOS = ['logo', 'cores', 'produto']; // vox.js e lancamento.js
280
- if (!partes.every((pt) => pt && (composicao.TIPOS.includes(pt.tipo) || CINEMA.includes(pt.tipo) || NOVOS.includes(pt.tipo)))) return null;
282
+ /* A FALA do personagem (personagem.js) não tem tipo: é {falante, texto}. Sem
283
+ isto, o roteiro de diálogo caía no roteirista do servidor, que reescrevia
284
+ tudo com um apresentador só — e as duas vozes sumiam (10/10). */
285
+ const conversa = (x && !Array.isArray(x) && String(x.estilo || '').toLowerCase() === 'personagem') || partes.some((pt) => pt && typeof pt.falante === 'string' && pt.falante.trim());
286
+ const fala = (pt) => conversa && !pt.tipo && typeof (pt.texto || pt.fala) === 'string' && String(pt.texto || pt.fala).trim();
287
+ if (!partes.every((pt) => pt && (composicao.TIPOS.includes(pt.tipo) || CINEMA.includes(pt.tipo) || NOVOS.includes(pt.tipo) || fala(pt)))) return null;
281
288
  // Duas vozes: roteiro.vozes e parte.falante, limpos (falantes.js). Sem eles, nada muda.
282
289
  const obj = falantes.normalizar(Array.isArray(x) ? { partes } : x);
283
290
  return partes.some((pt) => CINEMA.includes(pt.tipo)) && !obj.estilo ? { ...obj, estilo: 'cinema' } : obj;
@@ -470,6 +477,17 @@ async function ferramentaMidia(args) {
470
477
  return r;
471
478
  }
472
479
 
480
+ /* O ELEMENTO do site, tirado pelo código (print-site.js): a lista das peças
481
+ (logo, botão, cartão, título…) e uma peça sozinha, com o fundo transparente. */
482
+ if (acao === 'elementos') return printSite.elementosDoSite(args.url || args.site || args.termo, { celular: !!args.celular });
483
+ if (acao === 'elemento') {
484
+ if (!p) return { ok: false, error: 'para tirar o elemento é preciso dizer em qual projeto (id).' };
485
+ const r = await printSite.capturarElemento(args.url || args.site, { pasta: p.pasta, seletor: args.seletor, texto: args.texto || args.termo,
486
+ celular: !!args.celular, transparente: args.transparente !== false, margem: args.margem });
487
+ if (r.ok) projeto.anotar(p.id, 'midia', `elemento de ${r.url} (${args.seletor || args.texto || args.termo}): ${r.src}`);
488
+ return r;
489
+ }
490
+
473
491
  // O catálogo de efeitos que a IA escreveu (efeitos.js): guardar, listar, reusar.
474
492
  if (acao === 'efeitos') return { ok: true, efeitos: efeitos.cardapio() };
475
493
  if (acao === 'guardar-efeito' || acao === 'usar-efeito') {
@@ -1113,7 +1131,7 @@ async function prepararRoteiro(p, args) {
1113
1131
  /* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
1114
1132
  com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
1115
1133
  o agente tenha escolhido outro estilo pelo nome. */
1116
- if (/\b3\s?d\b|tridimensional/i.test(pedidoTxt) && (!args.estilo || estilo === 'gerativo' || estilo === 'lancamento')) {
1134
+ if (/\b3\s?d\b|tridimensional/i.test(pedidoTxt) && estilo !== 'personagem' && (!args.estilo || estilo === 'gerativo' || estilo === 'lancamento')) {
1117
1135
  // O 3D mora numa batida do gerativo; o lançamento antigo, só pelo nome.
1118
1136
  if (!args.estilo && estilo !== 'lancamento') estilo = 'gerativo';
1119
1137
  if (!roteiro.modelo3d) roteiro.modelo3d = variacao3d.modeloDoPedido(pedidoTxt);
@@ -1485,6 +1503,9 @@ async function prepararRoteiro(p, args) {
1485
1503
  // O fluido é o vídeo de produto: voz limpa e segura, e rápida ("mais rápido", 06/10).
1486
1504
  : estilo === 'fluido' ? 'Leia em português do Brasil como narrador de vídeo de produto premium: seguro, próximo e RÁPIDO, '
1487
1505
  + 'frases limpas e contínuas, quase sem pausa entre elas. Nunca arraste, nunca leia devagar; sem tom de anúncio.'
1506
+ // O personagem CONVERSA: voz de gente falando com gente, não de locutor.
1507
+ : estilo === 'personagem' ? 'Fale em português do Brasil como alguém conversando de verdade num podcast: natural, próximo, '
1508
+ + 'com as pausas e a entonação de quem está pensando junto. Sem voz de locutor, sem leitura.'
1488
1509
  : estilo === 'impacto' ? 'Leia em português do Brasil como locutor de comercial agressivo e vendedor: energia alta, ritmo rápido, '
1489
1510
  + 'voz encorpada, ênfase forte nas palavras de impacto, pausas curtas e secas entre as frases, sorrindo na voz.'
1490
1511
  // "A narração tá lenta, deixa mais viva" (29/09): todo estilo fala com energia de reels.
@@ -1508,7 +1529,7 @@ async function prepararRoteiro(p, args) {
1508
1529
  áudio acelera 1,12× (1,3× ficou corrido demais, 29/09; atempo não muda o tom) e cada tempo de palavra
1509
1530
  acompanha; `roteiro.ritmoVoz` ajusta (1 desliga). */
1510
1531
  // O fluido corre mais ("mais rápido", 06/10): 1,22× por padrão.
1511
- const ritmo = Math.max(1, Math.min(1.5, Number(roteiro.ritmoVoz || args.ritmoVoz) || (estilo === 'fluido' ? 1.22 : 1.12)));
1532
+ const ritmo = Math.max(1, Math.min(1.5, Number(roteiro.ritmoVoz || args.ritmoVoz) || (estilo === 'fluido' ? 1.22 : estilo === 'personagem' ? 1.06 : 1.12)));
1512
1533
  if (ritmo > 1.01) {
1513
1534
  const rap = mp3.replace(/\.mp3$/, '-r.mp3');
1514
1535
  const c = await video.rodar(video.ffmpeg(), ['-y', '-i', mp3, '-filter:a', `atempo=${ritmo.toFixed(3)}`, rap], { timeout: 120000 });
@@ -1697,6 +1718,11 @@ async function prepararRoteiro(p, args) {
1697
1718
  if (r.ok) conferirAssinatura(p, r, roteiro.assinatura || 'fluido');
1698
1719
  return r;
1699
1720
  }
1721
+ if (estilo === 'personagem' && modo === 'video') {
1722
+ const r = personagem.montarPersonagem({ roteiro, largura, altura, tempos, narracao, sons, trilha });
1723
+ if (r.ok) projeto.anotar(p.id, 'plano', `estilo personagem: ${r.resumo.elenco.join(' e ')} no set ${r.resumo.cenario}, ${r.resumo.planos} planos, ${r.resumo.segundos}s`);
1724
+ return r;
1725
+ }
1700
1726
  if (estilo === 'motion' && modo === 'video') {
1701
1727
  const r = motion.montarMotion({ roteiro, largura, altura, tempos, narracao, sons, trilha, imagens });
1702
1728
  if (r.ok) projeto.anotar(p.id, 'plano', `estilo motion: ${r.resumo.segundos}s contínuos, ${r.resumo.partes} partes, ${r.resumo.fps} fps`);
@@ -1788,6 +1814,12 @@ async function ferramentaRender(args, ctx = {}) {
1788
1814
  if (revisao.consertos.length) projeto.anotar(args.id, 'revisao', revisao.consertos.join('; '));
1789
1815
  if (revisao.avisos.length) projeto.anotar(args.id, 'revisao', 'avisos: ' + revisao.avisos.join('; '));
1790
1816
 
1817
+ /* A ÁREA SEGURA E O RESPIRO (margem.js): nada a menos de 6%/7% da borda,
1818
+ nada colado em nada. É conta, não pedido: o elemento que o modelo pôs
1819
+ encostado entra, o texto largo encolhe, o colado se afasta. */
1820
+ const guarda = margem.guardarPlano(pl);
1821
+ if (guarda.consertos.length) projeto.anotar(args.id, 'margem', guarda.consertos.slice(0, 12).join('; '));
1822
+
1791
1823
  // Os ícones vêm ANTES do render: um render que busca ícone na internet
1792
1824
  // falha no meio, depois de vinte minutos.
1793
1825
  const { mapa, faltaram } = await assets.iconesDoPlano(pl, p.pasta);
@@ -147,6 +147,38 @@ function useMedidas(P) {
147
147
  return medidas;
148
148
  }
149
149
 
150
+ /* ── a área segura: o bloco MEDIDO cabe em b.seguro ──────────────────────
151
+ b.seguro vem do planner (margem.limitesNaCamera): é onde, no mundo, o
152
+ bloco pode ficar para não encostar na borda da tela com a câmera daquela
153
+ batida. Largo demais: encolhe tudo junto (fonte e posição) em volta do
154
+ centro; fora: anda para dentro. A barra do acento mora à esquerda do bloco
155
+ e o ponto final à direita: contam como parte dele. Partitura antiga (sem
156
+ b.seguro): como antes. */
157
+ function encaixar(b, caixas) {
158
+ const seg = b.seguro;
159
+ const cs = Object.values(caixas);
160
+ if (!seg || !cs.length) return;
161
+ const sMax = Math.max(...cs.map((c) => c.s));
162
+ const temBarra = (b.acento || []).some((a) => a.forma === 'barra');
163
+ const x0 = Math.min(...cs.map((c) => c.x)), y0 = Math.min(...cs.map((c) => c.y));
164
+ const x1 = Math.max(...cs.map((c) => c.x + c.w)), y1 = Math.max(...cs.map((c) => c.y + c.h));
165
+ const deriva = b.vida === 'deriva' ? (x1 - x0) * 0.03 : 0;
166
+ const esq = (temBarra ? Math.max(18, (b.vao || 0) * 0.7, (y1 - y0) * 0.08) + 8 : 0) + sMax * 0.04 + deriva, dir = sMax * 0.22 + deriva;
167
+ const k = Math.min(1, (seg.x1 - seg.x0) / (x1 - x0 + esq + dir), (seg.y1 - seg.y0) / (y1 - y0));
168
+ const ox = (x0 + x1) / 2, oy = (y0 + y1) / 2;
169
+ const nx0 = ox + (x0 - ox) * k - esq * k, nx1 = ox + (x1 - ox) * k + dir * k;
170
+ const ny0 = oy + (y0 - oy) * k, ny1 = oy + (y1 - oy) * k;
171
+ const dx = nx0 < seg.x0 ? seg.x0 - nx0 : nx1 > seg.x1 ? seg.x1 - nx1 : 0;
172
+ const dy = ny0 < seg.y0 ? seg.y0 - ny0 : ny1 > seg.y1 ? seg.y1 - ny1 : 0;
173
+ if (k > 0.999 && !dx && !dy) return;
174
+ const X = (x) => ox + (x - ox) * k + dx, Y = (y) => oy + (y - oy) * k + dy;
175
+ for (const c of cs) {
176
+ c.x = X(c.x); c.y = Y(c.y); c.w *= k; c.h *= k; c.s *= k;
177
+ c.palavras = c.palavras.map((p) => ({ x: X(p.x), w: p.w * k }));
178
+ c.letras = c.letras.map((q) => ({ x: X(q.x), w: q.w * k }));
179
+ }
180
+ }
181
+
150
182
  // ── o leiaute: da medida às caixas no mundo ─────────────────────────────
151
183
  function leiaute(P, med) {
152
184
  const W = P.largura, H = P.altura;
@@ -210,6 +242,7 @@ function leiaute(P, med) {
210
242
  const s = aspas.tamanho;
211
243
  caixas[aspas.id] = { x: cp.x - s * 0.04, y: cp.y - s * 0.62, w: s * 0.5, h: s, s, palavras: [{ x: cp.x, w: s * 0.5 }], letras: [{ x: cp.x, w: s * 0.5 }] };
212
244
  }
245
+ encaixar(b, caixas);
213
246
  // O bloco inteiro (para a barra e a moldura).
214
247
  const cs = Object.values(caixas);
215
248
  const bx = { x: Math.min(...cs.map((c) => c.x)), y: Math.min(...cs.map((c) => c.y)), x1: Math.max(...cs.map((c) => c.x + c.w)), y1: Math.max(...cs.map((c) => c.y + c.h)) };
@@ -260,8 +293,16 @@ function geometria(a, b, L, P) {
260
293
  return { pts, larg: Math.max(4 * base, cx.s * 0.03), alfa: 1, frente: true, cx: ccx, cy: ccy };
261
294
  }
262
295
  if (f === 'barra') {
263
- const x = lay.bloco.x - Math.max(18 * base, (lay.bloco.y1 - lay.bloco.y) * 0.08);
264
- for (let i = 0; i < N; i++) { const k = i / (N - 1); pts.push([x, lerp(lay.bloco.y + 4, lay.bloco.y1 - 4, k)]); }
296
+ // O vão da barra é o respiro do vídeo (margem.js): colada, ela riscava a primeira letra.
297
+ // A deriva da vida abre as letras até 3% da linha para fora: a barra recua junto.
298
+ const x0 = lay.bloco.x - Math.max(18 * base, (b.vao || 0) * 0.7, (lay.bloco.y1 - lay.bloco.y) * 0.08) - (b.vida === 'deriva' ? (lay.bloco.x1 - lay.bloco.x) * 0.03 : 0);
299
+ /* O texto mora na batida, que chega ~80 px mais perto da lente enquanto se
300
+ lê; o traço, não. Sem esta conta, o texto crescia por cima da barra (a
301
+ primeira letra riscada, 10/10): a barra vai para onde o texto É VISTO. */
302
+ const D = Math.round(Math.max(P.largura, P.altura) * 0.85), kz = D / (D - 80);
303
+ const X = (q) => P.largura / 2 + (q - P.largura / 2) * kz, Y = (q) => P.altura / 2 + (q - P.altura / 2) * kz;
304
+ const x = X(x0);
305
+ for (let i = 0; i < N; i++) { const k = i / (N - 1); pts.push([x, Y(lerp(lay.bloco.y + 4, lay.bloco.y1 - 4, k))]); }
265
306
  return { pts, larg: Math.max(8 * base, 10 * base), alfa: 1, frente: true, cx: x, cy: (lay.bloco.y + lay.bloco.y1) / 2 };
266
307
  }
267
308
  if (f === 'moldura') {
@@ -959,9 +1000,11 @@ export function Gerativo({ el, t, fps }) {
959
1000
  if (x.heroi.modo === 'campo') return null; // desenhado atrás, antes do produto
960
1001
  const R = Math.min(W, H) * (W >= H ? 0.2 : 0.17);
961
1002
  let slot = null;
962
- if (W - M - bl.x1 >= R * 2.3) slot = { cx: bl.x1 + (W - M - bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R };
963
- else if (bl.y - M * 1.4 >= R * 2.2) slot = { cx: W / 2, cy: M * 1.4 + (bl.y - M * 1.4) / 2, R };
964
- else if (H - M * 1.4 - bl.y1 >= R * 2.2) slot = { cx: W / 2, cy: bl.y1 + (H - M * 1.4 - bl.y1) / 2, R };
1003
+ // A área segura (margem.js), com o vão do bloco; partitura antiga: a margem do mundo.
1004
+ const S = x.seguro || { x0: M, y0: M * 1.4, x1: W - M, y1: H - M * 1.4 }, V = x.vao || 0;
1005
+ if (S.x1 - bl.x1 - V >= R * 2.3) slot = { cx: bl.x1 + V + (S.x1 - bl.x1 - V) / 2, cy: (bl.y + bl.y1) / 2, R };
1006
+ else if (bl.y - V - S.y0 >= R * 2.2) slot = { cx: W / 2, cy: S.y0 + (bl.y - V - S.y0) / 2, R };
1007
+ else if (S.y1 - bl.y1 - V >= R * 2.2) slot = { cx: W / 2, cy: bl.y1 + V + (S.y1 - bl.y1 - V) / 2, R };
965
1008
  // Sem vazio, não entra (09/10): o anel no meio do texto era enfeite que ninguém pediu.
966
1009
  else return null;
967
1010
  return <Heroi b={x} t={t} P={P} slot={slot} />;
@@ -972,16 +1015,21 @@ export function Gerativo({ el, t, fps }) {
972
1015
  (ic.lados, gerativo.js: direita, esquerda, acima, abaixo ou canto);
973
1016
  sem espaço lá, tenta o próximo; sem nenhum, não entra — nunca
974
1017
  por cima do texto. Partitura antiga (sem lados): direita ou acima. */
975
- const ic = x.icone3d, tam = ic.tamanho, M = P.margem, bl = lay.bloco;
976
- const xTopo = x.bloco.alinha === 'centro' ? (bl.x + bl.x1) / 2 - tam / 2 : bl.x - tam * 0.08;
977
- const fora = (qx, qy) => qx + tam <= bl.x - 8 || qx >= bl.x1 + 8 || qy + tam <= bl.y - 8 || qy >= bl.y1 + 8;
1018
+ const ic = x.icone3d, bl = lay.bloco;
1019
+ /* A área segura do ícone (margem.js): ele vive mais perto da lente que o
1020
+ texto, então o limite dele é mais apertado; e fica a um vão (4% do lado
1021
+ curto) do bloco. Partitura antiga: a margem do mundo, como antes. */
1022
+ const M = P.margem, S = x.seguroIcone || { x0: M, y0: M, x1: W - M, y1: H - M }, V = x.vao || 8;
1023
+ const tam = Math.min(ic.tamanho, (S.x1 - S.x0) * 0.4, (S.y1 - S.y0) * 0.6);
1024
+ const xTopo = Math.min(S.x1 - tam, Math.max(S.x0, x.bloco.alinha === 'centro' ? (bl.x + bl.x1) / 2 - tam / 2 : bl.x - tam * 0.08));
1025
+ const fora = (qx, qy) => qx + tam <= bl.x - V || qx >= bl.x1 + V || qy + tam <= bl.y - V || qy >= bl.y1 + V;
978
1026
  const LUGAR = {
979
- 'direita': () => (W - M - bl.x1 >= tam * 0.9 ? [Math.min(W - M - tam, bl.x1 + (W - M - bl.x1 - tam) / 2), (bl.y + bl.y1) / 2 - tam / 2] : null),
980
- 'esquerda': () => (bl.x - M >= tam * 0.9 ? [Math.max(M * 0.6, M + (bl.x - M - tam) / 2), (bl.y + bl.y1) / 2 - tam / 2] : null),
981
- 'acima': () => (bl.y - M * 1.2 >= tam * 0.85 ? [xTopo, Math.max(M, bl.y - tam - 0.02 * H)] : null),
982
- 'abaixo': () => (H - M * 1.2 - bl.y1 >= tam * 0.85 ? [xTopo, Math.min(H - M - tam, bl.y1 + 0.02 * H)] : null),
1027
+ 'direita': () => (S.x1 - bl.x1 - V >= tam ? [Math.min(S.x1 - tam, bl.x1 + V + (S.x1 - bl.x1 - V - tam) / 2), Math.min(S.y1 - tam, Math.max(S.y0, (bl.y + bl.y1) / 2 - tam / 2))] : null),
1028
+ 'esquerda': () => (bl.x - V - S.x0 >= tam ? [Math.max(S.x0, S.x0 + (bl.x - V - S.x0 - tam) / 2), Math.min(S.y1 - tam, Math.max(S.y0, (bl.y + bl.y1) / 2 - tam / 2))] : null),
1029
+ 'acima': () => (bl.y - V - S.y0 >= tam ? [xTopo, Math.max(S.y0, bl.y - V - tam)] : null),
1030
+ 'abaixo': () => (S.y1 - bl.y1 - V >= tam ? [xTopo, Math.min(S.y1 - tam, bl.y1 + V)] : null),
983
1031
  // O canto livre mais longe do texto: o ícone pequeno, de assinatura.
984
- 'canto': () => [[W - M - tam, M], [W - M - tam, H - M - tam], [M, M], [M, H - M - tam]].find(([qx, qy]) => fora(qx, qy)) || null,
1032
+ 'canto': () => [[S.x1 - tam, S.y0], [S.x1 - tam, S.y1 - tam], [S.x0, S.y0], [S.x0, S.y1 - tam]].find(([qx, qy]) => fora(qx, qy)) || null,
985
1033
  };
986
1034
  const ordem = ic.lados || [ic.lado, 'acima'];
987
1035
  const achou = ordem.map((l) => LUGAR[l] && LUGAR[l]()).find(Boolean);
@@ -995,7 +1043,7 @@ export function Gerativo({ el, t, fps }) {
995
1043
  transform: 'translateZ(120px) scale(' + (0.6 + 0.4 * k).toFixed(4) + ') rotate(' + ((1 - k) * -8).toFixed(2) + 'deg)', transformOrigin: '50% 60%' }}
996
1044
  dangerouslySetInnerHTML={{ __html: ic.svg }} />;
997
1045
  }
998
- return <div style={{ position: 'absolute', inset: 0, transform: 'translateZ(150px)' }}><Icone3D el={{ ...ic, x: Math.round(px), y: Math.round(py) }} t={t} /></div>;
1046
+ return <div style={{ position: 'absolute', inset: 0, transform: 'translateZ(150px)' }}><Icone3D el={{ ...ic, tamanho: tam, x: Math.round(px), y: Math.round(py) }} t={t} /></div>;
999
1047
  })()}
1000
1048
  {x.linhas.map((l, li) => {
1001
1049
  const cx = lay.caixas[l.id];
@@ -9,6 +9,7 @@ import { Papel } from './base';
9
9
  import { TRES } from './tres';
10
10
  import { GERATIVO } from './gerativo';
11
11
  import { FLUIDO } from './fluido';
12
+ import { PERSONAGEM } from './personagem';
12
13
 
13
- export const KIT = { ...VOX, ...MARCA, ...TRES, ...GERATIVO, ...FLUIDO, papel: Papel };
14
+ export const KIT = { ...VOX, ...MARCA, ...TRES, ...GERATIVO, ...FLUIDO, ...PERSONAGEM, papel: Papel };
14
15
  export { Filtros, Acabamento, Hud, CURVAS_KIT, bolhaCaminho } from './base';