primocode 8.38.1 → 8.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/act.js +87 -2
- package/lib/audio-livre.js +172 -0
- package/lib/catalogo.js +7 -0
- package/lib/marca.js +280 -0
- package/lib/referencia.js +210 -63
- package/lib/remotion.js +245 -45
- package/lib/studio.js +76 -3
- package/lib/tools.js +13 -0
- package/package.json +2 -2
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/arquivos/tela-motion-3ecaa.json +135 -0
- package/studio/arquivos/tela-motion-d6150.json +135 -0
- package/studio/edge.py +66 -16
- package/studio/ferramentas/01-modelos.md +20 -2
- package/studio/ferramentas/corte.md +140 -35
- package/studio/ferramentas/tela.md +21 -0
- package/studio/server.py +47 -11
- package/studio/studio.log +28 -0
- package/studio/web/js/player.js +30 -2
- package/studio/web/js/render.js +74 -4
- package/studio/web/js/sfx.js +128 -2
- package/studio/web/render.css +95 -0
package/studio/server.py
CHANGED
|
@@ -374,11 +374,21 @@ def preparar_voz(doc):
|
|
|
374
374
|
Com o ElevenLabs configurado, não há nada para baixar: a síntese é
|
|
375
375
|
imediata e os 90 MB do Piper ficam para o dia em que faltarem chave ou
|
|
376
376
|
internet."""
|
|
377
|
-
if _tem_narracao(doc) and not elevenlabs_lib.configurado()
|
|
377
|
+
if _tem_narracao(doc) and not elevenlabs_lib.configurado():
|
|
378
|
+
# O Edge (pip, segundos) e o Piper (90 MB, minutos) começam JUNTOS.
|
|
379
|
+
# Antes só o Piper era preparado aqui, e o Edge — a melhor voz grátis —
|
|
380
|
+
# só era instalado quando a primeira cena já estava pedindo áudio: a
|
|
381
|
+
# narração caía no Google enquanto o pip rodava, e o dono ouviu a
|
|
382
|
+
# voz de robô no vídeo inteiro.
|
|
378
383
|
try:
|
|
379
|
-
|
|
384
|
+
edge_lib.instalar()
|
|
380
385
|
except Exception:
|
|
381
386
|
pass
|
|
387
|
+
if not voz_lib.pronta():
|
|
388
|
+
try:
|
|
389
|
+
voz_lib.instalar()
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
382
392
|
|
|
383
393
|
|
|
384
394
|
# ------------------------------------------------------- narração em arquivo --
|
|
@@ -474,6 +484,7 @@ def _gravar_narracao(arq_id):
|
|
|
474
484
|
if c.get("narracaoAudio") and not c.get("narracaoVoz"):
|
|
475
485
|
continue
|
|
476
486
|
audio, ext, dur = None, ".wav", None
|
|
487
|
+
motor_usado = None
|
|
477
488
|
# A CORRENTE, na ordem de qualidade. Cada motor cai no seguinte
|
|
478
489
|
# quando não devolve áudio — e o Google, no fim, garante que a
|
|
479
490
|
# cena nunca fique muda. É o que faz o pré-render nunca parar.
|
|
@@ -483,14 +494,19 @@ def _gravar_narracao(arq_id):
|
|
|
483
494
|
if elevenlabs_lib.configurado():
|
|
484
495
|
audio, dur = elevenlabs_lib.sintetizar(texto, voz)
|
|
485
496
|
if audio:
|
|
486
|
-
ext = ".mp3"
|
|
497
|
+
ext, motor_usado = ".mp3", "elevenlabs"
|
|
487
498
|
|
|
488
499
|
# 2) Edge TTS: neural, grátis, sem chave. MP3, duração medida pelo
|
|
489
|
-
# VTT que o próprio edge escreve.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
500
|
+
# VTT que o próprio edge escreve. Ninguém está olhando este
|
|
501
|
+
# laço: se o pip ainda roda, espera por ele (até 2 min) — é de
|
|
502
|
+
# graça, e é o que separa voz de gente de voz de robô.
|
|
503
|
+
if not audio:
|
|
504
|
+
if not edge_lib.disponivel():
|
|
505
|
+
edge_lib.instalar(esperar=120)
|
|
506
|
+
if edge_lib.disponivel():
|
|
507
|
+
dados, edur = edge_lib.sintetizar(texto, voz)
|
|
508
|
+
if dados:
|
|
509
|
+
audio, dur, ext, motor_usado = dados, edur, ".mp3", "edge"
|
|
494
510
|
|
|
495
511
|
# 3) Piper: neural e offline, quando baixado e compatível. WAV.
|
|
496
512
|
if not audio:
|
|
@@ -503,15 +519,19 @@ def _gravar_narracao(arq_id):
|
|
|
503
519
|
sys.stderr.write("[voz] Piper cena %d falhou: %s\n" % (i + 1, e))
|
|
504
520
|
audio = None
|
|
505
521
|
if audio:
|
|
506
|
-
dur, ext = _duracao_wav(audio), ".wav"
|
|
522
|
+
dur, ext, motor_usado = _duracao_wav(audio), ".wav", "piper"
|
|
507
523
|
|
|
508
524
|
# 4) Google: última reserva. Robótica, mas responde. MP3 — a
|
|
509
525
|
# duração sai do tamanho (CBR de 24 kbps ≈ 3000 bytes/s).
|
|
526
|
+
# Vai MARCADA na cena (`narracaoMotor`): o agente lê e avisa a
|
|
527
|
+
# pessoa, em vez de ela descobrir ouvindo.
|
|
510
528
|
if not audio:
|
|
511
529
|
dados = _google_tts(texto)
|
|
512
530
|
if dados:
|
|
513
|
-
audio, ext = dados, ".mp3"
|
|
531
|
+
audio, ext, motor_usado = dados, ".mp3", "google"
|
|
514
532
|
dur = round(len(dados) / 3000.0, 2)
|
|
533
|
+
sys.stderr.write("[voz] RESERVA na cena %d: Google (edge-tts: %s)\n"
|
|
534
|
+
% (i + 1, edge_lib.ultimo_erro() or "não instalado"))
|
|
515
535
|
|
|
516
536
|
if not audio:
|
|
517
537
|
continue
|
|
@@ -520,6 +540,7 @@ def _gravar_narracao(arq_id):
|
|
|
520
540
|
fh.write(audio)
|
|
521
541
|
c["narracaoAudio"] = "/midia/" + nome
|
|
522
542
|
c["narracaoVoz"] = assinatura
|
|
543
|
+
c["narracaoMotor"] = motor_usado
|
|
523
544
|
# A duração medida vai junto: com ela o player sabe quanto a cena
|
|
524
545
|
# REALMENTE dura (a fala inteira + respiro) e a linha do tempo
|
|
525
546
|
# vira determinística — sem ela, o relógio congela na beirada da
|
|
@@ -540,6 +561,8 @@ def _gravar_narracao(arq_id):
|
|
|
540
561
|
and cenas[i].get("narracao") == c.get("narracao"):
|
|
541
562
|
cenas[i]["narracaoAudio"] = c["narracaoAudio"]
|
|
542
563
|
cenas[i]["narracaoVoz"] = c["narracaoVoz"]
|
|
564
|
+
if c.get("narracaoMotor"):
|
|
565
|
+
cenas[i]["narracaoMotor"] = c["narracaoMotor"]
|
|
543
566
|
if c.get("narracaoDur"):
|
|
544
567
|
cenas[i]["narracaoDur"] = c["narracaoDur"]
|
|
545
568
|
gravar(atual)
|
|
@@ -1473,6 +1496,11 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
1473
1496
|
# ElevenLabs porque é a melhor voz gratuita da corrente — vozes
|
|
1474
1497
|
# da Microsoft, muito acima do Google. Precisa de internet.
|
|
1475
1498
|
if not audio and motor in ("", "edge"):
|
|
1499
|
+
# Pacote ainda não instalado? ESPERA o pip (até 60 s) em vez de
|
|
1500
|
+
# cair para o Google. Quem pediu voz tem internet — é o que o Edge
|
|
1501
|
+
# precisa — e meio minuto é menos ruim que a voz de robô no vídeo.
|
|
1502
|
+
if not edge_lib.disponivel():
|
|
1503
|
+
edge_lib.instalar(esperar=60)
|
|
1476
1504
|
dados, _ = edge_lib.sintetizar(texto, {"voz": voz_pedida,
|
|
1477
1505
|
"grave": grave, "velocidade": velocidade})
|
|
1478
1506
|
if dados:
|
|
@@ -1505,17 +1533,25 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
1505
1533
|
except Exception as e:
|
|
1506
1534
|
sys.stderr.write("[voz] Voicebox falhou (%s); indo de Google\n" % e)
|
|
1507
1535
|
audio = b""
|
|
1508
|
-
# 3) Google: uma voz só, mas responde sempre.
|
|
1536
|
+
# 3) Google: uma voz só, mas responde sempre. É a EXCEÇÃO, e vai
|
|
1537
|
+
# marcada: o cabeçalho diz qual motor falou, para o player e o
|
|
1538
|
+
# agente saberem que a voz saiu pela reserva — em vez de a pessoa
|
|
1539
|
+
# descobrir ouvindo.
|
|
1540
|
+
motor_usado = "elevenlabs" if (audio and tipo == "audio/mpeg" and elevenlabs_lib.configurado()) else \
|
|
1541
|
+
("edge" if audio and tipo == "audio/mpeg" else ("piper" if audio and tipo == "audio/wav" else "voicebox" if audio else "google"))
|
|
1509
1542
|
if not audio:
|
|
1510
1543
|
tipo = "audio/mpeg"
|
|
1511
1544
|
try:
|
|
1512
1545
|
audio = b"".join(self._trecho_de_voz(t, idioma) for t in _picar(texto, 190))
|
|
1513
1546
|
except Exception as e: # rede, bloqueio, mudança do serviço
|
|
1514
1547
|
return self.js({"erro": "voz indisponível: %s" % e}, 502)
|
|
1548
|
+
sys.stderr.write("[voz] RESERVA: a narração saiu pelo Google (edge-tts: %s)\n"
|
|
1549
|
+
% (edge_lib.ultimo_erro() or "não instalado"))
|
|
1515
1550
|
if not audio:
|
|
1516
1551
|
return self.js({"erro": "voz não devolveu áudio"}, 502)
|
|
1517
1552
|
|
|
1518
1553
|
self.send_response(200)
|
|
1554
|
+
self.send_header("X-Voz-Motor", motor_usado)
|
|
1519
1555
|
self.send_header("Content-Type", tipo)
|
|
1520
1556
|
self.send_header("Content-Length", str(len(audio)))
|
|
1521
1557
|
self.send_header("Cache-Control", "no-store")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[studio] 2026-09-09T12:28:40+00:00 — iniciando na porta 7797
|
|
2
|
+
|
|
3
|
+
██ PrimoCode Studio
|
|
4
|
+
────────────────────────────────────────────────────────
|
|
5
|
+
Estúdio http://localhost:7797
|
|
6
|
+
Manual da IA http://localhost:7797/api/manual
|
|
7
|
+
Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
|
|
8
|
+
────────────────────────────────────────────────────────
|
|
9
|
+
Ctrl+C para parar
|
|
10
|
+
|
|
11
|
+
GET /api/arquivos
|
|
12
|
+
POST /api/arquivos
|
|
13
|
+
GET /api/arquivos/tela-motion-3ecaa
|
|
14
|
+
GET /api/voz/status
|
|
15
|
+
[studio] 2026-09-09T12:31:01+00:00 — iniciando na porta 7797
|
|
16
|
+
|
|
17
|
+
██ PrimoCode Studio
|
|
18
|
+
────────────────────────────────────────────────────────
|
|
19
|
+
Estúdio http://localhost:7797
|
|
20
|
+
Manual da IA http://localhost:7797/api/manual
|
|
21
|
+
Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
|
|
22
|
+
────────────────────────────────────────────────────────
|
|
23
|
+
Ctrl+C para parar
|
|
24
|
+
|
|
25
|
+
GET /api/arquivos
|
|
26
|
+
POST /api/arquivos
|
|
27
|
+
GET /api/arquivos/tela-motion-d6150
|
|
28
|
+
GET /api/voz/status
|
package/studio/web/js/player.js
CHANGED
|
@@ -188,10 +188,18 @@
|
|
|
188
188
|
"click" | "nenhum") e ganha do automático por transição. */
|
|
189
189
|
if (sfx && tocando && ehVideo && atual !== anterior && doc.sfx !== false) {
|
|
190
190
|
var cenaNova = cenas[atual] || {};
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
var somDaCena = cenaNova.som != null ? cenaNova.som : cenaNova.sfx;
|
|
192
|
+
if (somDaCena !== "nenhum") {
|
|
193
|
+
if (somDaCena) sfx.tocar(somDaCena);
|
|
193
194
|
else sfx.transicao(cenaNova.transicao, atual);
|
|
194
195
|
}
|
|
196
|
+
/* SOM POR ELEMENTO: `som` num elemento toca quando ELE entra — no
|
|
197
|
+
mesmo instante da animação de entrada (o mesmo atraso da cascata
|
|
198
|
+
que o render.js dá ao --atraso). Um número que faz "ding" ao
|
|
199
|
+
aparecer, um ícone que faz "pop", a barra que sobe com o riser: é
|
|
200
|
+
o que separa motion de slide animado. Nome da biblioteca ou
|
|
201
|
+
{arquivo, volume} — o arquivo entra no grafo e vai para o MP4. */
|
|
202
|
+
agendarSonsDosElementos(cenaNova);
|
|
195
203
|
}
|
|
196
204
|
narrar();
|
|
197
205
|
pintarApresentador();
|
|
@@ -507,6 +515,26 @@
|
|
|
507
515
|
|
|
508
516
|
var sfx = null;
|
|
509
517
|
|
|
518
|
+
/* Os sons dos ELEMENTOS da cena que acabou de entrar. O atraso é o mesmo
|
|
519
|
+
que o render.js dá à animação de entrada (cascata: i × 0,09 s + 0,1 s, ou
|
|
520
|
+
o `anim.atraso` declarado), então o som cai no instante em que o
|
|
521
|
+
elemento aparece. Trocar de cena antes cancela o que ainda não tocou —
|
|
522
|
+
senão o "ding" da cena 3 tocava em cima da cena 4. */
|
|
523
|
+
var sonsAgendados = [];
|
|
524
|
+
function agendarSonsDosElementos(cena) {
|
|
525
|
+
sonsAgendados.forEach(clearTimeout);
|
|
526
|
+
sonsAgendados = [];
|
|
527
|
+
if (!sfx || !cena || !Array.isArray(cena.elementos)) return;
|
|
528
|
+
cena.elementos.forEach(function (e, i) {
|
|
529
|
+
if (!e || !e.som) return;
|
|
530
|
+
var atraso = (e.anim && typeof e.anim === "object" && e.anim.atraso != null)
|
|
531
|
+
? +e.anim.atraso
|
|
532
|
+
: (e.chaves && e.chaves.length > 1 ? 0 : i * 0.09 + 0.1);
|
|
533
|
+
if (typeof e.som === "object" && e.som.em != null) atraso = +e.som.em; // segundos na cena, explícito
|
|
534
|
+
sonsAgendados.push(setTimeout(function () { sfx.tocar(e.som); }, Math.max(0, atraso) * 1000));
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
510
538
|
function prepararTrilha() {
|
|
511
539
|
if (typeof criarTrilha !== "function") return;
|
|
512
540
|
var cfg = doc.trilha;
|
package/studio/web/js/render.js
CHANGED
|
@@ -279,8 +279,14 @@ function aplicarChaves(no, e, ctx) {
|
|
|
279
279
|
// espera: "o que eu não toquei, fica como estava". O primeiro valor de
|
|
280
280
|
// cada campo, quando nem a primeira chave o declara, é o neutro (escala e
|
|
281
281
|
// opacidade em 1, deslocamento e giro em 0) — nunca zero para escala.
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
// giroX/giroY/prof são o 3D: viram rotateX/rotateY/translateZ com uma
|
|
283
|
+
// perspectiva na frente. É o que faz um cartão virar de verdade em vez de
|
|
284
|
+
// só encolher — pedido do dono: "quero que a IA faça coisas 3D, mexa com
|
|
285
|
+
// keyframes". escalaX/escalaY esticam num eixo só (a "gota" que se
|
|
286
|
+
// achata ao pousar). `prof`, não `z`: `z` no elemento já é a ordem das
|
|
287
|
+
// camadas (zIndex), e o mesmo nome com dois sentidos é armadilha.
|
|
288
|
+
var NEUTRO = { x: 0, y: 0, escala: 1, escalaX: 1, escalaY: 1, giro: 0, giroX: 0, giroY: 0, prof: 0, opacidade: 1, desfoque: 0 };
|
|
289
|
+
var CAMPOS = ["x", "y", "escala", "escalaX", "escalaY", "giro", "giroX", "giroY", "prof", "opacidade", "desfoque"];
|
|
284
290
|
var herdado = {};
|
|
285
291
|
CAMPOS.forEach(function (c) {
|
|
286
292
|
// semente: primeiro valor declarado em qualquer chave, senão o neutro.
|
|
@@ -301,8 +307,17 @@ function aplicarChaves(no, e, ctx) {
|
|
|
301
307
|
// Assim a chave continua valendo se a pessoa arrastar o elemento no
|
|
302
308
|
// palco depois — o contrário obrigaria a refazer todas as chaves.
|
|
303
309
|
var dx = herdado.x, dy = herdado.y;
|
|
310
|
+
var tem3d = herdado.giroX || herdado.giroY || herdado.prof;
|
|
311
|
+
// A perspectiva vai NA transformação, não no pai: o pai é a cena, e uma
|
|
312
|
+
// perspectiva lá mudaria todos os elementos. Constante em todas as
|
|
313
|
+
// chaves, para o navegador interpolar só o que muda.
|
|
314
|
+
if (tem3d) mov.push("perspective(1200px)");
|
|
304
315
|
if (dx || dy) mov.push("translate(" + dx + "px," + dy + "px)");
|
|
316
|
+
if (herdado.prof) mov.push("translateZ(" + herdado.prof + "px)");
|
|
305
317
|
if (herdado.escala !== 1) mov.push("scale(" + herdado.escala + ")");
|
|
318
|
+
if (herdado.escalaX !== 1 || herdado.escalaY !== 1) mov.push("scale(" + herdado.escalaX + "," + herdado.escalaY + ")");
|
|
319
|
+
if (herdado.giroX) mov.push("rotateX(" + herdado.giroX + "deg)");
|
|
320
|
+
if (herdado.giroY) mov.push("rotateY(" + herdado.giroY + "deg)");
|
|
306
321
|
if (herdado.giro) mov.push("rotate(" + herdado.giro + "deg)");
|
|
307
322
|
|
|
308
323
|
var linhas = ["transform:" + (mov.length ? mov.join(" ") : "none")];
|
|
@@ -328,9 +343,38 @@ function aplicarChaves(no, e, ctx) {
|
|
|
328
343
|
var dur = _numero(ctx.duracao, 4);
|
|
329
344
|
no.style.animationName = nome;
|
|
330
345
|
no.style.animationDuration = dur + "s";
|
|
331
|
-
no.style.animationTimingFunction = e.suavizar
|
|
346
|
+
no.style.animationTimingFunction = curvaDe(e.suavizar);
|
|
332
347
|
no.style.animationFillMode = "both";
|
|
333
348
|
no.style.animationIterationCount = e.repetir ? "infinite" : "1";
|
|
349
|
+
if (e.repetir && e.vaiEVolta) no.style.animationDirection = "alternate";
|
|
350
|
+
if (tem3dEm(chaves)) no.style.transformStyle = "preserve-3d";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function tem3dEm(chaves) {
|
|
354
|
+
for (var i = 0; i < chaves.length; i++) {
|
|
355
|
+
if (chaves[i].giroX || chaves[i].giroY || chaves[i].prof) return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* As curvas com NOME. `suavizar: false` continua sendo linear; o padrão
|
|
361
|
+
continua sendo a curva suave. Os nomes existem porque "elástico" e "salto"
|
|
362
|
+
são o que separa motion de slide animado, e pedir cubic-bezier a quem
|
|
363
|
+
escreve JSON é pedir chute. */
|
|
364
|
+
var CURVAS = {
|
|
365
|
+
suave: "cubic-bezier(.4,0,.2,1)",
|
|
366
|
+
linear: "linear",
|
|
367
|
+
entrada: "cubic-bezier(.4,0,1,1)", // acelera: sai devagar, chega rápido
|
|
368
|
+
saida: "cubic-bezier(0,0,.2,1)", // freia: chega devagar
|
|
369
|
+
elastico: "cubic-bezier(.34,1.56,.64,1)", // passa do ponto e volta
|
|
370
|
+
salto: "cubic-bezier(.68,-.55,.27,1.55)", // recua, dispara, passa e assenta
|
|
371
|
+
degrau: "steps(6, end)", // quadro a quadro, para o "stop motion"
|
|
372
|
+
};
|
|
373
|
+
function curvaDe(suavizar) {
|
|
374
|
+
if (suavizar === false) return CURVAS.linear;
|
|
375
|
+
if (typeof suavizar === "string" && CURVAS[suavizar]) return CURVAS[suavizar];
|
|
376
|
+
if (typeof suavizar === "string" && /^(cubic-bezier|steps)\(/.test(suavizar)) return suavizar;
|
|
377
|
+
return CURVAS.suave;
|
|
334
378
|
}
|
|
335
379
|
|
|
336
380
|
/* ── identidade da marca ─────────────────────────────────────────────────
|
|
@@ -527,6 +571,14 @@ function renderElemento(e, ctx) {
|
|
|
527
571
|
if (e.opacidade != null) no.style.opacity = e.opacidade;
|
|
528
572
|
if (e.sombra) no.style.boxShadow = e.sombra === true ? "0 30px 70px -25px rgba(0,0,0,.6)" : e.sombra;
|
|
529
573
|
if (e.z != null) no.style.zIndex = e.z;
|
|
574
|
+
/* MATERIAL: "vidro" é o liquid glass — translúcido, desfocando o que está
|
|
575
|
+
atrás, com bisel de luz em cima e o reflexo diagonal. Vale em QUALQUER
|
|
576
|
+
elemento (texto, forma, ícone, imagem, lista, cartões), por isso mora
|
|
577
|
+
aqui e não num tipo. O CSS faz o resto por `data-material`. */
|
|
578
|
+
if (e.material) {
|
|
579
|
+
no.dataset.material = e.material;
|
|
580
|
+
if (e.raio != null) no.style.borderRadius = _px(e.raio);
|
|
581
|
+
}
|
|
530
582
|
|
|
531
583
|
/* animação */
|
|
532
584
|
var an = e.anim || ctx.animPadrao;
|
|
@@ -882,7 +934,14 @@ function renderElemento(e, ctx) {
|
|
|
882
934
|
|
|
883
935
|
case "forma": {
|
|
884
936
|
var f = e.forma || "retangulo";
|
|
885
|
-
|
|
937
|
+
// Forma de vidro: o `preenchimento` vira a TINTA sob o vidro (fraca),
|
|
938
|
+
// e o vidro em si é do CSS. Pintar o fundo inteiro aqui cobriria o
|
|
939
|
+
// desfoque — e um vidro opaco é um retângulo.
|
|
940
|
+
if (e.material === "vidro") {
|
|
941
|
+
if (e.preenchimento) no.style.setProperty("--tinta", e.preenchimento);
|
|
942
|
+
} else {
|
|
943
|
+
no.style.background = e.preenchimento || "var(--c-tom)";
|
|
944
|
+
}
|
|
886
945
|
if (e.borda) no.style.border = (e.borda.largura || 2) + "px solid " + (e.borda.cor || "var(--c-linha)");
|
|
887
946
|
if (f === "circulo") no.style.borderRadius = "999px";
|
|
888
947
|
else no.style.borderRadius = _px(e.raio != null ? e.raio : 0);
|
|
@@ -1182,6 +1241,17 @@ function renderElemento(e, ctx) {
|
|
|
1182
1241
|
no.style.width = _px(e.l || t);
|
|
1183
1242
|
no.style.height = _px(e.a || t);
|
|
1184
1243
|
no.style.color = e.cor || "var(--c-tom)";
|
|
1244
|
+
/* ÍCONE COMO OBJETO: `estilo: "app"` é o azulejo de aplicativo —
|
|
1245
|
+
gradiente da cor, brilho em cima, sombra embaixo, o traço branco —
|
|
1246
|
+
e `estilo: "vidro"` é o mesmo azulejo em liquid glass. Pedido do
|
|
1247
|
+
dono ("icon, liquid glass, bem motion"): um ícone de traço sozinho
|
|
1248
|
+
é sinal; num azulejo vira coisa. `fundo` pinta o azulejo; sem ele, a
|
|
1249
|
+
cor do tema. */
|
|
1250
|
+
if (e.estilo === "app" || e.estilo === "vidro") {
|
|
1251
|
+
no.dataset.estilo = e.estilo;
|
|
1252
|
+
no.style.setProperty("--azulejo", e.fundo || e.cor || "var(--c-tom)");
|
|
1253
|
+
if (e.estilo === "app") no.style.color = e.corTraco || "#fff";
|
|
1254
|
+
}
|
|
1185
1255
|
// `caminho`: o ícone desenhado pelo modelo (path SVG 24×24) — ver
|
|
1186
1256
|
// icones.js. Sem ele, um da lista pelo nome.
|
|
1187
1257
|
no.innerHTML = e.caminho
|
package/studio/web/js/sfx.js
CHANGED
|
@@ -85,6 +85,110 @@ function criarSfx(ctx, destino) {
|
|
|
85
85
|
o.start(agora); o.stop(agora + 0.07);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/* ── A biblioteca cresceu porque três sons não fazem motion ───────────
|
|
89
|
+
Pedido do dono: "efeitos sonoros… bem motion mesmo". Whoosh, impacto e
|
|
90
|
+
click cobrem a transição; o que entra num elemento — um número que
|
|
91
|
+
aparece, um ícone que pousa, uma barra que sobe — pedia mais. Todos
|
|
92
|
+
sintetizados, sem arquivo e sem licença, calibrados para ficar SOB a
|
|
93
|
+
narração. */
|
|
94
|
+
|
|
95
|
+
/* Bolha estourando: um seno que sobe rápido e some. Entrada de item. */
|
|
96
|
+
function pop() {
|
|
97
|
+
var agora = ctx.currentTime;
|
|
98
|
+
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
99
|
+
o.type = "sine";
|
|
100
|
+
o.frequency.setValueAtTime(380, agora);
|
|
101
|
+
o.frequency.exponentialRampToValueAtTime(900, agora + 0.06);
|
|
102
|
+
g.gain.setValueAtTime(0.0001, agora);
|
|
103
|
+
g.gain.exponentialRampToValueAtTime(0.16, agora + 0.012);
|
|
104
|
+
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.14);
|
|
105
|
+
o.connect(g); g.connect(saida);
|
|
106
|
+
o.start(agora); o.stop(agora + 0.16);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Sino de notificação: dois parciais (fundamental + quinta acima), cauda
|
|
110
|
+
longa. Confirmação, número final, "pronto". */
|
|
111
|
+
function ding() {
|
|
112
|
+
var agora = ctx.currentTime;
|
|
113
|
+
[[1320, 0.11], [1980, 0.05], [2640, 0.025]].forEach(function (p, i) {
|
|
114
|
+
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
115
|
+
o.type = "sine";
|
|
116
|
+
o.frequency.value = p[0];
|
|
117
|
+
g.gain.setValueAtTime(0.0001, agora);
|
|
118
|
+
g.gain.exponentialRampToValueAtTime(p[1], agora + 0.008);
|
|
119
|
+
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.9 - i * 0.15);
|
|
120
|
+
o.connect(g); g.connect(saida);
|
|
121
|
+
o.start(agora); o.stop(agora + 1.0);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Subida (riser): ruído filtrado que sobe por um segundo e meio e para
|
|
126
|
+
seco — a tensão antes do número grande ou da assinatura. */
|
|
127
|
+
function subida() {
|
|
128
|
+
var agora = ctx.currentTime;
|
|
129
|
+
var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
130
|
+
s.buffer = bufferRuido();
|
|
131
|
+
s.loop = true;
|
|
132
|
+
f.type = "bandpass"; f.Q.value = 2.2;
|
|
133
|
+
f.frequency.setValueAtTime(180, agora);
|
|
134
|
+
f.frequency.exponentialRampToValueAtTime(3800, agora + 1.5);
|
|
135
|
+
g.gain.setValueAtTime(0.0001, agora);
|
|
136
|
+
g.gain.exponentialRampToValueAtTime(0.13, agora + 1.4);
|
|
137
|
+
g.gain.setValueAtTime(0.0001, agora + 1.52);
|
|
138
|
+
s.connect(f); f.connect(g); g.connect(saida);
|
|
139
|
+
s.start(agora); s.stop(agora + 1.6);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Tique de relógio/teclado: madeira seca. Passo de lista, contagem. */
|
|
143
|
+
function tique() {
|
|
144
|
+
var agora = ctx.currentTime;
|
|
145
|
+
var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
146
|
+
s.buffer = bufferRuido();
|
|
147
|
+
f.type = "highpass"; f.frequency.value = 2600;
|
|
148
|
+
g.gain.setValueAtTime(0.14, agora);
|
|
149
|
+
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.04);
|
|
150
|
+
s.connect(f); f.connect(g); g.connect(saida);
|
|
151
|
+
s.start(agora); s.stop(agora + 0.05);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Brilho: arpejo rápido e agudo, o "sparkle" de um destaque ou de uma
|
|
155
|
+
estrela acendendo. */
|
|
156
|
+
function brilho() {
|
|
157
|
+
var agora = ctx.currentTime;
|
|
158
|
+
[1568, 2093, 2637, 3136].forEach(function (hz, i) {
|
|
159
|
+
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
160
|
+
o.type = "triangle";
|
|
161
|
+
o.frequency.value = hz;
|
|
162
|
+
var t = agora + i * 0.045;
|
|
163
|
+
g.gain.setValueAtTime(0.0001, t);
|
|
164
|
+
g.gain.exponentialRampToValueAtTime(0.06, t + 0.01);
|
|
165
|
+
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.35);
|
|
166
|
+
o.connect(g); g.connect(saida);
|
|
167
|
+
o.start(t); o.stop(t + 0.4);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Um ARQUIVO de som (o efeito baixado da internet pelo agente, ou o do
|
|
172
|
+
usuário), tocado dentro do MESMO grafo — é o que faz ele entrar no MP4
|
|
173
|
+
exportado junto da trilha. Decodificado uma vez por URL. */
|
|
174
|
+
var cacheArquivos = {};
|
|
175
|
+
function arquivo(url, volume) {
|
|
176
|
+
if (!url) return;
|
|
177
|
+
var pronto = cacheArquivos[url] || (cacheArquivos[url] = fetch(url)
|
|
178
|
+
.then(function (r) { return r.arrayBuffer(); })
|
|
179
|
+
.then(function (b) { return ctx.decodeAudioData(b); })
|
|
180
|
+
.catch(function () { return null; }));
|
|
181
|
+
pronto.then(function (buffer) {
|
|
182
|
+
if (!buffer) return;
|
|
183
|
+
var s = ctx.createBufferSource(), g = ctx.createGain();
|
|
184
|
+
s.buffer = buffer;
|
|
185
|
+
g.gain.value = volume == null ? 0.7 : Math.max(0, Math.min(1, volume));
|
|
186
|
+
s.connect(g); g.connect(saida);
|
|
187
|
+
s.start(ctx.currentTime);
|
|
188
|
+
});
|
|
189
|
+
return pronto;
|
|
190
|
+
}
|
|
191
|
+
|
|
88
192
|
/* O efeito certo para a ENTRADA de uma cena, pela transição dela.
|
|
89
193
|
flash e zoom pedem peso; corte seco não pede nada. */
|
|
90
194
|
function transicao(tipo, indice) {
|
|
@@ -93,15 +197,37 @@ function criarSfx(ctx, destino) {
|
|
|
93
197
|
whoosh(((indice || 0) * 0.37) % 1);
|
|
94
198
|
}
|
|
95
199
|
|
|
96
|
-
|
|
200
|
+
/* Toca pelo NOME ou por {arquivo, volume}. É o que o player chama para o
|
|
201
|
+
`som` de um elemento ou de uma cena. Nome desconhecido não toca nada —
|
|
202
|
+
e não quebra. */
|
|
203
|
+
function tocar(som) {
|
|
204
|
+
if (!som) return;
|
|
205
|
+
if (typeof som === "string") { if (typeof api[som] === "function" && som !== "tocar") api[som](); return; }
|
|
206
|
+
if (som.arquivo) return arquivo(som.arquivo, som.volume);
|
|
207
|
+
if (som.nome && typeof api[som.nome] === "function") api[som.nome]();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var api = {
|
|
97
211
|
whoosh: whoosh,
|
|
98
212
|
impacto: impacto,
|
|
99
213
|
click: click,
|
|
214
|
+
pop: pop,
|
|
215
|
+
ding: ding,
|
|
216
|
+
subida: subida,
|
|
217
|
+
tique: tique,
|
|
218
|
+
brilho: brilho,
|
|
219
|
+
arquivo: arquivo,
|
|
220
|
+
tocar: tocar,
|
|
100
221
|
transicao: transicao,
|
|
101
222
|
volume: function (v) { saida.gain.value = v; },
|
|
102
223
|
};
|
|
224
|
+
return api;
|
|
103
225
|
}
|
|
104
226
|
|
|
227
|
+
/* Os nomes que o documento pode usar em `som`/`sfx` — lidos pela spec e pelo
|
|
228
|
+
servidor, para recusar nome inventado com a lista certa. */
|
|
229
|
+
var SONS = ["whoosh", "impacto", "click", "pop", "ding", "subida", "tique", "brilho"];
|
|
230
|
+
|
|
105
231
|
if (typeof module !== "undefined" && module.exports) {
|
|
106
|
-
module.exports = { criarSfx: criarSfx };
|
|
232
|
+
module.exports = { criarSfx: criarSfx, SONS: SONS };
|
|
107
233
|
}
|
package/studio/web/render.css
CHANGED
|
@@ -423,6 +423,101 @@
|
|
|
423
423
|
0 18px 44px rgba(0, 0, 0, .34);
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
/* ── MATERIAL: vidro (liquid glass) ─────────────────────────────────────
|
|
427
|
+
Pedido do dono: "liquid glass, icon, bem motion". O que faz o vidro ser
|
|
428
|
+
vidro, e não um retângulo translúcido:
|
|
429
|
+
· DESFOCA o que está atrás (backdrop-filter) e SATURA — o fundo fica
|
|
430
|
+
vivo através dele, como numa lente;
|
|
431
|
+
· o BISEL: uma linha clara em cima (onde a luz bate) e uma escura
|
|
432
|
+
embaixo, dentro da própria caixa;
|
|
433
|
+
· o REFLEXO diagonal (::before), que no vídeo atravessa devagar — é o
|
|
434
|
+
"líquido" do nome;
|
|
435
|
+
· a TINTA opcional (--tinta), fraca, para o vidro ter cor sem ficar opaco.
|
|
436
|
+
Vale em qualquer elemento: texto vira pílula de vidro, forma vira painel,
|
|
437
|
+
ícone vira azulejo, imagem ganha moldura. ─────────────────────────────── */
|
|
438
|
+
.el[data-material="vidro"] {
|
|
439
|
+
position: absolute;
|
|
440
|
+
background:
|
|
441
|
+
linear-gradient(135deg, rgba(255, 255, 255, .26), rgba(255, 255, 255, .07) 38%,
|
|
442
|
+
rgba(255, 255, 255, .02) 60%, rgba(255, 255, 255, .14)),
|
|
443
|
+
color-mix(in srgb, var(--tinta, transparent) 28%, transparent);
|
|
444
|
+
-webkit-backdrop-filter: blur(22px) saturate(1.65);
|
|
445
|
+
backdrop-filter: blur(22px) saturate(1.65);
|
|
446
|
+
border: 1px solid rgba(255, 255, 255, .30);
|
|
447
|
+
border-radius: 28px;
|
|
448
|
+
box-shadow:
|
|
449
|
+
inset 0 1px 0 rgba(255, 255, 255, .60),
|
|
450
|
+
inset 0 -1px 0 rgba(0, 0, 0, .20),
|
|
451
|
+
inset 0 0 0 1px rgba(255, 255, 255, .05),
|
|
452
|
+
0 26px 60px -22px rgba(0, 0, 0, .60);
|
|
453
|
+
overflow: hidden;
|
|
454
|
+
}
|
|
455
|
+
.el[data-material="vidro"]::before {
|
|
456
|
+
content: ""; position: absolute; inset: -40% -30%; pointer-events: none;
|
|
457
|
+
background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, .30) 47%,
|
|
458
|
+
rgba(255, 255, 255, .07) 53%, transparent 60%);
|
|
459
|
+
transform: translateX(-28%);
|
|
460
|
+
}
|
|
461
|
+
.cena.animar .el[data-material="vidro"]::before { animation: brilhoVidro 7s cubic-bezier(.4, 0, .2, 1) infinite; }
|
|
462
|
+
@keyframes brilhoVidro {
|
|
463
|
+
0%, 100% { transform: translateX(-28%); }
|
|
464
|
+
50% { transform: translateX(28%); }
|
|
465
|
+
}
|
|
466
|
+
.el.txt[data-material="vidro"] { padding: .42em .85em; }
|
|
467
|
+
.el.forma[data-material="vidro"] { background-color: transparent; }
|
|
468
|
+
.el.imagem[data-material="vidro"] { padding: 14px; }
|
|
469
|
+
.el.imagem[data-material="vidro"] img { border-radius: calc(28px - 12px); }
|
|
470
|
+
.el.lista[data-material="vidro"], .el.cartoes[data-material="vidro"], .el.numeros[data-material="vidro"] { padding: 4% 5%; }
|
|
471
|
+
/* Tema claro: o vidro escurece um pouco, senão some no branco. */
|
|
472
|
+
[data-tema="claro"] .el[data-material="vidro"], .tema-claro .el[data-material="vidro"] {
|
|
473
|
+
background:
|
|
474
|
+
linear-gradient(135deg, rgba(255, 255, 255, .62), rgba(255, 255, 255, .30) 40%, rgba(255, 255, 255, .55)),
|
|
475
|
+
color-mix(in srgb, var(--tinta, transparent) 22%, transparent);
|
|
476
|
+
border-color: rgba(0, 0, 0, .10);
|
|
477
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9), inset 0 -1px 0 rgba(0, 0, 0, .06), 0 26px 60px -24px rgba(0, 0, 0, .28);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* ── ÍCONE COMO OBJETO: o azulejo de aplicativo ──────────────────────────
|
|
481
|
+
`estilo: "app"`: a cor em gradiente com duas luzes, o brilho na metade de
|
|
482
|
+
cima, a sombra colorida embaixo e o traço branco com sombra própria — é a
|
|
483
|
+
gramática de ícone de app, e o olho lê "produto" em vez de "sinal".
|
|
484
|
+
`estilo: "vidro"`: o mesmo azulejo em liquid glass. ─────────────────── */
|
|
485
|
+
.el.icone[data-estilo="app"], .el.icone[data-estilo="vidro"] {
|
|
486
|
+
position: absolute; border-radius: 23.5%; overflow: hidden;
|
|
487
|
+
}
|
|
488
|
+
/* O traço ocupa 58% do azulejo. NÃO por padding em %: padding em
|
|
489
|
+
porcentagem é relativo à LARGURA DO PAI (a cena, 1920px), e 21% virava
|
|
490
|
+
400px de padding num ícone de 220 — o azulejo engolia o canto da tela e
|
|
491
|
+
o traço sumia. Medido na primeira captura. */
|
|
492
|
+
.el.icone[data-estilo="app"] svg, .el.icone[data-estilo="vidro"] svg { width: 58%; height: 58%; }
|
|
493
|
+
.el.icone[data-estilo="app"] {
|
|
494
|
+
--az: var(--azulejo, var(--c-tom));
|
|
495
|
+
background:
|
|
496
|
+
radial-gradient(130% 110% at 18% 8%, rgba(255, 255, 255, .55), transparent 52%),
|
|
497
|
+
linear-gradient(160deg, color-mix(in srgb, var(--az) 78%, #fff), var(--az) 52%,
|
|
498
|
+
color-mix(in srgb, var(--az) 68%, #000));
|
|
499
|
+
box-shadow:
|
|
500
|
+
inset 0 2px 0 rgba(255, 255, 255, .55),
|
|
501
|
+
inset 0 -3px 0 rgba(0, 0, 0, .28),
|
|
502
|
+
0 22px 44px -16px color-mix(in srgb, var(--az) 70%, #000);
|
|
503
|
+
}
|
|
504
|
+
.el.icone[data-estilo="app"]::after {
|
|
505
|
+
content: ""; position: absolute; left: 5%; right: 5%; top: 3%; height: 47%; pointer-events: none;
|
|
506
|
+
border-radius: 22% 22% 48% 48% / 30% 30% 60% 60%;
|
|
507
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, .34), rgba(255, 255, 255, .02));
|
|
508
|
+
}
|
|
509
|
+
.el.icone[data-estilo="app"] svg { position: relative; filter: drop-shadow(0 4px 8px rgba(0, 0, 0, .35)); }
|
|
510
|
+
.el.icone[data-estilo="vidro"] {
|
|
511
|
+
--tinta: var(--azulejo, var(--c-tom));
|
|
512
|
+
background:
|
|
513
|
+
linear-gradient(135deg, rgba(255, 255, 255, .28), rgba(255, 255, 255, .07) 40%, rgba(255, 255, 255, .16)),
|
|
514
|
+
color-mix(in srgb, var(--tinta) 30%, transparent);
|
|
515
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.6); backdrop-filter: blur(20px) saturate(1.6);
|
|
516
|
+
border: 1px solid rgba(255, 255, 255, .32);
|
|
517
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6), inset 0 -1px 0 rgba(0, 0, 0, .2), 0 24px 50px -20px rgba(0, 0, 0, .55);
|
|
518
|
+
}
|
|
519
|
+
.el.icone[data-estilo="vidro"] svg { position: relative; filter: drop-shadow(0 3px 10px rgba(0, 0, 0, .3)); }
|
|
520
|
+
|
|
426
521
|
/* ── selo ──────────────────────────────────────────────────────────────── */
|
|
427
522
|
.el.selo {
|
|
428
523
|
display: inline-flex; align-items: center; gap: .5em;
|