primocode 10.1.0 → 10.3.0-beta.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.
@@ -23,6 +23,7 @@ import { CURVAS_KIT, alfa } from './base';
23
23
  const ed = CURVAS_KIT.editorial, cam = CURVAS_KIT.camera;
24
24
  const c01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
25
25
  const lerp = (a, b, k) => a + (b - a) * k;
26
+ const lum = (hex) => { const n = parseInt(String(hex).slice(1), 16); return (0.299 * (n >> 16 & 255) + 0.587 * (n >> 8 & 255) + 0.114 * (n & 255)) / 255; };
26
27
  const entra = (t) => t * t;
27
28
  const SANS = (f) => '"' + f + '", system-ui, sans-serif';
28
29
  const SERIF = (f) => '"' + f + '", Georgia, serif';
@@ -52,7 +53,7 @@ function medir(t, tam, peso, serifa, P, mono) {
52
53
 
53
54
  /* Um bloco de texto: quebra palavra a palavra (largura medida), cada linha
54
55
  centrada em cx (ou alinhada à esquerda/direita), o bloco centrado em cy. */
55
- function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', cor, mono = false }) {
56
+ function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', cor, corAcento = null, mono = false }) {
56
57
  const esp = medir(' ', tam, peso, false, P, mono);
57
58
  const ws = f.palavras.map((w) => ({ ...w, w: medir(w.t, w.acento ? tam * 1.14 : tam, peso, w.acento, P, mono) }));
58
59
  const linhas = [[]];
@@ -67,7 +68,7 @@ function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', co
67
68
  const top = cy - h / 2;
68
69
  const x0 = alinha === 'centro' ? cx - w / 2 : alinha === 'esq' ? cx : cx - w;
69
70
  return {
70
- f, tam, peso, cor, esp, lh, w, h, top, cx, cy, mono, left: x0,
71
+ f, tam, peso, cor, corAcento, esp, lh, w, h, top, cx, cy, mono, left: x0,
71
72
  linhas: linhas.map((l, i) => ({ palavras: l, y: top + i * lh, x: alinha === 'centro' ? cx - larg(l) / 2 : alinha === 'esq' ? cx : cx - larg(l) })),
72
73
  };
73
74
  }
@@ -108,6 +109,60 @@ function leiaute(P) {
108
109
  ent = [0.3, 0.8];
109
110
  }
110
111
  const poe = (bl) => { out.blocos.push(bl); return bl; };
