Compare commits

...
Author SHA1 Message Date
julioandClaude Opus 5.5 2dab45604c feat(rosto): dormindo com olhos fechados e Zzz subindo
Dormindo, os rostos novos mostram os olhos como tracos (-) e tres Z de
tamanhos diferentes saem do canto do olho direito, sobem crescendo e somem
em ciclo. Na Matriz a boca vira '_' (- _ -) e os Z sao maiores, em LEDs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 03:19:58 -03:00
julioandClaude Opus 5.5 982bf56cd0 feat(rosto): sobrancelha perto do olho, boca mais alta e ajustes de posicao
A sobrancelha passa a ser posicionada a partir do topo real do olho de cada
estilo e formato (antes usava a altura do olho classico, alto demais nos
rostos novos). Nos rostos novos a boca sobe um pouco. Novos ajustes
'Subir a boca' e 'Distancia das sobrancelhas' (tela Rosto e Ajustes),
validados no servidor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 03:18:17 -03:00
julioandClaude Opus 5.5 1029b5fa99 feat(rosto): iris, boca e sobrancelhas na Matriz; emocoes da tela Rosto vao ao celular
- opcao "iris" (app config, Ajustes e tela Rosto): anel mais escuro com
  pupila que acompanha o olhar, dilata na alegria e contrai no susto; na
  Matriz vira LEDs apagados
- Matriz desenha boca e sobrancelhas em LEDs (antes a tela de pontos
  cobria o SVG); Bloco e Lente passam a obedecer o ajuste de boca
- tela Rosto: boca, sobrancelhas e iris ali mesmo; as teclas de emocao
  agora vao ao celular por padrao ("testar so na previa" e opcional)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 03:15:39 -03:00
