fix(rosto): parado mais humano; tonto com espirais e bocejo com a mão

Parado ele vivia escondendo o rosto: a IA local (Qwen 0.6B) reagia a cada
som ou "sozinho" a cada 20-60 s e repetia 'esconder' apesar do "varie".

- IA local: cada evento só oferece gestos que combinam (barulho: susto ou
  olhar; movimento: olhar e acenar), os dois últimos ficam fora do enum,
  no máximo a cada 45 s, não interrompe atividade e, com o modo vida
  ligado, não reage a "sozinho".
- Sem ninguém olhando (sem movimento/PIR há 3 min) nada de gesto "para
  alguém" (acenar, tímido, esconder...): ele vive a vida dele e começa
  atividades com mais frequência.
- Gestos soltos no celular: quase sempre sutis (olhar de lado, piscar
  devagar), gesto grande raro e nunca repetido em seguida.
- Olhar em repouso com fixações e sacadas em vez do vaivém contínuo.
- Tonto: espirais girando no lugar dos olhos (todos os rostos, inclusive
  Matriz) e boca de "ó". Bocejo: mão na frente da boca com tapinhas e
  Zzz subindo. As mãos agora aparecem também quando o gesto vem sozinho.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 21:38:32 -03:00
co-authored by Claude Opus 5.5
parent e73897d9c1
commit ff06f02705
10 changed files with 235 additions and 33 deletions
+56 -10
View File
@@ -279,6 +279,13 @@ function olho(indice, cx, cy, rx, ry, ab, forma, cor, lado, lx, ly) {
if(p.desenho!==desenho){if(p.figurinha)p.figurinha.remove();p.figurinha=window.carinhaConstruirSVG(desenho);p.desenho=desenho;if(p.figurinha)document.getElementById('o'+(indice+1)).append(p.figurinha);}
if(p.figurinha){p.figurinha.style.display='';p.figurinha.setAttribute('x',cx-rx*.65);p.figurinha.setAttribute('y',cy-ry*.65);p.figurinha.setAttribute('width',rx*1.3);p.figurinha.setAttribute('height',ry*1.3);}
}
if (espiralAtiva()) {
p.traco.setAttribute("d", caminhoEspiral(cx, cy, Math.min(rx, ry) * 0.95, lado));
p.traco.setAttribute("stroke-width", rx * 0.17);
p.traco.setAttribute("stroke", rgb(cor));
p.traco.style.display = "";
return;
}
if (icone) {
forma='aberto';p.icone.textContent=window.carinhaOlhosIcone||'';
p.icone.setAttribute('x',cx);p.icone.setAttribute('y',cy);
@@ -372,12 +379,12 @@ function sequencias() {
const pula = (n, alto, baixo, d) => Array.from({ length: n }, (_, i) => [{ dy: i % 2 ? baixo : alto }, d]);
const rir = [[{ forma: "feliz", boca: "aberta", esc: 1.08, dy: -4 }, 0.12], ...pula(8, -6, 2, 0.1),
[{ forma: "feliz", boca: "sorriso", dy: 0 }, 0.25], [{ forma: "aberto", boca: "neutra", esc: 1.0 }, 0.3]];
const bocejar = [[{ ab: 0.5, boca: "aberta", dy: -4, esc: 1.05 }, 0.5], [{ ab: 0.3, dy: -5 }, 1.0],
const bocejar = [[{ ab: 0.5, boca: "aberta", dy: -4, esc: 1.05 }, 0.5], [{ ab: 0.25, dy: -5 }, 1.0],
[{ ab: 0.0, boca: "neutra", dy: 2, esc: 1.0 }, 0.25], [{ ab: 1.0, dy: 0 }, 0.35]];
const confuso = [[{ ab2: 0.55, dx: -8, esc: 0.98 }, 0.25], [{ dx: 8 }, 0.45], [{ dx: -6 }, 0.4], [{}, 0.4],
[{ ab2: 1.0, dx: 0, esc: 1.0 }, 0.3]];
const roda = Array.from({ length: 16 }, (_, i) => [{ dx: 14 * Math.cos(i * Math.PI / 4), dy: 9 * Math.sin(i * Math.PI / 4) }, 0.1]);
const tonto = [[{ esc: 0.94, boca: "aberta" }, 0.15], ...roda, [{ dx: 0, dy: 0, esc: 1.0, boca: "neutra" }, 0.35]];
const balanca = Array.from({ length: 8 }, (_, i) => [{ dx: i % 2 ? -5 : 5, dy: i % 2 ? 2 : -2 }, 0.2]);
const tonto = [[{ esc: 1.05, boca: "aberta" }, 0.15], ...balanca, [{ dx: 0, dy: 0, esc: 1.0, boca: "neutra" }, 0.35]];
const timido = [[{ forma: "feliz", boca: "sorriso", dx: -14, dy: 9, esc: 0.9, cor: ROSA }, 0.35], [{}, 1.2],
[{ dx: 0, dy: 0, esc: 1.0, forma: "aberto", cor: CIANO }, 0.4]];
const comemorar = [[{ forma: "feliz", boca: "sorriso", cor: AMARELO, dy: -10, esc: 1.15 }, 0.15], [{ dy: 2, cor: ROSA }, 0.15],
@@ -458,9 +465,24 @@ function atualizarFeicao(s) {
for (const c of ["curva", "lado", "abre", "dx"]) a[c] += (b[c] - a[c]) * k;
for (let i = 0; i < 2; i++) { a.sob[i] += (b.sob[i] - a.sob[i]) * k; a.inc[i] += (b.inc[i] - a.inc[i]) * k; }
}
/* Gestos soltos mais humanos: quase sempre coisas sutis (olhar de lado, piscar devagar, olhar para
baixo); de vez em quando um gesto maior do humor, nunca o mesmo duas vezes seguidas. Gestos
"para alguém" (acenar, tímido, piscadinha...) só quando tem gente por perto. */
const SUTIS = ["olhar_esq", "olhar_dir", "olhar_baixo", "piscar2", "olhar_esq", "olhar_dir"];
const PARA_ALGUEM = ["acenar", "coracao", "piscadinha", "timido", "esconder", "espiar", "surpreso"];
const RECENTES = [];
function alguemOlhando() {
const agora = performance.now() / 1000;
return (MOV.ativo && agora - MOV.ultimoMov < 30) || (TRACK.alvo && agora - TRACK.visto < 30);
}
function gestoSolto() {
const lista = LIVRES_HUMOR[HUMOR] || LIVRES;
return lista[Math.floor(Math.random() * lista.length)];
const grande = Math.random() < ({ feliz: 0.3, entediado: 0.25, bravo: 0.3 }[HUMOR] || 0.18);
let lista = grande ? (LIVRES_HUMOR[HUMOR] || LIVRES) : SUTIS;
if (!alguemOlhando()) lista = lista.filter((g) => !PARA_ALGUEM.includes(g));
const opcoes = lista.filter((g) => !RECENTES.includes(g));
const g = (opcoes.length ? opcoes : SUTIS)[Math.floor(Math.random() * (opcoes.length || SUTIS.length))];
RECENTES.push(g); if (RECENTES.length > 2) RECENTES.shift();
return g;
}
function proximo() {
@@ -476,7 +498,22 @@ function proximo() {
r.kf_t = 0;
}
const ESPIRAL = { ate: 0 }; // tonto: espiral no lugar dos olhos até este instante
const espiralAtiva = () => performance.now() / 1000 < ESPIRAL.ate;
function caminhoEspiral(cx, cy, R, lado) {
const voltas = 2.75, giro = performance.now() / 1000 * 7 * lado;
let d = "M " + cx + "," + cy;
for (let th = 0.22; th <= voltas * Math.PI * 2; th += 0.22) {
const rr = R * th / (voltas * Math.PI * 2), a = th * lado + giro;
d += " L " + (cx + Math.cos(a) * rr).toFixed(1) + "," + (cy + Math.sin(a) * rr).toFixed(1);
}
return d;
}
function animar(nome) {
if (nome === "tonto") ESPIRAL.ate = performance.now() / 1000 + 2.1;
// mãos (esconder, espiar, acenar, pensar, bocejar) valem também quando o gesto vem sozinho
if (SEQS[nome] && window.MotorRosto && window.MotorRosto.maos) window.MotorRosto.maos(nome);
if (SEQS[nome]) {
r.fila = SEQS[nome].map(([a, d]) => [Object.assign({}, a), d]);
proximo();
@@ -502,6 +539,7 @@ function passo(dt) { // avanca a fila; true se ha gesto em
/* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */
let ATIV = null;
const OLHAR = { x: 0, y: 0, ax: 0, ay: 0, prox: 0 }; // ponto que os olhos estão fitando no repouso
function atividade(nome, segundos) {
if (!window.MotorRosto || !window.MotorRosto.atividade) return;
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
@@ -523,9 +561,18 @@ function repouso(t) { // pose calma: olha devagar, "respira
r.boca = "neutra";
r.cor = CIANO.slice();
r.ab = 1.0; r.ab2 = null; r.esc = 1.0;
// o vaivem de sempre + a direcao de quem se mexeu na frente (se houver)
r.dx = 8.0 * Math.sin(t * 0.55) + 3.0 * Math.sin(t * 1.7) + MOV.olharX;
r.dy = 3.5 * Math.sin(t * 0.9) + 1.5 * Math.sin(t * 2.3) + MOV.olharY;
// olhar de gente: fixa num ponto por 1 a 4 s e salta rápido para outro (quase sempre perto do
// centro, às vezes para o lado), mais a direção de quem se mexeu na frente (se houver)
if (t >= OLHAR.prox || t < OLHAR.prox - 10) {
const longe = Math.random() < 0.25;
OLHAR.ax = (Math.random() * 2 - 1) * (longe ? 18 : 7);
OLHAR.ay = (Math.random() * 2 - 1) * (longe ? 8 : 3);
OLHAR.prox = t + 1.2 + Math.random() * 3;
}
OLHAR.x += (OLHAR.ax - OLHAR.x) * 0.3;
OLHAR.y += (OLHAR.ay - OLHAR.y) * 0.3;
r.dx = OLHAR.x + 1.0 * Math.sin(t * 0.9) + MOV.olharX;
r.dy = OLHAR.y + 0.8 * Math.sin(t * 1.3) + MOV.olharY;
if (ATIV) { // fazendo algo: olha para a atividade
const v = ATIV.v;
r.dx = r.dx * (1 - v) * 0.3 + ATIV.x * v;
@@ -555,7 +602,7 @@ function desenhar() {
OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; });
if (comMotor) {
window.MotorRosto.desenhar({
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo, espiral: espiralAtiva(),
icone: !r.dormindo && !window.carinhaOlhosDesenho ? window.carinhaOlhosIcone || null : null,
carga: window.carinhaBateria || null,
iris: EXTRAS.iris,
@@ -660,7 +707,6 @@ function trata(ordem) {
} else if (SEQS[ordem]) {
if (window.MotorRosto) {
if (ordem === "musica") window.MotorRosto.faixa("equalizador", { segundos: 2.3 });
else window.MotorRosto.maos(ordem);
}
animar(ordem);
} else if (ordem === "calma") {