112
+ /* A IMAGEM (fluido.js escolhe a batida e o jeito): o texto se arruma em
113
+ volta dela, e o portador vira um ponto discreto enquanto a foto manda. */
114
+ if (b.imagem && pr) {
115
+ const j = b.imagem.jeito;
116
+ const t0 = Math.max(b.de + 0.05, Math.min(...b.frases.map((f) => f.palavras[0].em)));
117
+ const [sb] = fr('sub');
118
+ const branco = '#ffffff';
119
+ let foto, tY;
120
+ if (j === 'cartao') {
121
+ const cw = deitado ? W * 0.36 : W * 0.74, ch = deitado ? H * 0.62 : H * 0.42;
122
+ foto = { x: deitado ? cx + W * 0.2 : cx, y: deitado ? cy : cy + H * 0.12, w: cw, h: ch };
123
+ tY = deitado ? cy : cy - H * 0.24;
124
+ } else if (j === 'faixa') {
125
+ foto = { x: cx, y: deitado ? cy - H * 0.08 : cy - H * 0.06, w: W, h: deitado ? H * 0.46 : H * 0.34 };
126
+ tY = foto.y + foto.h / 2 + 130 * u;
127
+ } else { foto = { x: cx, y: cy, w: W, h: H }; tY = j === 'vazado' ? cy : cy + H * (deitado ? 0.22 : 0.2); }
128
+ const tx2 = j === 'cartao' && deitado ? cx - W * 0.2 : cx;
129
+ const sobreFoto = j === 'janela' || j === 'circulo';
130
+ let tam = (j === 'vazado' ? 200 : sobreFoto ? 92 : 76) * u;
131
+ if (j === 'vazado') {
132
+ const l100 = pr.palavras.reduce((s2, w) => s2 + medir(w.t, 100, 700, false, P), 0) + medir(' ', 100, 700, false, P) * (pr.palavras.length - 1);
133
+ tam = Math.min(360 * u, (W * 0.84 / (1 + 0.022 * (bi + 1))) / Math.max(1, l100) * 100);
134
+ }
135
+ const larg2 = j === 'cartao' && deitado ? W * 0.34 : W * 0.84;
136
+ const tb = poe(bloco(P, pr, { tam, peso: j === 'vazado' ? 700 : 400, max: larg2, cx: tx2, cy: tY, cor: sobreFoto ? branco : T, corAcento: sobreFoto ? branco : null }));
137
+ if (sb) poe(bloco(P, sb, { tam: 34 * u, max: larg2, cx: tx2, cy: tY + tb.h / 2 + 56 * u, cor: sobreFoto ? alfa(branco, 0.82) : S }));
138
+ out.imagem = { jeito: j, src: b.imagem.src, foto, t0, texto: tb };
139
+ // O portador: no círculo ele É a foto (cresce); nos outros, vira um ponto e some.
140
+ if (j === 'circulo') vai(Math.max(0, t0 - 0.5), t0 + 0.2, forma(cx, cy, 0, 0, { solid: 1, ac: 1, op: 0 }));
141
+ else vai(ent[0], ent[1], forma(tx2, tY - tb.h / 2 - 60 * u, 10 * u, 10 * u, { solid: 1, ac: 1, op: j === 'vazado' ? 1 : 0 }));
142
+ return out;
143
+ }
144
+ /* A PALAVRA GIGANTE (fluido.js marca): a frase curta enche a largura,
145
+ sem pílula, peso fino. Na VIRADA o portador cresce até cobrir a tela
146
+ de acento, e o texto troca para a cor que lê sobre ele. */
147
+ if (b.gigante && pr) {
148
+ const base = 100;
149
+ const larguraA100 = pr.palavras.reduce((s2, w) => s2 + medir(w.t, w.acento ? base * 1.14 : base, 300, w.acento, P), 0) + medir(' ', base, 300, false, P) * (pr.palavras.length - 1);
150
+ // Cabe já descontando o zoom que a câmera terá nesta batida (1 + 0,022 por batida) e a deriva.
151
+ const zoomAqui = 1 + 0.022 * (bi + 1);
152
+ const tam = Math.min(330 * u, Math.max(96 * u, (W * 0.8 / zoomAqui) / Math.max(1, larguraA100) * base));
153
+ const sobre = b.virada ? (lum(pal.acentos[0]) > 0.55 ? pal.fundo[0] : '#ffffff') : T;
154
+ const acentoSobre = b.virada ? sobre : pal.acentos[0];
155
+ const g = poe(bloco(P, pr, { tam, peso: 300, max: W * 0.84 / zoomAqui, cx, cy, cor: sobre, corAcento: acentoSobre }));
156
+ const [sb] = fr('sub');
157
+ if (sb) poe(bloco(P, sb, { tam: 34 * u, max: W * 0.7, cx, cy: cy + g.h / 2 + 70 * u, cor: b.virada ? sobre : S }));
158
+ // A virada começa com a PRIMEIRA frase que aparece (a voz pode dizer o sub antes do título).
159
+ const t0 = Math.max(b.de + 0.1, Math.min(...b.frases.map((f) => f.palavras[0].em)));
160
+ if (b.virada) {
161
+ const D = Math.hypot(W, H) * 1.08;
162
+ vai(Math.max(0, t0 - 0.55), t0 - 0.05, forma(cx, cy, D, D, { solid: 1, ac: 1 }));
163
+ } else vai(ent[0], ent[1], forma(cx, cy - g.h / 2 - 70 * u, 14 * u, 14 * u, { solid: 1, ac: 1 }));
164
+ return out;
165
+ }
111
166
  switch (b.tipo) {
112
167
  case 'abertura': {
113
168
  const t = pr && poe(bloco(P, pr, { tam: (lado === 'centro' ? 96 : 80) * u, max: larg, cx: tx, cy: ty, cor: T }));
@@ -146,7 +201,9 @@ function leiaute(P) {
146
201
  out.extra = { pilulas, tt };
147
202
  // O portador vira a PRIMEIRA pílula; as outras nascem na ponta da curva.
148
203
  const p0 = pilulas[0];
149
- if (p0) vai(ent[0], ent[1], forma(p0.x, p0.y, p0.w, p0.h, { fill: 1, stroke: 0.35 }));
204
+ // A primeira pílula nasce com o primeiro item (antes, ficava vazia e enorme esperando a voz).
205
+ const i0 = p0 ? p0.f.palavras[0].em : b.de;
206
+ if (p0) vai(Math.max(ent[0], i0 - 0.4), Math.max(ent[1], i0 + 0.1), forma(p0.x, p0.y, p0.w, p0.h, { fill: 1, stroke: 0.35 }));
150
207
  else vai(ent[0], ent[1], forma(cx, cy, 12 * u, 12 * u, { solid: 1, ac: 1 }));
151
208
  break;
152
209
  }
@@ -220,8 +277,12 @@ function leiaute(P) {
220
277
  const h = umaLinha ? (t ? t.tam * 2 : 60 * u) : t.h;
221
278
  if (sb) poe(umaLinha ? bloco(P, sb, { tam: 34 * u, max: larg, cx: tx, cy: ty + h / 2 + 62 * u, cor: S })
222
279
  : bloco(P, sb, { tam: 34 * u, max: larg, cx: t.left, cy: ty + h / 2 + 50 * u, alinha: 'esq', cor: S }));
223
- if (umaLinha) vai(ent[0], ent[1], forma(tx, ty, (t ? t.w : 200 * u) + 110 * u, h, { fill: 1, stroke: 0.35 }));
224
- else vai(ent[0], ent[1], forma(t.left - 40 * u, ty, 6 * u, t.h * 0.92, { solid: 1, ac: 1, r: 3 }));
280
+ /* A pílula nasce com a primeira palavra, não antes: entrando em b.de,
281
+ ela ficava VAZIA até a voz chegar na frase (teste de 08/10). */
282
+ const p0 = pr ? pr.palavras[0].em : b.de;
283
+ const entT = [Math.max(ent[0], p0 - 0.4), Math.max(ent[1], p0 + 0.1)];
284
+ if (umaLinha) vai(entT[0], entT[1], forma(tx, ty, (t ? t.w : 200 * u) + 110 * u, h, { fill: 1, stroke: 0.35 }));
285
+ else vai(entT[0], entT[1], forma(t.left - 40 * u, ty, 6 * u, t.h * 0.92, { solid: 1, ac: 1, r: 3 }));
225
286
  }
226
287
  }
227
288
  return out;
@@ -261,7 +322,7 @@ function Palavras({ bl, sai, t, P, op = 1, de = null }) {
261
322
  filter: (1 - k) + o > 0.02 ? 'blur(' + ((1 - k) * 8 + o * 6).toFixed(2) + 'px)' : undefined,
262
323
  fontFamily: w.acento ? SERIF(P.fontes.serifa) : bl.mono ? MONO(P.fontes.mono) : SANS(P.fontes.texto), fontStyle: w.acento ? 'italic' : 'normal',
263
324
  fontWeight: w.acento ? 400 : bl.peso, fontSize: w.acento ? bl.tam * 1.14 : bl.tam, letterSpacing: w.acento || bl.mono ? 0 : '-0.02em',
264
- color: w.acento ? pal.acentos[0] : bl.cor,
325
+ color: w.acento ? (bl.corAcento || pal.acentos[0]) : bl.cor,
265
326
  }}>{w.t}</span>
266
327
  );
267
328
  })}
@@ -340,6 +401,55 @@ function Cartao({ c, ui, de: de0, sai, t, P, u: u0 }) {
340
401
  );
341
402
  }
342
403
 