7 changed files with 255 additions and 24 deletions
+42 -6
View File
@@ -92,7 +92,7 @@ addEventListener("resize", ajustar);
const PRETO = [6, 8, 14];
const CIANO = [92, 214, 255];
const EXTRAS = { boca: true, sobrancelhas: false };
const EXTRAS = { boca: true, sobrancelhas: false, iris: false, subirBoca: 0, distSobrancelha: 16 };
const CIANO_E = [150, 232, 255];
const VERM = [255, 78, 78];
const ROSA = [255, 112, 176];
@@ -150,8 +150,18 @@ function caminhoLinha(x0, y0, x1, y1) {
return "M " + x0 + "," + y0 + " L " + x1 + "," + y1;
}
/* formas da boca (o humor escolhe qual) */
function deslocamentoBoca() {
const auto = ROSTO && ROSTO.motor ? 22 : 0; // rostos novos têm olhos mais baixos
return -(auto + (EXTRAS.subirBoca || 0)) * ESC;
}
/* Topo do olho atual (para a sobrancelha ficar perto dele, em qualquer estilo/formato). */
function metadeOlho(esc) {
const m = ROSTO && ROSTO.motor && window.MotorRosto && window.MotorRosto.metadeAltura
? window.MotorRosto.metadeAltura(ROSTO.motor, FORMATO) : 1;
return m * RY * esc;
}
function caminhoBoca(tipo) {
const cx = BOCA[0], cy = BOCA[1], l = BOCA_L, meia = l / 2;
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca(), l = BOCA_L, meia = l / 2;
if (tipo === "aberta") { // surpresa: um "O"
const rx = l * 0.17, ry = l * 0.22;
return caminhoElipse(cx, cy, rx, ry);
@@ -404,6 +414,8 @@ function desenhar() {
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
icone: !r.dormindo && !window.carinhaOlhosDesenho ? window.carinhaOlhosIcone || null : null,
carga: window.carinhaBateria || null,
iris: EXTRAS.iris,
tracos: ROSTO.motor === "matriz" ? tracosDoRosto(esc) : null,
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
parado: !r.kf && !seguindo && !r.dormindo,
olhos: olhos.map(([c, abx, fx, lado]) => ({
@@ -417,16 +429,17 @@ function desenhar() {
olho(i, c[0] + dx * mover * ESC, c[1] + dy * mover * ESC, RX * esc, RY * esc,
abx, fx, r.cor, lado, dx, dy * 0.6);
}
if (ROSTO.motor && !ROSTO.boca) elBoca.style.display = "none";
const matriz = comMotor && ROSTO.motor === "matriz";
if (matriz) elBoca.style.display = "none"; // a Matriz desenha a boca em LEDs
else desenharBoca();
for (let i = 0; i < 2; i++) {
const brow = document.getElementById('sobrancelha' + (i + 1));
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo ? '' : 'none';
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo && !matriz ? '' : 'none';
if (!EXTRAS.sobrancelhas || r.dormindo) continue;
const c = i === 0 ? E1 : E2;
const lado = i === 0 ? 1 : -1;
const largura = RX * esc * 0.85;
const y = c[1] - RY * esc - 20 * ESC;
const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
const inclinacao = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const elevacao = r.esc > 1.1 ? -15 * ESC : 0;
brow.setAttribute('d', 'M ' + (c[0]-largura) + ',' + (y-inclinacao+elevacao) + ' Q ' + c[0] + ',' + (y-10*ESC+elevacao) + ' ' + (c[0]+largura) + ',' + (y+inclinacao+elevacao));
@@ -435,6 +448,27 @@ function desenhar() {
}
}
/* Boca e sobrancelhas como caminhos SVG, para a Matriz acender em LEDs (mesmas formas do SVG). */
function tracosDoRosto(esc) {
const tracos = [];
if (EXTRAS.boca) {
const forma = r.boca;
if (r.dormindo) { // dormindo na Matriz: "– _ –"
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca() + BOCA_L * 0.08, l = BOCA_L * 0.32;
tracos.push({ d: 'M ' + (cx - l) + ',' + cy + ' L ' + (cx + l) + ',' + cy, largura: BOCA_L * 0.16 });
} else tracos.push({ d: caminhoBoca(forma), largura: BOCA_L * 0.18, cheio: forma === "aberta" });
}
if (EXTRAS.sobrancelhas && !r.dormindo) {
[E1, E2].forEach((c, i) => {
const lado = i === 0 ? 1 : -1, largura = RX * esc * 0.85, y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
const inc = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const ele = r.esc > 1.1 ? -15 * ESC : 0;
tracos.push({ d: 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele), largura: 9 * ESC });
});
}
return tracos;
}
// ------------------------------------------------------------------ controle
/* Troca de rosto (biblioteca acima). Vale na hora, sem recarregar a pagina. */
function trocarRosto(nome) {
@@ -894,7 +928,9 @@ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
let configRecebida = null;
function configurarApp(c) {
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true;
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true;
if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca;
if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha;
AUTO = c.auto;
AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume;
COMPORTAMENTO.ouvir = c.ouvir; COMPORTAMENTO.reagir = c.reagir;
+58 -1
View File
@@ -118,6 +118,11 @@
const h = est.h * p.ry * respira * abre * (1 - amassa * 0.6);
const x = p.cx - w / 2, y = p.cy - h / 2;
c.fillStyle = css(o.cor);
if (o.dormindo && !temConteudo) { // dormindo: "–" (olho fechado)
const esp = Math.max(2, p.ry * 0.2);
caixa(c, p.cx - w * 0.5, p.cy + h * 0.08 - esp / 2, w, esp, esp / 2); c.fill();
return;
}
if (e.coracao > 0.5) { coracao(c, p.cx, p.cy, Math.min(w, h) * 0.55 * respira); return; }
caixa(c, x, y, w, h, est.r * p.ry * respira);
c.fill();
@@ -132,7 +137,21 @@
c.beginPath(); c.ellipse(p.cx, y + h * (1.62 - e.bot), w * 0.75, h * 0.62, 0, 0, Math.PI * 2); c.fill();
c.globalCompositeOperation = 'source-over';
if (temConteudo && fecha < 0.6) conteudo(c, o, p.cx, p.cy, Math.min(w, h) * 0.74);
else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
else if (o.iris && fecha < 0.6 && e.bot < 0.35) {
const R = Math.min(w, h) * 0.3;
const ix = Math.max(x + R * 1.1, Math.min(x + w - R * 1.1, p.cx + (p.lx + sacada.x) * w * 0.016));
const iy = Math.max(y + R * 1.1, Math.min(y + h - R * 1.1, p.cy + h * 0.04 + (p.ly + sacada.y) * h * 0.016));
c.fillStyle = css(o.cor.map((v) => v * 0.42));
c.beginPath(); c.arc(ix, iy, R, 0, Math.PI * 2); c.fill();
c.globalCompositeOperation = 'destination-out';
const pupila = p.ab > 1.05 ? 0.38 : e.bot > 0.2 ? 0.62 : 0.5; // susto contrai, alegria dilata
c.beginPath(); c.arc(ix, iy, R * pupila, 0, Math.PI * 2); c.fill();
c.globalCompositeOperation = 'source-over';
if (est.brilho) {
c.fillStyle = 'rgba(245,250,255,0.9)';
c.beginPath(); c.arc(ix - R * 0.42, iy - R * 0.42, R * 0.2, 0, Math.PI * 2); c.fill();
}
} else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
const hx = Math.max(x + w * 0.18, Math.min(x + w * 0.62, x + w * 0.3 + (p.lx + sacada.x) * w * 0.012));
const hy = Math.max(y + h * 0.18, Math.min(y + h * 0.5, y + h * 0.26 + (p.ly + sacada.y) * h * 0.012));
c.fillStyle = 'rgba(245,250,255,0.9)';
@@ -205,7 +224,15 @@
bctx.clearRect(0, 0, colunas, linhas);
bctx.save(); bctx.scale(f, f);
olhos.forEach((p, i) => olhoPlano(bctx, o, p, estilo, estado[i]));
for (const traco of o.tracos || []) {
if (typeof traco.d !== 'string') continue;
const caminho = new Path2D(traco.d);
bctx.strokeStyle = bctx.fillStyle = css(o.cor);
bctx.lineWidth = traco.largura; bctx.lineCap = 'round'; bctx.lineJoin = 'round';
if (traco.cheio) bctx.fill(caminho); else bctx.stroke(caminho);
}
desenharMaos(bctx, o, olhos, tela.height / d);
desenharZzz(bctx, o, olhos, 1.6); // em LEDs o Z precisa ser maior para ler
bctx.restore();
const px = bctx.getImageData(0, 0, colunas, linhas).data;
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, tela.width, tela.height);
@@ -245,10 +272,12 @@
} else if (estilo.lente) {
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
desenharMaos(ctx, o, olhos, tela.height);
desenharZzz(ctx, o, olhos);
} else {
cctx.clearRect(0, 0, camada.width, camada.height);
olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i]));
desenharMaos(cctx, o, olhos, tela.height);
desenharZzz(cctx, o, olhos);
ctx.save();
if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); }
ctx.drawImage(camada, 0, 0);
@@ -367,6 +396,28 @@
}
}
// ------------------------------------------------------------------ Zzz
function desenharZzz(c, o, olhos, fator) {
fator = fator || 1;
if (!o.dormindo) return;
const d = olhos.slice().sort((a, b) => a.cx - b.cx)[1]; // olho da direita na tela
const x0 = d.cx + d.ry * 0.7, y0 = d.cy - d.ry * 0.55;
c.save();
c.strokeStyle = css(o.cor); c.lineCap = 'round'; c.lineJoin = 'round';
for (let i = 0; i < 3; i++) {
const k = ((o.t || 0) * 0.32 + i / 3) % 1; // cada Z num ponto do ciclo
const tam = d.ry * (0.28 + 0.3 * k) * fator;
const x = x0 + d.ry * 0.7 * k + Math.sin(k * Math.PI * 2) * d.ry * 0.08, y = y0 - d.ry * 1.7 * k;
c.globalAlpha = Math.min(1, k / 0.15) * (1 - k);
c.lineWidth = Math.max(1.5, tam * 0.2);
c.beginPath();
c.moveTo(x - tam / 2, y - tam / 2); c.lineTo(x + tam / 2, y - tam / 2);
c.lineTo(x - tam / 2, y + tam / 2); c.lineTo(x + tam / 2, y + tam / 2);
c.stroke();
}
c.restore();
}
// --------------------------------------------------------- faixa de baixo
// A parte de baixo da tela vira palco: legenda da conversa, pensando, equalizador e carga.
const faixa = { modo: null, texto: '', t0: null, dur: 0 };
@@ -471,6 +522,12 @@
window.MotorRosto = {
desenhar, retrato, esconder,
/** Metade da altura do olho (em frações de RY) para o estilo/formato: posiciona as sobrancelhas. */
metadeAltura: (estilo, formato) => {
const base = ESTILOS[estilo]; if (!base) return 1;
if (base.lente) return base.w * 0.5 * 1.12;
return ((!base.lente && FORMATOS[formato]) || base).h / 2;
},
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
})();
+42 -6
View File
@@ -92,7 +92,7 @@ addEventListener("resize", ajustar);
const PRETO = [6, 8, 14];
const CIANO = [92, 214, 255];
const EXTRAS = { boca: true, sobrancelhas: false };
const EXTRAS = { boca: true, sobrancelhas: false, iris: false, subirBoca: 0, distSobrancelha: 16 };
const CIANO_E = [150, 232, 255];
const VERM = [255, 78, 78];
const ROSA = [255, 112, 176];
@@ -150,8 +150,18 @@ function caminhoLinha(x0, y0, x1, y1) {
return "M " + x0 + "," + y0 + " L " + x1 + "," + y1;
}
/* formas da boca (o humor escolhe qual) */
function deslocamentoBoca() {
const auto = ROSTO && ROSTO.motor ? 22 : 0; // rostos novos têm olhos mais baixos
return -(auto + (EXTRAS.subirBoca || 0)) * ESC;
}
/* Topo do olho atual (para a sobrancelha ficar perto dele, em qualquer estilo/formato). */
function metadeOlho(esc) {
const m = ROSTO && ROSTO.motor && window.MotorRosto && window.MotorRosto.metadeAltura
? window.MotorRosto.metadeAltura(ROSTO.motor, FORMATO) : 1;
return m * RY * esc;
}
function caminhoBoca(tipo) {
const cx = BOCA[0], cy = BOCA[1], l = BOCA_L, meia = l / 2;
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca(), l = BOCA_L, meia = l / 2;
if (tipo === "aberta") { // surpresa: um "O"
const rx = l * 0.17, ry = l * 0.22;
return caminhoElipse(cx, cy, rx, ry);
@@ -404,6 +414,8 @@ function desenhar() {
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
icone: !r.dormindo && !window.carinhaOlhosDesenho ? window.carinhaOlhosIcone || null : null,
carga: window.carinhaBateria || null,
iris: EXTRAS.iris,
tracos: ROSTO.motor === "matriz" ? tracosDoRosto(esc) : null,
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
parado: !r.kf && !seguindo && !r.dormindo,
olhos: olhos.map(([c, abx, fx, lado]) => ({
@@ -417,16 +429,17 @@ function desenhar() {
olho(i, c[0] + dx * mover * ESC, c[1] + dy * mover * ESC, RX * esc, RY * esc,
abx, fx, r.cor, lado, dx, dy * 0.6);
}
if (ROSTO.motor && !ROSTO.boca) elBoca.style.display = "none";
const matriz = comMotor && ROSTO.motor === "matriz";
if (matriz) elBoca.style.display = "none"; // a Matriz desenha a boca em LEDs
else desenharBoca();
for (let i = 0; i < 2; i++) {
const brow = document.getElementById('sobrancelha' + (i + 1));
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo ? '' : 'none';
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo && !matriz ? '' : 'none';
if (!EXTRAS.sobrancelhas || r.dormindo) continue;
const c = i === 0 ? E1 : E2;
const lado = i === 0 ? 1 : -1;
const largura = RX * esc * 0.85;
const y = c[1] - RY * esc - 20 * ESC;
const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
const inclinacao = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const elevacao = r.esc > 1.1 ? -15 * ESC : 0;
brow.setAttribute('d', 'M ' + (c[0]-largura) + ',' + (y-inclinacao+elevacao) + ' Q ' + c[0] + ',' + (y-10*ESC+elevacao) + ' ' + (c[0]+largura) + ',' + (y+inclinacao+elevacao));
@@ -435,6 +448,27 @@ function desenhar() {
}
}
/* Boca e sobrancelhas como caminhos SVG, para a Matriz acender em LEDs (mesmas formas do SVG). */
function tracosDoRosto(esc) {
const tracos = [];
if (EXTRAS.boca) {
const forma = r.boca;
if (r.dormindo) { // dormindo na Matriz: "– _ –"
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca() + BOCA_L * 0.08, l = BOCA_L * 0.32;
tracos.push({ d: 'M ' + (cx - l) + ',' + cy + ' L ' + (cx + l) + ',' + cy, largura: BOCA_L * 0.16 });
} else tracos.push({ d: caminhoBoca(forma), largura: BOCA_L * 0.18, cheio: forma === "aberta" });
}
if (EXTRAS.sobrancelhas && !r.dormindo) {
[E1, E2].forEach((c, i) => {
const lado = i === 0 ? 1 : -1, largura = RX * esc * 0.85, y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
const inc = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const ele = r.esc > 1.1 ? -15 * ESC : 0;
tracos.push({ d: 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele), largura: 9 * ESC });
});
}
return tracos;
}
// ------------------------------------------------------------------ controle
/* Troca de rosto (biblioteca acima). Vale na hora, sem recarregar a pagina. */
function trocarRosto(nome) {
@@ -894,7 +928,9 @@ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
let configRecebida = null;
function configurarApp(c) {
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true;
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true;
if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca;
if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha;
AUTO = c.auto;
AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume;
COMPORTAMENTO.ouvir = c.ouvir; COMPORTAMENTO.reagir = c.reagir;
+3 -2
View File
@@ -20,7 +20,7 @@ PADRAO = dict(preview=False, auto=True, audio=True, volume=0.35,
somaltolim=0.55, sompausa=20, somcooldown=5,
movlim=0.05, movforte=0.12, movcooldown=6, movolhar=40,
economia=True, brilho=0.7, brilhoRepouso=0.08, repousoTela=30,
boca=True, sobrancelhas=False, videoRTC=True,
boca=True, sobrancelhas=False, iris=False, subirBoca=0, distSobrancelha=16, videoRTC=True,
sensorMic=True, sensorCamera=True, micSono=True, cameraSono=False,
cameraLado='user', videoResolucao='auto', videoFPS=0,
rastrearRosto=True, rastrearMaos=True, seguirRastreamento=True, mostrarRastreamento=True)
@@ -30,7 +30,8 @@ ESCOLHAS = dict(cameraLado=('user', 'environment'),
LIMITES = dict(volume=(0, 1), somlim=(0, 1), somganho=(0, 1),
somaltolim=(0, 1), sompausa=(1, 3600), somcooldown=(0, 120),
movlim=(0, 1), movforte=(0, 1), movcooldown=(0, 120), movolhar=(0, 100),
brilho=(0.01, 1), brilhoRepouso=(0.01, 1), repousoTela=(5, 3600))
brilho=(0.01, 1), brilhoRepouso=(0.01, 1), repousoTela=(5, 3600),
subirBoca=(-20, 60), distSobrancelha=(0, 60))
def validar(dados):
+58 -1
View File
@@ -118,6 +118,11 @@
const h = est.h * p.ry * respira * abre * (1 - amassa * 0.6);
const x = p.cx - w / 2, y = p.cy - h / 2;
c.fillStyle = css(o.cor);
if (o.dormindo && !temConteudo) { // dormindo: "–" (olho fechado)
const esp = Math.max(2, p.ry * 0.2);
caixa(c, p.cx - w * 0.5, p.cy + h * 0.08 - esp / 2, w, esp, esp / 2); c.fill();
return;
}
if (e.coracao > 0.5) { coracao(c, p.cx, p.cy, Math.min(w, h) * 0.55 * respira); return; }
caixa(c, x, y, w, h, est.r * p.ry * respira);
c.fill();
@@ -132,7 +137,21 @@
c.beginPath(); c.ellipse(p.cx, y + h * (1.62 - e.bot), w * 0.75, h * 0.62, 0, 0, Math.PI * 2); c.fill();
c.globalCompositeOperation = 'source-over';
if (temConteudo && fecha < 0.6) conteudo(c, o, p.cx, p.cy, Math.min(w, h) * 0.74);
else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
else if (o.iris && fecha < 0.6 && e.bot < 0.35) {
const R = Math.min(w, h) * 0.3;
const ix = Math.max(x + R * 1.1, Math.min(x + w - R * 1.1, p.cx + (p.lx + sacada.x) * w * 0.016));
const iy = Math.max(y + R * 1.1, Math.min(y + h - R * 1.1, p.cy + h * 0.04 + (p.ly + sacada.y) * h * 0.016));
c.fillStyle = css(o.cor.map((v) => v * 0.42));
c.beginPath(); c.arc(ix, iy, R, 0, Math.PI * 2); c.fill();
c.globalCompositeOperation = 'destination-out';
const pupila = p.ab > 1.05 ? 0.38 : e.bot > 0.2 ? 0.62 : 0.5; // susto contrai, alegria dilata
c.beginPath(); c.arc(ix, iy, R * pupila, 0, Math.PI * 2); c.fill();
c.globalCompositeOperation = 'source-over';
if (est.brilho) {
c.fillStyle = 'rgba(245,250,255,0.9)';
c.beginPath(); c.arc(ix - R * 0.42, iy - R * 0.42, R * 0.2, 0, Math.PI * 2); c.fill();
}
} else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
const hx = Math.max(x + w * 0.18, Math.min(x + w * 0.62, x + w * 0.3 + (p.lx + sacada.x) * w * 0.012));
const hy = Math.max(y + h * 0.18, Math.min(y + h * 0.5, y + h * 0.26 + (p.ly + sacada.y) * h * 0.012));
c.fillStyle = 'rgba(245,250,255,0.9)';
@@ -205,7 +224,15 @@
bctx.clearRect(0, 0, colunas, linhas);
bctx.save(); bctx.scale(f, f);
olhos.forEach((p, i) => olhoPlano(bctx, o, p, estilo, estado[i]));
for (const traco of o.tracos || []) {
if (typeof traco.d !== 'string') continue;
const caminho = new Path2D(traco.d);
bctx.strokeStyle = bctx.fillStyle = css(o.cor);
bctx.lineWidth = traco.largura; bctx.lineCap = 'round'; bctx.lineJoin = 'round';
if (traco.cheio) bctx.fill(caminho); else bctx.stroke(caminho);
}
desenharMaos(bctx, o, olhos, tela.height / d);
desenharZzz(bctx, o, olhos, 1.6); // em LEDs o Z precisa ser maior para ler
bctx.restore();
const px = bctx.getImageData(0, 0, colunas, linhas).data;
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, tela.width, tela.height);
@@ -245,10 +272,12 @@
} else if (estilo.lente) {
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
desenharMaos(ctx, o, olhos, tela.height);
desenharZzz(ctx, o, olhos);
} else {
cctx.clearRect(0, 0, camada.width, camada.height);
olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i]));
desenharMaos(cctx, o, olhos, tela.height);
desenharZzz(cctx, o, olhos);
ctx.save();
if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); }
ctx.drawImage(camada, 0, 0);
@@ -367,6 +396,28 @@
}
}
// ------------------------------------------------------------------ Zzz
function desenharZzz(c, o, olhos, fator) {
fator = fator || 1;
if (!o.dormindo) return;
const d = olhos.slice().sort((a, b) => a.cx - b.cx)[1]; // olho da direita na tela
const x0 = d.cx + d.ry * 0.7, y0 = d.cy - d.ry * 0.55;
c.save();
c.strokeStyle = css(o.cor); c.lineCap = 'round'; c.lineJoin = 'round';
for (let i = 0; i < 3; i++) {
const k = ((o.t || 0) * 0.32 + i / 3) % 1; // cada Z num ponto do ciclo
const tam = d.ry * (0.28 + 0.3 * k) * fator;
const x = x0 + d.ry * 0.7 * k + Math.sin(k * Math.PI * 2) * d.ry * 0.08, y = y0 - d.ry * 1.7 * k;
c.globalAlpha = Math.min(1, k / 0.15) * (1 - k);
c.lineWidth = Math.max(1.5, tam * 0.2);
c.beginPath();
c.moveTo(x - tam / 2, y - tam / 2); c.lineTo(x + tam / 2, y - tam / 2);
c.lineTo(x - tam / 2, y + tam / 2); c.lineTo(x + tam / 2, y + tam / 2);
c.stroke();
}
c.restore();
}
// --------------------------------------------------------- faixa de baixo
// A parte de baixo da tela vira palco: legenda da conversa, pensando, equalizador e carga.
const faixa = { modo: null, texto: '', t0: null, dur: 0 };
@@ -471,6 +522,12 @@
window.MotorRosto = {
desenhar, retrato, esconder,
/** Metade da altura do olho (em frações de RY) para o estilo/formato: posiciona as sobrancelhas. */
metadeAltura: (estilo, formato) => {
const base = ESTILOS[estilo]; if (!base) return 1;
if (base.lente) return base.w * 0.5 * 1.12;
return ((!base.lente && FORMATOS[formato]) || base).h / 2;
},
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
})();
+3
View File
@@ -46,6 +46,9 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
{ chave: 'auto', rotulo: 'Gestos espontâneos', ajuda: 'Pisca e olha em volta sem comando', tipo: 'toggle' },
{ chave: 'boca', rotulo: 'Mostrar boca', tipo: 'toggle' },
{ chave: 'sobrancelhas', rotulo: 'Mostrar sobrancelhas', tipo: 'toggle' },
{ chave: 'iris', rotulo: 'Mostrar íris', ajuda: 'Pupila que acompanha o olhar (Viva, Bloco e Matriz)', tipo: 'toggle' },
{ chave: 'subirBoca', rotulo: 'Subir a boca', tipo: 'slider', min: -20, max: 60, passo: 2 },
{ chave: 'distSobrancelha', rotulo: 'Distância das sobrancelhas', tipo: 'slider', min: 0, max: 60, passo: 2 },
{ chave: 'audio', rotulo: 'Sons no celular', ajuda: 'Bipes do próprio robô', tipo: 'toggle' },
{ chave: 'volume', rotulo: 'Volume dos sons', tipo: 'slider', min: 0, max: 1, passo: 0.05 },
{ chave: 'brilho', rotulo: 'Brilho normal', tipo: 'slider', min: 0.01, max: 1, passo: 0.01 },
+49 -8
View File
@@ -4,7 +4,9 @@
cor, CORES, FORMATOS_INFO, formato, gesto, GESTOS_INFO, hex, nomeRosto, rgb, rosto, ROSTOS_INFO,
} from '../lib/comandos.svelte';
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
import Slider from '../lib/componentes/Slider.svelte';
import Toggle from '../lib/componentes/Toggle.svelte';
import { salvarApp } from '../lib/ajustes.svelte';
import { estado } from '../lib/estado.svelte';
let previa = $state<{ ordem: (t: string) => void; legenda: (t: string) => void }>();
@@ -24,8 +26,8 @@
const c = voz();
if (c) (window as any).VozRobo.balbuciar(c, FRASE, 0.5);
}
// Por padrão as emoções só aparecem aqui na prévia; ligado, vão também para o celular.
let noCelular = $state(false);
// As emoções vão para o celular (e a prévia mostra junto); "só na prévia" serve para testar quieto.
let soPrevia = $state(false);
const atual = $derived(estado.aparencia.rosto);
const formatoAtual = $derived(estado.aparencia.formato ?? 'padrao');
@@ -36,8 +38,8 @@
const EMOCOES = ['feliz', 'coracao', 'surpreso', 'triste', 'bravo', 'piscadinha', 'acenar', 'esconder', 'espiar', 'pensar', 'musica', 'dormir', 'acordar'];
function emocao(g: string) {
if (noCelular) gesto(g);
else previa?.ordem(g);
if (soPrevia) previa?.ordem(g);
else gesto(g);
const c = voz();
if (c) (window as any).VozRobo.tocar(c, g, 0.5);
}
@@ -88,10 +90,10 @@
onchange={(v) => (ouvirAqui = v)}
/>
<Toggle
rotulo="Mandar as emoções também para o celular"
ajuda="Desligado, elas aparecem só na prévia"
checked={noCelular}
onchange={(v) => (noCelular = v)}
rotulo="Testar só na prévia, sem mandar ao celular"
ajuda="Desligado, cada emoção aparece no celular na hora"
checked={soPrevia}
onchange={(v) => (soPrevia = v)}
/>
</section>
@@ -130,6 +132,39 @@
{/if}
</section>
<section class="bloco-escolha" aria-labelledby="t-detalhes">
<h2 id="t-detalhes">Detalhes do rosto</h2>
<div class="detalhes">
<Toggle rotulo="Boca" checked={estado.appConfig?.boca !== false} disabled={!estado.appConfig} onchange={(v) => salvarApp({ boca: v })} />
<Toggle rotulo="Sobrancelhas" checked={!!estado.appConfig?.sobrancelhas} disabled={!estado.appConfig} onchange={(v) => salvarApp({ sobrancelhas: v })} />
<Toggle
rotulo="Íris"
ajuda={aceitaFormato ? 'Pupila que acompanha o olhar' : 'Vale para Viva, Bloco e Matriz'}
checked={!!estado.appConfig?.iris}
disabled={!estado.appConfig}
onchange={(v) => salvarApp({ iris: v })}
/>
</div>
<div class="detalhes">
<Slider
rotulo="Subir a boca"
valor={Number(estado.appConfig?.subirBoca ?? 0)}
min={-20}
max={60}
passo={2}
onchange={(v) => salvarApp({ subirBoca: v })}
/>
<Slider
rotulo="Distância das sobrancelhas"
valor={Number(estado.appConfig?.distSobrancelha ?? 16)}
min={0}
max={60}
passo={2}
onchange={(v) => salvarApp({ distSobrancelha: v })}
/>
</div>
</section>
<section class="bloco-escolha" aria-labelledby="t-cor">
<h2 id="t-cor">Cor dos olhos</h2>
<div class="cores">
@@ -356,6 +391,12 @@
height: 18px;
border-radius: 6px;
}
.detalhes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
gap: var(--e-2) var(--e-6);
max-width: 780px;
}
.cores {
display: flex;
flex-wrap: wrap;