feat(rosto): atividades (momentos dele) e seis gestos novos

Atividades: a tela se divide, o rosto vai para a esquerda e encolhe, e
à direita um bonequinho do Carinha (silhueta na cor dos olhos) faz algo:
ler (vira a página), cantar (microfone e notas), jogar bola
(embaixadinha), videogame (TV com joguinho), pintar (cavalete que ganha
pinceladas) e malhar (halteres e suor). Os olhos grandes acompanham:
leem as linhas, seguem a bola, olham a TV. Camada própria atrás dos
olhos, vale para todos os rostos; em pé (retrato) a cena vai para baixo.

- Comando {tipo: "atividade", valor: nome|parar}; toque na tela, dormir
  e E-Stop encerram; o celular informa a atividade na telemetria.
- Modo vida escolhe pelo humor (feliz canta e joga bola, tranquilo lê
  e pinta, entediado joga videogame), nunca de madrugada, e não
  interrompe uma atividade em andamento.
- Gestos novos: rir, comemorar, tímido, confuso, bocejar e tonto; o
  humor e o modo vida também os usam.
- Painel: favoritos vão até 20 e o bravo volta junto com os novos;
  botões de atividade no Ao vivo, no Ctrl+K e tópico no guia.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 21:12:14 -03:00
co-authored by Claude Opus 5.5
parent 48fd51e377
commit d7a17b5683
13 changed files with 895 additions and 36 deletions
+58 -8
View File
@@ -5,7 +5,13 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000}
svg{display:block;width:100%;height:100%}
svg{display:block;width:100%;height:100%;position:relative;z-index:2}
/* atividade (momentos dele): a tela se divide. Deitado, o rosto vai para a esquerda e o bonequinho
aparece à direita; em pé, o rosto sobe e ele aparece embaixo. */
#tela,#rostoMotor{transition:transform .8s cubic-bezier(.2,.8,.2,1);transform-origin:50% 50%}
body.atividade #tela,body.atividade #rostoMotor{transform:translateX(-23%) scale(.74)}
@media (orientation:portrait){body.atividade #tela,body.atividade #rostoMotor{transform:translateY(-20%) scale(.8)}}
body.atividade #tela>rect{opacity:0} /* o fundo preto do SVG não pode tapar o bonequinho */
.corpo{fill-rule:evenodd}
.traco{fill:none;stroke-linecap:round}
.corpo,.traco{filter:drop-shadow(0 0 2px currentColor)}
@@ -361,7 +367,23 @@ function sequencias() {
[{ dy: -5 }, 0.25], [{ dy: 5 }, 0.25], [{ dy: -5 }, 0.25], [{ dy: 5 }, 0.25], [{ forma: "aberto", dy: 0 }, 0.3]];
const alegre = [[{ forma: "feliz", cor: AMARELO, boca: "sorriso" }, 0.14], [{ cor: VERDE }, 0.14],
[{ cor: CIANO }, 0.14], [{ forma: "aberto" }, 0.2]];
return { esconder, espiar, acenar, pensar, musica,
// Mais expressões: risada, bocejo, confusão, tontura, timidez e comemoração.
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],
[{ 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 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],
[{ dy: -10, cor: VERDE }, 0.15], [{ dy: 2, cor: CIANO }, 0.15], [{ dy: -10, cor: AMARELO }, 0.15],
[{ dy: 0, cor: CIANO }, 0.2], [{ forma: "aberto", esc: 1.0 }, 0.3]];
return { rir, bocejar, confuso, tonto, timido, comemorar,
esconder, espiar, acenar, pensar, musica,
piscar, piscar2, olhar_esq, olhar_dir, olhar_baixo, feliz, surpreso,
bravo, coracao, triste, piscadinha, alegre };
}
@@ -378,11 +400,11 @@ const LIVRES = ["feliz", "olhar_esq", "olhar_dir", "olhar_baixo",
/* Modo vida (servidor): o humor do bichinho escolhe os gestos soltos entre um ato e outro. */
const LIVRES_HUMOR = {
feliz: ["feliz", "alegre", "piscadinha", "coracao", "olhar_esq", "olhar_dir"],
tranquilo: ["olhar_esq", "olhar_dir", "feliz", "piscadinha", "olhar_baixo"],
entediado: ["olhar_esq", "olhar_dir", "olhar_baixo", "pensar"],
carente: ["olhar_baixo", "triste", "olhar_esq", "olhar_dir"],
tranquilo: ["olhar_esq", "olhar_dir", "feliz", "piscadinha", "olhar_baixo", "timido"],
entediado: ["olhar_esq", "olhar_dir", "olhar_baixo", "pensar", "bocejar", "confuso"],
carente: ["olhar_baixo", "triste", "olhar_esq", "olhar_dir", "timido"],
triste: ["triste", "olhar_baixo", "olhar_baixo"],
cansado: ["olhar_baixo", "piscar2"],
cansado: ["olhar_baixo", "piscar2", "bocejar"],
};
let HUMOR = "normal";
@@ -471,6 +493,15 @@ function passo(dt) { // avanca a fila; true se ha gesto em
return true;
}
/* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */
let ATIV = null;
function atividade(nome) {
if (!window.MotorRosto || !window.MotorRosto.atividade) return;
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
if (window.MotorRosto.atividade(nome) && nome === "cantar") tocarSom("musica");
}
function repouso(t) { // pose calma: olha devagar, "respira"
if (r.dormindo) {
r.forma = "dormir"; r.forma2 = "dormir"; r.boca = "sono";
@@ -488,6 +519,14 @@ function repouso(t) { // pose calma: olha devagar, "respira
// 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;
if (ATIV) { // fazendo algo: olha para a atividade
const v = ATIV.v;
r.dx = r.dx * (1 - v) * 0.3 + ATIV.x * v;
r.dy = r.dy * (1 - v) * 0.3 + ATIV.y * v;
if (ATIV.forma) r.forma = ATIV.forma;
if (ATIV.boca) r.boca = ATIV.boca;
if (ATIV.ab) r.ab = ATIV.ab;
}
}
function desenhar() {
@@ -499,7 +538,7 @@ function desenhar() {
// desliza na direcao do movimento; parado, volta ao repouso discreto
const tAgora = performance.now() / 1000;
const seguindo = TRACK.alvo && tAgora - TRACK.visto < .35 && !r.dormindo;
const mirando = MOV.ativo && (tAgora - MOV.ultimoMov) < 1.2;
const mirando = (MOV.ativo && (tAgora - MOV.ultimoMov) < 1.2) || !!ATIV;
const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0);
const dx = seguindo ? MOV.olharX : r.dx, dy = seguindo ? MOV.olharY : r.dy;
const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]];
@@ -594,8 +633,11 @@ function trata(ordem) {
HUMOR = ordem.slice(6).trim();
} else if (ordem.indexOf("ritmo ") === 0) {
PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal;
} else if (ordem.indexOf("atividade ") === 0) {
atividade(ordem.slice(10).trim());
} else if (ordem === "dormir" || ordem === "sono") {
r.dormindo = true;
atividade(null);
} else if (ordem === "acordar") {
r.dormindo = false; animar("surpreso");
} else if (ordem === "aleatorio") {
@@ -609,6 +651,7 @@ function trata(ordem) {
} else if (ordem === "calma") {
// E-Stop do painel: interrompe a animação e volta ao rosto neutro.
r.fila = []; r.kf = null; r.boca = "neutra"; r.dormindo = false;
atividade(null);
} else if (ordem === "parar") {
try { window.close(); } catch (e) { r.fila = []; }
}
@@ -695,6 +738,7 @@ if (MODO_PAINEL) {
const dx = e.clientX - inicio.x, dy = e.clientY - inicio.y, dur = performance.now() - inicio.t;
inicio = null;
if (r.dormindo) { trata('acordar'); cuidar('cutucar'); return; }
if (ATIV && !ATIV.saindo) { atividade(null); animar('surpreso'); cuidar('cutucar'); return; } // interrompeu
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) && dur < 900) { animar('coracao'); cuidar('carinho'); return; }
if (dur < 350 && Math.hypot(dx, dy) < 15) {
cuidar('cutucar');
@@ -1128,6 +1172,7 @@ async function sincronizarApp() {
centro: [MOV.cx, MOV.cy], config: configRecebida,
brilho: window.carinhaBrilho,
tela_apagada: !!window.carinhaTelaApagada,
atividade: window.MotorRosto && window.MotorRosto.atividadeAtual ? window.MotorRosto.atividadeAtual() : null,
bateria: window.carinhaBateria,
aparelho: window.carinhaAparelho,
video: window.carinhaVideo,
@@ -1227,6 +1272,10 @@ function laco(t) {
checarSilencio(t / 1000);
atualizarFeicao(t / 1000);
ATIV = window.MotorRosto && window.MotorRosto.olharAtividade && !r.dormindo ? window.MotorRosto.olharAtividade(t / 1000) : null;
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
const ocupado = congelado ? true : passo(dt);
if (!ocupado) {
descanso += dt;
@@ -1236,7 +1285,7 @@ function laco(t) {
} else if (t >= proxPiscar) {
animar("piscar");
proxPiscar = t + 1600 + Math.random() * 3500;
} else if (AUTO && t >= proxGesto) {
} else if (AUTO && t >= proxGesto && !ATIV) {
animar(gestoSolto());
// cansado e triste mexem menos; feliz, mais
const ritmo = { cansado: 1.8, triste: 1.5, feliz: 0.8 }[HUMOR] || 1;
@@ -1247,6 +1296,7 @@ function laco(t) {
}
desenhar();
if (window.MotorRosto && window.MotorRosto.palco) window.MotorRosto.palco({ t: t / 1000, cor: CIANO });
contador++;
if (t - marco >= 1000) {