404
+ // ── a foto: cinco jeitos de entrar, nenhum repetido no vídeo ────────────
405
+ function Foto({ im, b, sai, t, P, u, W, H }) {
406
+ const k = ed(c01((t - (im.t0 - 0.45)) / 0.75)); // a entrada
407
+ const o = entra(c01((t - sai) / 0.4)); // a saída
408
+ if (k <= 0.001 || o >= 0.999) return null;
409
+ const vida = c01((t - im.t0) / Math.max(1, b.ate - im.t0)); // o empurrão lento enquanto ela fica
410
+ const src = staticFile(im.src);
411
+ const f = im.foto;
412
+ const img = (extra = {}) => <Img src={src} style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', objectFit: 'cover', ...extra }} />;
413
+ const escurece = <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(0,0,0,0.05) 30%, rgba(0,0,0,0.62) 100%)' }} />;
414
+ if (im.jeito === 'janela') {
415
+ return <div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, overflow: 'hidden', opacity: 1 - o,
416
+ clipPath: 'inset(0 ' + ((1 - k) * 100).toFixed(2) + '% 0 0)' }}>
417
+ {img({ transform: 'scale(' + (1.14 - 0.08 * vida) + ') translateX(' + ((1 - k) * -6) + '%)' })}{escurece}</div>;
418
+ }
419
+ if (im.jeito === 'circulo') {
420
+ const r = Math.hypot(W, H) * 0.55 * k * (1 - o);
421
+ return <div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, overflow: 'hidden',
422
+ clipPath: 'circle(' + r.toFixed(1) + 'px at ' + W / 2 + 'px ' + H / 2 + 'px)' }}>
423
+ {img({ transform: 'scale(' + (1.2 - 0.12 * k - 0.05 * vida) + ')' })}{escurece}</div>;
424
+ }
425
+ if (im.jeito === 'cartao') {
426
+ const gira = (1 - k) * -38 + Math.sin(t * 0.6) * 3;
427
+ return <div style={{ position: 'absolute', left: f.x - f.w / 2, top: f.y - f.h / 2, width: f.w, height: f.h, borderRadius: 28 * u, overflow: 'hidden',
428
+ opacity: k * (1 - o), boxShadow: '0 ' + 50 * u + 'px ' + 90 * u + 'px -' + 30 * u + 'px rgba(0,0,0,0.55)',
429
+ transform: 'perspective(' + 1500 * u + 'px) translateY(' + ((1 - k) * 140 * u - o * 60 * u) + 'px) rotateY(' + gira + 'deg) rotateX(' + (6 * (1 - k)) + 'deg) scale(' + (0.86 + 0.14 * k) + ')' }}>
430
+ {img({ transform: 'scale(' + (1.08 - 0.06 * vida) + ')' })}</div>;
431
+ }
432
+ if (im.jeito === 'faixa') {
433
+ const meio = (1 - k + o) * 50;
434
+ return <div style={{ position: 'absolute', left: 0, top: f.y - f.h / 2, width: W, height: f.h, overflow: 'hidden',
435
+ clipPath: 'inset(' + meio.toFixed(2) + '% 0 ' + meio.toFixed(2) + '% 0)' }}>
436
+ {img({ transform: 'scale(1.12) translateX(' + (-4 + 8 * vida) + '%)' })}</div>;
437
+ }
438
+ // vazado: as letras do título, com a foto por dentro (SVG: o texto é o recorte).
439
+ const tb = im.texto;
440
+ const id = 'vz' + b.i;
441
+ const fam = '"' + P.fontes.texto + '", system-ui, sans-serif';
442
+ return <svg width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, opacity: (1 - o) }}>
443
+ <defs><clipPath id={id}>{tb.linhas.map((l, li) => (
444
+ <text key={li} x={l.x} y={l.y + tb.lh * 0.8} fontFamily={fam} fontWeight={700} fontSize={tb.tam} letterSpacing={-0.02 * tb.tam}>
445
+ {l.palavras.map((w) => w.t).join(' ')}</text>))}</clipPath></defs>
446
+ <g clipPath={'url(#' + id + ')'} style={{ opacity: k }}>
447
+ <image href={src} x={-W * 0.06 * (1 - vida)} y={tb.top - tb.h * 0.6} width={W * 1.12} height={tb.h * 2.2} preserveAspectRatio="xMidYMid slice"
448
+ transform={'translate(0 ' + ((1 - k) * 40 * u) + ')'} />
449
+ </g>
450
+ </svg>;
451
+ }
452
+
343
453
  // ── a peça ───────────────────────────────────────────────────────────────
