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>
This commit is contained in:
julio
2026-10-06 03:18:17 -03:00
co-authored by Claude Opus 5.5
parent 1029b5fa99
commit 982bf56cd0
7 changed files with 68 additions and 10 deletions
+16 -4
View File
@@ -92,7 +92,7 @@ addEventListener("resize", ajustar);
const PRETO = [6, 8, 14]; const PRETO = [6, 8, 14];
const CIANO = [92, 214, 255]; const CIANO = [92, 214, 255];
const EXTRAS = { boca: true, sobrancelhas: false, iris: false }; const EXTRAS = { boca: true, sobrancelhas: false, iris: false, subirBoca: 0, distSobrancelha: 16 };
const CIANO_E = [150, 232, 255]; const CIANO_E = [150, 232, 255];
const VERM = [255, 78, 78]; const VERM = [255, 78, 78];
const ROSA = [255, 112, 176]; const ROSA = [255, 112, 176];
@@ -150,8 +150,18 @@ function caminhoLinha(x0, y0, x1, y1) {
return "M " + x0 + "," + y0 + " L " + x1 + "," + y1; return "M " + x0 + "," + y0 + " L " + x1 + "," + y1;
} }
/* formas da boca (o humor escolhe qual) */ /* 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) { 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" if (tipo === "aberta") { // surpresa: um "O"
const rx = l * 0.17, ry = l * 0.22; const rx = l * 0.17, ry = l * 0.22;
return caminhoElipse(cx, cy, rx, ry); return caminhoElipse(cx, cy, rx, ry);
@@ -429,7 +439,7 @@ function desenhar() {
const c = i === 0 ? E1 : E2; const c = i === 0 ? E1 : E2;
const lado = i === 0 ? 1 : -1; const lado = i === 0 ? 1 : -1;
const largura = RX * esc * 0.85; 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 inclinacao = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const elevacao = r.esc > 1.1 ? -15 * 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)); brow.setAttribute('d', 'M ' + (c[0]-largura) + ',' + (y-inclinacao+elevacao) + ' Q ' + c[0] + ',' + (y-10*ESC+elevacao) + ' ' + (c[0]+largura) + ',' + (y+inclinacao+elevacao));
@@ -447,7 +457,7 @@ function tracosDoRosto(esc) {
} }
if (EXTRAS.sobrancelhas && !r.dormindo) { if (EXTRAS.sobrancelhas && !r.dormindo) {
[E1, E2].forEach((c, i) => { [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 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 inc = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const ele = r.esc > 1.1 ? -15 * 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 }); 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 });
@@ -916,6 +926,8 @@ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
let configRecebida = null; let configRecebida = null;
function configurarApp(c) { function configurarApp(c) {
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === 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; AUTO = c.auto;
AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume; AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume;
COMPORTAMENTO.ouvir = c.ouvir; COMPORTAMENTO.reagir = c.reagir; COMPORTAMENTO.ouvir = c.ouvir; COMPORTAMENTO.reagir = c.reagir;
+6
View File
@@ -492,6 +492,12 @@
window.MotorRosto = { window.MotorRosto = {
desenhar, retrato, esconder, 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; } }, maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) }; faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
})(); })();
+16 -4
View File
@@ -92,7 +92,7 @@ addEventListener("resize", ajustar);
const PRETO = [6, 8, 14]; const PRETO = [6, 8, 14];
const CIANO = [92, 214, 255]; const CIANO = [92, 214, 255];
const EXTRAS = { boca: true, sobrancelhas: false, iris: false }; const EXTRAS = { boca: true, sobrancelhas: false, iris: false, subirBoca: 0, distSobrancelha: 16 };
const CIANO_E = [150, 232, 255]; const CIANO_E = [150, 232, 255];
const VERM = [255, 78, 78]; const VERM = [255, 78, 78];
const ROSA = [255, 112, 176]; const ROSA = [255, 112, 176];
@@ -150,8 +150,18 @@ function caminhoLinha(x0, y0, x1, y1) {
return "M " + x0 + "," + y0 + " L " + x1 + "," + y1; return "M " + x0 + "," + y0 + " L " + x1 + "," + y1;
} }
/* formas da boca (o humor escolhe qual) */ /* 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) { 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" if (tipo === "aberta") { // surpresa: um "O"
const rx = l * 0.17, ry = l * 0.22; const rx = l * 0.17, ry = l * 0.22;
return caminhoElipse(cx, cy, rx, ry); return caminhoElipse(cx, cy, rx, ry);
@@ -429,7 +439,7 @@ function desenhar() {
const c = i === 0 ? E1 : E2; const c = i === 0 ? E1 : E2;
const lado = i === 0 ? 1 : -1; const lado = i === 0 ? 1 : -1;
const largura = RX * esc * 0.85; 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 inclinacao = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const elevacao = r.esc > 1.1 ? -15 * 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)); brow.setAttribute('d', 'M ' + (c[0]-largura) + ',' + (y-inclinacao+elevacao) + ' Q ' + c[0] + ',' + (y-10*ESC+elevacao) + ' ' + (c[0]+largura) + ',' + (y+inclinacao+elevacao));
@@ -447,7 +457,7 @@ function tracosDoRosto(esc) {
} }
if (EXTRAS.sobrancelhas && !r.dormindo) { if (EXTRAS.sobrancelhas && !r.dormindo) {
[E1, E2].forEach((c, i) => { [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 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 inc = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
const ele = r.esc > 1.1 ? -15 * 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 }); 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 });
@@ -916,6 +926,8 @@ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
let configRecebida = null; let configRecebida = null;
function configurarApp(c) { function configurarApp(c) {
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === 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; AUTO = c.auto;
AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume; AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume;
COMPORTAMENTO.ouvir = c.ouvir; COMPORTAMENTO.reagir = c.reagir; 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, somaltolim=0.55, sompausa=20, somcooldown=5,
movlim=0.05, movforte=0.12, movcooldown=6, movolhar=40, movlim=0.05, movforte=0.12, movcooldown=6, movolhar=40,
economia=True, brilho=0.7, brilhoRepouso=0.08, repousoTela=30, economia=True, brilho=0.7, brilhoRepouso=0.08, repousoTela=30,
boca=True, sobrancelhas=False, iris=False, videoRTC=True, boca=True, sobrancelhas=False, iris=False, subirBoca=0, distSobrancelha=16, videoRTC=True,
sensorMic=True, sensorCamera=True, micSono=True, cameraSono=False, sensorMic=True, sensorCamera=True, micSono=True, cameraSono=False,
cameraLado='user', videoResolucao='auto', videoFPS=0, cameraLado='user', videoResolucao='auto', videoFPS=0,
rastrearRosto=True, rastrearMaos=True, seguirRastreamento=True, mostrarRastreamento=True) 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), LIMITES = dict(volume=(0, 1), somlim=(0, 1), somganho=(0, 1),
somaltolim=(0, 1), sompausa=(1, 3600), somcooldown=(0, 120), somaltolim=(0, 1), sompausa=(1, 3600), somcooldown=(0, 120),
movlim=(0, 1), movforte=(0, 1), movcooldown=(0, 120), movolhar=(0, 100), 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): def validar(dados):
+6
View File
@@ -492,6 +492,12 @@
window.MotorRosto = { window.MotorRosto = {
desenhar, retrato, esconder, 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; } }, maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) }; faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
})(); })();
+2
View File
@@ -47,6 +47,8 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
{ chave: 'boca', rotulo: 'Mostrar boca', tipo: 'toggle' }, { chave: 'boca', rotulo: 'Mostrar boca', tipo: 'toggle' },
{ chave: 'sobrancelhas', rotulo: 'Mostrar sobrancelhas', 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: '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: '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: '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 }, { chave: 'brilho', rotulo: 'Brilho normal', tipo: 'slider', min: 0.01, max: 1, passo: 0.01 },
+19
View File
@@ -4,6 +4,7 @@
cor, CORES, FORMATOS_INFO, formato, gesto, GESTOS_INFO, hex, nomeRosto, rgb, rosto, ROSTOS_INFO, cor, CORES, FORMATOS_INFO, formato, gesto, GESTOS_INFO, hex, nomeRosto, rgb, rosto, ROSTOS_INFO,
} from '../lib/comandos.svelte'; } from '../lib/comandos.svelte';
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte'; import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
import Slider from '../lib/componentes/Slider.svelte';
import Toggle from '../lib/componentes/Toggle.svelte'; import Toggle from '../lib/componentes/Toggle.svelte';
import { salvarApp } from '../lib/ajustes.svelte'; import { salvarApp } from '../lib/ajustes.svelte';
import { estado } from '../lib/estado.svelte'; import { estado } from '../lib/estado.svelte';
@@ -144,6 +145,24 @@
onchange={(v) => salvarApp({ iris: v })} onchange={(v) => salvarApp({ iris: v })}
/> />
</div> </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>
<section class="bloco-escolha" aria-labelledby="t-cor"> <section class="bloco-escolha" aria-labelledby="t-cor">