diff --git a/android/app/assets/carinha.html b/android/app/assets/carinha.html index 09db809..8f2a4de 100644 --- a/android/app/assets/carinha.html +++ b/android/app/assets/carinha.html @@ -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 }; const CIANO_E = [150, 232, 255]; const VERM = [255, 78, 78]; const ROSA = [255, 112, 176]; @@ -404,6 +404,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,11 +419,12 @@ 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; @@ -435,6 +438,24 @@ 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; + 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] - RY * esc * 1.25 - 20 * 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 +915,7 @@ 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; 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; diff --git a/android/app/assets/rosto-motor.js b/android/app/assets/rosto-motor.js index c9ff88b..5137506 100644 --- a/android/app/assets/rosto-motor.js +++ b/android/app/assets/rosto-motor.js @@ -132,7 +132,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,6 +219,13 @@ 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); bctx.restore(); const px = bctx.getImageData(0, 0, colunas, linhas).data; diff --git a/carinha.html b/carinha.html index 09db809..8f2a4de 100644 --- a/carinha.html +++ b/carinha.html @@ -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 }; const CIANO_E = [150, 232, 255]; const VERM = [255, 78, 78]; const ROSA = [255, 112, 176]; @@ -404,6 +404,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,11 +419,12 @@ 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; @@ -435,6 +438,24 @@ 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; + 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] - RY * esc * 1.25 - 20 * 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 +915,7 @@ 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; 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; diff --git a/controle_pc.py b/controle_pc.py index 0a0bf26..fc877c2 100644 --- a/controle_pc.py +++ b/controle_pc.py @@ -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, videoRTC=True, sensorMic=True, sensorCamera=True, micSono=True, cameraSono=False, cameraLado='user', videoResolucao='auto', videoFPS=0, rastrearRosto=True, rastrearMaos=True, seguirRastreamento=True, mostrarRastreamento=True) diff --git a/rosto-motor.js b/rosto-motor.js index c9ff88b..5137506 100644 --- a/rosto-motor.js +++ b/rosto-motor.js @@ -132,7 +132,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,6 +219,13 @@ 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); bctx.restore(); const px = bctx.getImageData(0, 0, colunas, linhas).data; diff --git a/web/src/lib/ajustes.svelte.ts b/web/src/lib/ajustes.svelte.ts index 58c36e0..00c58bc 100644 --- a/web/src/lib/ajustes.svelte.ts +++ b/web/src/lib/ajustes.svelte.ts @@ -46,6 +46,7 @@ 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: '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 }, diff --git a/web/src/telas/Rosto.svelte b/web/src/telas/Rosto.svelte index 8fca3d0..03a1bdb 100644 --- a/web/src/telas/Rosto.svelte +++ b/web/src/telas/Rosto.svelte @@ -5,6 +5,7 @@ } from '../lib/comandos.svelte'; import RostoPrevia from '../lib/componentes/RostoPrevia.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 +25,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 +37,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 +89,10 @@ onchange={(v) => (ouvirAqui = 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)} /> @@ -130,6 +131,21 @@ {/if} +
+

Detalhes do rosto

+
+ salvarApp({ boca: v })} /> + salvarApp({ sobrancelhas: v })} /> + salvarApp({ iris: v })} + /> +
+
+

Cor dos olhos

@@ -356,6 +372,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;