344
454
  export function Fluido({ el, t }) {
345
455
  const P = el.partitura;
@@ -371,12 +481,13 @@ export function Fluido({ el, t }) {
371
481
  const so = P.logo && ult.tipo === 'cta' ? 1 - ed(c01((t - ult.de - 0.65) / 0.5)) : 1;
372
482
 
373
483
  const camadas = [];
374
- L.batidas.forEach(({ b, blocos, extra, risca, cartao, comando }, i) => {
484
+ L.batidas.forEach(({ b, blocos, extra, risca, cartao, comando, imagem }, i) => {
375
485
  const corte = b.transicao === 'corte' || (L.batidas[i + 1] && L.batidas[i + 1].b.transicao === 'corte');
376
486
  const ultima = i === L.batidas.length - 1;
377
487
  const sai = ultima ? 1e9 : b.ate - (corte ? 0 : 0.15), fimVis = b.ate + (corte ? 0 : 0.4);
378
488
  if (t < b.de - 0.3 || t > fimVis || (corte && t >= b.ate && !ultima)) return;
379
489
  if (cartao) camadas.push(<Cartao key={'k' + i} c={cartao} ui={b.ui} de={b.de} sai={sai} t={t} P={P} u={u} />);
490
+ if (imagem) camadas.push(<Foto key={'f' + i} im={imagem} b={b} sai={sai} t={t} P={P} u={u} W={W} H={H} />);
380
491
  // A lista: as curvas que se desenham de uma pílula à próxima, e as pílulas que nascem na ponta.
381
492
  if (b.tipo === 'lista' && extra) {
382
493
  const ps = extra.pilulas;
@@ -398,17 +509,27 @@ export function Fluido({ el, t }) {
398
509
  background: sup, border: (1.2 * u) + 'px solid ' + alfa(pal.tinta, 0.2), opacity: kp * o, transform: 'scale(' + (0.7 + 0.3 * kp) + ')',
399
510
  boxShadow: pal.escuro ? undefined : '0 14px 34px -14px ' + alfa(pal.tinta, 0.3) }} />);
400
511
  }
401
- // O ícone de cada pílula: um brilho de quatro pontas que gira ao nascer.
512
+ /* O marcador de cada pílula muda de vídeo para vídeo (P.marcador): o
513
+ brilho de quatro pontas em toda lista era vício (08/10). */
402
514
  const kd = ed(c01((t - p.f.de) / 0.5));
403
- camadas.push(<svg key={'d' + i + j} width={p.ic} height={p.ic} viewBox="-1 -1 2 2" style={{ position: 'absolute', left: p.x - p.w / 2 + 34 * u, top: p.y - p.ic / 2,
515
+ const mk = P.marcador || 'brilho';
516
+ const mx0 = p.x - p.w / 2 + 34 * u, my0 = p.y - p.ic / 2;
517
+ if (mk === 'brilho') camadas.push(<svg key={'d' + i + j} width={p.ic} height={p.ic} viewBox="-1 -1 2 2" style={{ position: 'absolute', left: mx0, top: my0,
404
518
  opacity: kd * o, transform: 'rotate(' + ((1 - kd) * -90 + Math.sin(t * 1.3 + j) * 6) + 'deg) scale(' + kd + ')' }}>
405
519
  <path d={BRILHO4} fill={pal.acentos[0]} />
406
520
  </svg>);
521
+ else if (mk === 'numero') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0 - 4 * u, top: p.y - p.ic * 0.62, height: p.ic * 1.24, lineHeight: p.ic * 1.24 + 'px',
522
+ fontFamily: MONO(P.fontes.mono), fontSize: p.ic * 0.78, color: pal.acentos[0], opacity: kd * o, transform: 'translateY(' + (1 - kd) * 10 * u + 'px)' }}>{String(j + 1).padStart(2, '0')}</div>);
523
+ else if (mk === 'traco') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0, top: p.y - 1.5 * u, width: p.ic * kd, height: 3 * u, borderRadius: 3,
524
+ background: pal.acentos[0], opacity: o }} />);
525
+ else if (mk === 'ponto') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0 + p.ic * 0.3, top: p.y - p.ic * 0.2, width: p.ic * 0.4, height: p.ic * 0.4, borderRadius: 99,
526
+ background: pal.acentos[0], opacity: o, transform: 'scale(' + kd + ')' }} />);
407
527
  });
408
528
  }
409
529
  for (const bl of blocos) {
410
530
  const f = bl.f;
411
531
  if (b.tipo === 'passos' && extra) continue;
532
+ if (imagem && imagem.jeito === 'vazado' && bl === imagem.texto) continue; // as letras são a foto (Foto desenha)
412
533
  const op = f.papel === 'nao' && risca ? 1 - 0.55 * ed(c01((t - risca) / 0.4)) : 1;
413
534
  if (f.papel === 'numero' && b.numero) {
414
535
  const k = ed(c01((t - f.de) / 1.1));
@@ -284,6 +284,17 @@ function geometria(a, b, L, P) {
284
284
  ponto e anda até onde o próximo traço começa) e os estados dela. */
285
285
  function estadosDoAcento(P, L) {
286
286
  const base = Math.min(P.largura, P.altura) / 1080;
287
+ /* O ACENTO EM TRAÇO (09/10, "todo vídeo tem aquela bolinha"): sem a semente
288
+ no meio da tela, sem a viagem em ponto e sem o recolher — cada traço se
289
+ desenha no seu lugar e some com a batida (acentoEm). */
290
+ const semBola = P.fundo && P.fundo.acento === 'traco';
291
+ if (semBola) {
292
+ const out = [];
293
+ P.batidas.forEach((b, bi) => b.acento.forEach((a) => out.push({ t: a.t, dur: a.dur, g: geometria(a, b, L, P), cor: b.cores.acento, batida: bi, forma: a.forma, ate: b.ate })));
294
+ out.sort((a, b) => a.t - b.t);
295
+ out.semBola = true;
296
+ return out;
297
+ }
287
298
  const out = [{ t: 0, dur: 0.35, g: (() => { const r = 16 * base; const pts = []; for (let i = 0; i < N; i++) { const q = (i / (N - 1)) * Math.PI * 2; pts.push([P.largura / 2 + Math.cos(q) * r * 0.35, P.altura / 2 + Math.sin(q) * r * 0.35]); } return { pts, larg: r * 1.35, alfa: 1, frente: true, cx: P.largura / 2, cy: P.altura / 2 }; })(), cor: P.batidas[0].cores.acento, batida: 0 }];
288
299
  P.batidas.forEach((b, bi) => {
289
300
  b.acento.forEach((a, k) => {
@@ -295,12 +306,32 @@ function estadosDoAcento(P, L) {
295
306
  }
296
307
  out.push({ t: a.t, dur: a.dur, g, cor: b.cores.acento, batida: bi, forma: a.forma });
297
308
  });
309
+ /* No fim da batida o acento se RECOLHE num ponto: ele ficava na última
310
+ forma até a batida seguinte, e a moldura sobrava sozinha na tela depois
311
+ que a palavra saía (teste de 08/10). */
312
+ const ult = b.acento[b.acento.length - 1];
313
+ if (ult && b.ate != null) {
314
+ const g = geometria(ult, b, L, P);
315
+ const t0 = Math.max(ult.t + (ult.dur || 0), b.ate - 0.3);
316
+ if (t0 < b.ate) out.push({ t: t0, dur: 0.25, g: geometria({ forma: 'viagem', x: g.cx, y: g.cy, r: 7 * base }, b, L, P), cor: b.cores.acento, batida: bi });
317
+ }
298
318
  });
299
319
  out.sort((a, b) => a.t - b.t);
300
320
  return out;
301
321
  }
302
322
 
303
323
  function acentoEm(t, estados) {
324
+ if (estados.semBola) {
325
+ // O traço se desenha (os pontos aparecem em ordem) e some no fim da batida; antes do primeiro, nada.
326
+ let k = -1;
327
+ while (k < estados.length - 1 && estados[k + 1].t <= t) k++;
328
+ const cur = estados[k];
329
+ if (!cur) return { pts: [[0, 0], [0, 0]], larg: 0.1, alfa: 0, frente: true, cor: '#000000', cx: 0, cy: 0 };
330
+ const p = arrasto(c01((t - cur.t) / Math.max(0.01, cur.dur)));
331
+ const n = Math.max(2, Math.ceil(cur.g.pts.length * p));
332
+ const some = 1 - c01((t - (cur.ate - 0.22)) / 0.22);
333
+ return { pts: cur.g.pts.slice(0, n), larg: cur.g.larg, alfa: cur.g.alfa * some, frente: cur.g.frente, tampa: cur.g.tampa, cor: cur.cor, cx: cur.g.cx, cy: cur.g.cy };
334
+ }
304
335
  let k = 0;
305
336
  while (k < estados.length - 1 && estados[k + 1].t <= t) k++;
306
337
  const cur = estados[k], prev = estados[Math.max(0, k - 1)];
@@ -650,7 +681,7 @@ function Fundo({ P, b, prevB, t, cam, frame, origem }) {
650
681
  return (
651
682
  <div style={{ position: 'absolute', inset: 0, background: corBase, overflow: 'hidden' }}>
652
683
  {circulo}
653
- <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />
684
+ {(f.ambiente || 'bokeh') !== 'limpo' && <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />}
654
685
  {(f.particulas || []).map((q, i) => {
655
686
  const x = ((q.x - cam.x * q.z * 1.6 + t * q.v * (i % 2 ? 1 : -0.6)) % W + W) % W;
656
687
  const y = ((q.y - cam.y * q.z * 1.6 - t * q.v * 0.7) % H + H) % H;
@@ -773,25 +804,39 @@ function Heroi({ b, t, P, slot }) {
773
804
  return <svg width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible', opacity: op }}>{els}</svg>;
774
805
  }
775
806
 
776
- /* O HUD do motion designer, em toda batida: cantos de enquadramento, a marca,
777
- a seção, o timecode, os quadros por segundo e a régua do vídeo. */
807
+ /* A MOLDURA (o HUD do motion designer): cantos de enquadramento, o assunto do
808
+ vídeo, a SEÇÃO ("01 · A CONVERSA"), o timecode e a régua do vídeo — fixa do
809
+ começo ao fim, como no reel de referência. Mora dentro da área segura
810
+ (P.hud.area: no 9:16, longe do status e dos botões do celular). 'cantos' é
811
+ a versão mínima: os cantos e a seção. Partitura antiga (sem área): a margem de antes. */
778
812
  const SECOES = { gigante: 'TESE', pilha: 'ARGUMENTO', divisao: 'CONTRASTE', contador: 'NÚMERO', cascata: 'LISTA', parede: 'TEXTURA', citacao: 'CITAÇÃO', marca: 'MARCA', palavras: 'FALA', produto: 'PRODUTO' };
779
813
  function HudG({ P, b, bi, t, fps }) {
780
- const W = P.largura, H = P.altura, m = Math.round(Math.min(W, H) * 0.035), tam = Math.max(11, Math.round(Math.min(W, H) * 0.0125));
814
+ const W = P.largura, H = P.altura, h = P.hud, deitado = W >= H;
815
+ const m0 = Math.round(Math.min(W, H) * 0.035);
816
+ const A = h.area || { x: m0, topo: m0, base: m0 };
817
+ const tam = Math.max(11, Math.round(Math.min(W, H) * (deitado ? 0.0125 : 0.018)));
818
+ const c = Math.round(Math.min(W, H) * 0.026);
781
819
  const cor = alfa(b.cores.texto, 0.72);
782
820
  const f = Math.round(t * fps);
783
821
  const tc = [Math.floor(t / 60), Math.floor(t) % 60, f % fps].map((n) => String(n).padStart(2, '0')).join(':');
784
- const rot = { position: 'absolute', fontFamily: familia(P.fontes.mono), fontSize: tam, letterSpacing: '0.14em', color: cor, whiteSpace: 'nowrap', textTransform: 'uppercase' };
785
- const canto = (x, y, sx, sy) => <div key={x + '-' + y} style={{ position: 'absolute', left: x - (sx < 0 ? m * 0.7 : 0), top: y - (sy < 0 ? m * 0.7 : 0), width: m * 0.7, height: m * 0.7,
822
+ const rot = { position: 'absolute', fontFamily: familia(P.fontes.mono), fontSize: tam, letterSpacing: '0.14em', color: cor, whiteSpace: 'nowrap', textTransform: 'uppercase', lineHeight: 1 };
823
+ const L = A.x, T = A.topo, R = W - A.x, B = H - A.base;
824
+ const canto = (x, y, sx, sy) => <div key={x + '-' + y} style={{ position: 'absolute', left: sx > 0 ? x : x - c, top: sy > 0 ? y : y - c, width: c, height: c,
786
825
  borderLeft: sx > 0 ? '1.5px solid ' + cor : undefined, borderRight: sx < 0 ? '1.5px solid ' + cor : undefined, borderTop: sy > 0 ? '1.5px solid ' + cor : undefined, borderBottom: sy < 0 ? '1.5px solid ' + cor : undefined }} />;
826
+ // A seção: o número e o nome dela (a partitura antiga não tem seção — vale a composição).
827
+ const k = b.secao != null ? b.secao : bi;
828
+ const nome = (h.secoes || [])[k] || SECOES[b.comp] || String(b.comp).toUpperCase();
829
+ const secao = <div style={{ ...rot, right: W - R + c * 0.6, top: T + c * 0.6 }}>{String(k + 1).padStart(2, '0') + ' · ' + nome}</div>;
830
+ if (h.tipo === 'cantos') return <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>{canto(L, T, 1, 1)}{canto(R, T, -1, 1)}{canto(L, B, 1, -1)}{canto(R, B, -1, -1)}{secao}</div>;
831
+ const n = Math.max(1, (h.secoes || []).length);
787
832
  return (
788
833
  <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
789
- {canto(m, m, 1, 1)}{canto(W - m, m, -1, 1)}{canto(m, H - m, 1, -1)}{canto(W - m, H - m, -1, -1)}
790
- <div style={{ ...rot, left: m * 1.5, top: m * 1.1 }}><b>{P.hud.marca}</b><span style={{ opacity: 0.6 }}>{' · MOTION · ' + P.hud.ano}</span></div>
791
- <div style={{ ...rot, right: m * 1.5, top: m * 1.1 }}>{String(bi + 1).padStart(2, '0') + ' — ' + (SECOES[b.comp] || b.comp.toUpperCase())}</div>
792
- <div style={{ ...rot, left: m * 1.5, bottom: m * 1.1 }}>{tc}<span style={{ opacity: 0.55 }}>{' ' + fps + ' FPS'}</span></div>
793
- <div style={{ ...rot, right: m * 1.5, bottom: m * 1.1 }}>{W + '×' + H}</div>
794
- <div style={{ position: 'absolute', left: m * 1.5, bottom: m * 0.62, height: 1.5, width: (W - m * 3) * c01(t / P.duracao), background: cor }} />
834
+ {canto(L, T, 1, 1)}{canto(R, T, -1, 1)}{canto(L, B, 1, -1)}{canto(R, B, -1, -1)}
835
+ <div style={{ ...rot, left: L + c * 0.6, top: T + c * 0.6 }}><b>{h.marca}</b>{deitado && <span style={{ opacity: 0.6 }}>{' · ' + h.ano}</span>}</div>
836
+ {secao}
837
+ <div style={{ ...rot, left: L + c * 0.6, bottom: H - B + c * 0.6 }}>{tc}<span style={{ opacity: 0.55 }}>{' ' + fps + ' FPS'}</span></div>
838
+ <div style={{ ...rot, right: W - R + c * 0.6, bottom: H - B + c * 0.6 }}>{(h.secoes ? String(k + 1).padStart(2, '0') + ' / ' + String(n).padStart(2, '0') : W + '×' + H)}</div>
839
+ <div style={{ position: 'absolute', left: L + c * 0.6, top: B - c * 0.35, height: 1.5, width: (R - L - c * 1.2) * c01(t / P.duracao), background: cor }} />
795
840
  </div>
796
841
  );
797
842
  }
@@ -806,6 +851,9 @@ export function Gerativo({ el, t, fps }) {
806
851
  if (!L) return <div style={{ position: 'absolute', inset: 0, background: P.paleta.fundo }} />;
807
852
  const frame = Math.round(t * fps);
808
853
  const bs = P.batidas;
854
+ /* O AMBIENTE (09/10): o bokeh e o feixe de luz só quando a identidade pede
855
+ (P.fundo.ambiente). Partitura antiga, sem o campo: como antes (ambiente || 'bokeh'). */
856
+ const ambiente = P.fundo.ambiente || 'bokeh', comLuz = ambiente !== 'limpo';
809
857
  let bi = 0;
810
858
  while (bi < bs.length - 1 && t >= bs[bi + 1].de) bi++;
811
859
  const b = bs[bi];
@@ -914,18 +962,39 @@ export function Gerativo({ el, t, fps }) {
914
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 };
915
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 };
916
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 };
917
- else return <Heroi b={{ ...x, heroi: { ...x.heroi, tipo: 'aneis' } }} t={t} P={P} slot={{ cx: (bl.x + bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R: Math.min(W, H) * 0.3 }} />;
965
+ // Sem vazio, não entra (09/10): o anel no meio do texto era enfeite que ninguém pediu.
966
+ else return null;
918
967
  return <Heroi b={x} t={t} P={P} slot={slot} />;
919
968
  })()}
920
969
  <Textura b={x} cx={lay.caixas[x.linhas[0].id]} t={t} P={P} periodo={lay.textura} />
921
970
  {x.icone3d && (() => {
922
- /* O ícone 3D mora no vazio do bloco: à direita (deitado) ou acima
923
- (em pé); sem espaço, não entra — nunca por cima do texto. */
971
+ /* O ícone 3D mora no vazio do bloco, no lugar que o projeto escolheu
972
+ (ic.lados, gerativo.js: direita, esquerda, acima, abaixo ou canto);
973
+ sem espaço lá, tenta o próximo; sem nenhum, não entra — nunca
974
+ por cima do texto. Partitura antiga (sem lados): direita ou acima. */
924
975
  const ic = x.icone3d, tam = ic.tamanho, M = P.margem, bl = lay.bloco;
925
- let px = null, py = null;
926
- if (ic.lado === 'direita' && W - M - bl.x1 >= tam * 0.9) { px = Math.min(W - M - tam, bl.x1 + (W - M - bl.x1 - tam) / 2); py = (bl.y + bl.y1) / 2 - tam / 2; }
927
- else if (bl.y - M * 1.2 >= tam * 0.85) { px = x.bloco.alinha === 'centro' ? (bl.x + bl.x1) / 2 - tam / 2 : bl.x - tam * 0.08; py = Math.max(M, bl.y - tam - 0.02 * H); }
928
- if (px == null) return null;
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;
978
+ 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),
983
+ // 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,
985
+ };
986
+ const ordem = ic.lados || [ic.lado, 'acima'];
987
+ const achou = ordem.map((l) => LUGAR[l] && LUGAR[l]()).find(Boolean);
988
+ if (!achou) return null;
989
+ const [px, py] = achou;
990
+ /* O ÍCONE PLANO (09/10): o do Iconify que o roteiro pediu (o pão, o QR
991
+ code), na cor do acento da identidade — entra em mola e flutua de leve. */
992
+ if (ic.svg) {
993
+ const k = MOLA(t - ic.t), fl = Math.sin((t - ic.t) * 1.3) * 0.012 * tam, sai = c01((t - (x.ate - 0.25)) / 0.25);
994
+ return <div style={{ position: 'absolute', left: Math.round(px), top: Math.round(py + fl), width: tam, height: tam, color: ic.cor, opacity: c01(k * 1.4) * (1 - sai),
995
+ transform: 'translateZ(120px) scale(' + (0.6 + 0.4 * k).toFixed(4) + ') rotate(' + ((1 - k) * -8).toFixed(2) + 'deg)', transformOrigin: '50% 60%' }}
996
+ dangerouslySetInnerHTML={{ __html: ic.svg }} />;
997
+ }
929
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>;
930
999
  })()}
931
1000
  {x.linhas.map((l, li) => {
@@ -961,7 +1030,7 @@ export function Gerativo({ el, t, fps }) {
961
1030
  </div>
962
1031
  {/* A FRENTE: manchas de luz que passam perto da lente (bokeh) e o feixe
963
1032
  de luz que varre o quadro quando uma batida chega. */}
964
- {[0, 1, 2, 3].map((k) => {
1033
+ {ambiente === 'bokeh' && [0, 1, 2, 3].map((k) => {
965
1034
  const per = 5 + acaso(P.semente % 997 + k * 7) * 4, fase = acaso(P.semente % 991 + k * 13) * per;
966
1035
  const u = ((t + fase) % per) / per;
967
1036
  const tam = Math.min(W, H) * (0.28 + acaso(k * 17 + 3) * 0.3);
@@ -970,7 +1039,7 @@ export function Gerativo({ el, t, fps }) {
970
1039
  return <div key={'bk' + k} style={{ position: 'absolute', left: x - tam / 2, top: y - tam / 2, width: tam, height: tam, borderRadius: '50%', pointerEvents: 'none',
971
1040
  background: 'radial-gradient(circle, ' + alfa(cor, k % 2 ? 0.16 : 0.08) + ' 0%, transparent 66%)', opacity: Math.sin(Math.PI * u) }} />;
972
1041
  })}
973
- {(() => {
1042
+ {comLuz && (() => {
974
1043
  const k = c01((t - b.de) / 0.95);
975
1044
  if (k <= 0 || k >= 1) return null;
976
1045
  const x = lerp(-0.7 * W, 1.2 * W, ed(k));
@@ -735,7 +735,64 @@ export function Site({ el, t, fps }) {
735
735
  );
736
736
  }
737
737
 
738
+ // ── o print do site de verdade, em 3D ───────────────────────────────────
739
+ /* A peça `site` acima DESENHA um site genérico; esta mostra o print real
740
+ (print-site.js), numa janela de navegador ou num celular, inclinados em
741
+ perspectiva, com espessura, reflexo e a página rolando devagar.
742
+ "proporcao" é altura/largura do print: é ela que diz quanto há para rolar.
743
+ Só CSS 3D — o three.js entraria aqui só para o mesmo plano com textura. */
744
+ export function PrintSite({ el, t, fps }) {
745
+ const W = el.largura || 1280, H = el.altura || (el.celular ? 900 : 820);
746
+ const t0 = el.t || 0;
747
+ const cel = !!el.celular;
748
+ const m = mola(t, t0, fps, { damping: 30, stiffness: 90, mass: 1 });
749
+ const inc = el.inclinacao ?? (cel ? 14 : 18); // graus de giro em Y no repouso
750
+ const deriva = Math.sin((t - t0) * 0.6) * 3; // a câmera que respira, sem parar
751
+ const rotY = lerp(-48, -inc, m) + deriva;
752
+ const rotX = lerp(22, cel ? 6 : 9, m);
753
+ const z = lerp(-700, 0, m);
754
+ const barra = cel ? 0 : Math.round(H * 0.06); // a barra do navegador
755
+ const borda = cel ? Math.round(W * 0.045) : 0; // a moldura do celular
756
+ const telaW = W - borda * 2, telaH = H - barra - borda * 2;
757
+ // A página rola de cima até o fim, com respiro parado nas duas pontas.
758
+ const total = Math.max(0, telaW * (el.proporcao || 0) - telaH);
759
+ const dur = el.duracao || 6;
760
+ const k = el.rolar === false ? 0 : CURVAS_KIT.camera(entre(t, t0 + 1.0, t0 + dur - 0.6));
761
+ const brilho = 30 + 50 * entre(t, t0, t0 + dur);
762
+ const raio = cel ? Math.round(W * 0.14) : Math.round(H * 0.018);
763
+ return (
764
+ <div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, perspective: 2400, opacity: t >= t0 ? Math.min(1, m * 2) : 0 }}>
765
+ <div style={{ position: 'absolute', inset: 0, transformStyle: 'preserve-3d',
766
+ transform: 'translateZ(' + z + 'px) rotateX(' + rotX + 'deg) rotateY(' + rotY + 'deg)' }}>
767
+ {/* A espessura: duas placas atrás, mais escuras — o aparelho tem corpo. */}
768
+ {[18, 9].map((d) => (
769
+ <div key={d} style={{ position: 'absolute', inset: 0, borderRadius: raio, background: d > 10 ? '#0b0c10' : '#1d1f26', transform: 'translateZ(' + -d + 'px)' }} />
770
+ ))}
771
+ <div style={{ position: 'absolute', inset: 0, borderRadius: raio, overflow: 'hidden', background: cel ? '#0e0f13' : '#f4f4f6',
772
+ boxShadow: '0 80px 160px -40px rgba(0,0,0,0.55), 0 30px 60px -30px rgba(0,0,0,0.4)' }}>
773
+ {cel ? null : (
774
+ <div style={{ height: barra, display: 'flex', alignItems: 'center', gap: barra * 0.18, padding: '0 ' + barra * 0.4 + 'px', background: '#e9e9ee' }}>
775
+ {['#ff5f57', '#febc2e', '#28c840'].map((c) => <div key={c} style={{ width: barra * 0.24, height: barra * 0.24, borderRadius: 99, background: c }} />)}
776
+ <div style={{ marginLeft: barra * 0.4, flex: 1, maxWidth: W * 0.5, height: barra * 0.5, borderRadius: 99, background: '#fff', display: 'flex', alignItems: 'center',
777
+ padding: '0 ' + barra * 0.35 + 'px', fontFamily: MONO(), fontSize: barra * 0.26, color: '#55585f', whiteSpace: 'nowrap', overflow: 'hidden' }}>
778
+ {String(el.url || '').replace(/^https?:\/\//, '').replace(/\/$/, '')}
779
+ </div>
780
+ </div>
781
+ )}
782
+ <div style={{ position: 'absolute', left: borda, top: barra + borda, width: telaW, height: telaH, overflow: 'hidden', borderRadius: cel ? raio * 0.75 : 0, background: '#fff' }}>
783
+ <Img src={staticFile(el.src)} style={{ position: 'absolute', left: 0, top: 0, width: telaW, transform: 'translateY(' + (-total * k).toFixed(1) + 'px)' }} />
784
+ </div>
785
+ {cel ? <div style={{ position: 'absolute', left: '50%', top: borda * 1.3, width: W * 0.28, height: borda * 1.1, marginLeft: -W * 0.14, borderRadius: 99, background: '#0e0f13' }} /> : null}
786
+ {/* O reflexo que atravessa o vidro enquanto o aparelho gira. */}
787
+ <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none',
788
+ background: 'linear-gradient(115deg, rgba(255,255,255,0) ' + (brilho - 18) + '%, rgba(255,255,255,0.16) ' + brilho + '%, rgba(255,255,255,0) ' + (brilho + 18) + '%)' }} />
789
+ </div>
790
+ </div>
791
+ </div>
792
+ );
793
+ }
794
+
738
795
  export const MARCA = {
739
796
  orbe: Orbe, vetor: Vetor, manchete: Manchete, cromo: Cromo, cores: Cores, padrao: Padrao, particulas: Particulas,
740
- estrela: Estrela, cartoes: Cartoes, painel: Painel, esfera: Esfera, grade: Grade, site: Site,
797
+ estrela: Estrela, cartoes: Cartoes, painel: Painel, esfera: Esfera, grade: Grade, site: Site, printsite: PrintSite,
741
798
  };
@@ -278,8 +278,11 @@ const FORMAS = {
278
278
  };
279
279
 
280
280
  function Mat({ tipo, cor }) {
281
- // Metal escovado, bala (verniz) ou argila — os três refletem a sala (Ambiente).
281
+ // Metal escovado, bala (verniz), pérola, acetinado ou argila — todos refletem a sala (Ambiente).
282
282
  if (tipo === 'metal') return <meshPhysicalMaterial color={cor} roughness={0.32} metalness={0.85} clearcoat={0.4} envMapIntensity={1.2} />;
283
+ // Pérola (iridescente) e acetinado (meio metal, meio fosco): o cardápio de 09/10 (gerativo.MATERIAIS_ICONE).
284
+ if (tipo === 'perola') return <meshPhysicalMaterial color={cor} roughness={0.25} metalness={0.1} clearcoat={1} clearcoatRoughness={0.1} iridescence={1} iridescenceIOR={1.3} />;
285
+ if (tipo === 'acetinado') return <meshPhysicalMaterial color={cor} roughness={0.5} metalness={0.45} clearcoat={0.4} />;
283
286
  if (tipo === 'verniz') return <meshPhysicalMaterial color={cor} roughness={0.18} metalness={0} clearcoat={1} clearcoatRoughness={0.08} sheen={0.3} />;
284
287
  return <meshPhysicalMaterial color={cor} roughness={0.72} metalness={0} clearcoat={0.15} />; // argila: fosca, macia
285
288
  }
@@ -49,6 +49,8 @@ const PECAS = {
49
49
  esfera: {},
50
50
  grade: {},
51
51
  site: {},
52
+ // O print REAL do site (print-site.js) numa janela ou num celular em 3D, rolando.
53
+ printsite: { precisa: ['src'] },
52
54
  // O produto em 3D (kit/tres.jsx): fone, celular, lata ou caixa, de geometria.
53
55
  produto3d: { precisa: ['modelo'] },
54
56
  // Os ícones 3D de geometria (kit/tres.jsx): raio, coração, bateria, relógio, fone…
@@ -54,7 +54,9 @@ const kit = require('./kit.js');
54
54
  const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente', 'grupo', 'kit']);
55
55
  const FORMAS = new Set(['retangulo', 'circulo', 'linha', 'poligono', 'traco']);
56
56
  const AJUSTES = new Set(['cover', 'contain', 'preencher']);
57
- const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph', 'chicote', 'soco', 'flash', 'liquido']);
57
+ const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph', 'chicote', 'soco', 'flash', 'liquido',
58
+ // as 3D, que mexem nas duas cenas (remotion.js, estilo3d)
59
+ 'virar', 'cubo', 'profundidade', 'inclinar']);
58
60
 
59
61
  /* As propriedades que a animação por quadro-chave alcança. Cada uma é um
60
62
  número no tempo; a curva entre dois quadros é a suavização declarada.
@@ -248,6 +250,8 @@ function conferirElemento(caminho, el, pasta, problemas, duracao) {
248
250
  for (const oque of kit.conferirPeca(el)) problemas.push(erro(`${caminho}.peca`, oque));
249
251
  // A logo de uma peça é arquivo da pasta, como a imagem.
250
252
  if (el.logo) conferirArquivo(`${caminho}.logo`, el.logo, pasta, problemas);
253
+ // E o print do site (printsite) também: sem o arquivo, a janela 3D sai vazia.
254
+ if (el.src) conferirArquivo(`${caminho}.src`, el.src, pasta, problemas);
251
255
  break;
252
256
  }
253
257
  }