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) {
+307 -1
View File
@@ -518,6 +518,307 @@
c.restore();
}
// ------------------------------------------------------------ atividades
// "Momentos dele": um bonequinho do Carinha (silhueta na cor dos olhos) faz algo na parte de
// baixo da tela enquanto os olhos sobem, encolhem e acompanham. Camada própria, atrás dos olhos,
// vale para qualquer rosto (SVG ou motor). O carinha.html pergunta para onde olhar
// (olharAtividade) e chama palco() a cada quadro.
const ATIVIDADES = {
ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] },
cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] },
bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] },
videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] },
pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] },
malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] },
};
const ativ = { nome: null, t0: 0, dur: 0, fim: 0 };
let palcoTela = null, pctx = null;
const agoraS = () => performance.now() / 1000;
function iniciarAtividade(nome, segundos) {
if (!ATIVIDADES[nome]) { pararAtividade(); return false; }
const [a, b] = ATIVIDADES[nome].dur;
ativ.nome = nome; ativ.t0 = agoraS(); ativ.fim = 0;
ativ.dur = segundos || a + Math.random() * (b - a);
return true;
}
function pararAtividade() { if (ativ.nome && !ativ.fim) ativ.fim = agoraS(); }
// 0..1: entra e sai em 0,6 s; termina sozinha depois da duração
function visibilidade(t) {
if (!ativ.nome) return 0;
if (!ativ.fim && t - ativ.t0 >= ativ.dur) ativ.fim = t;
const entra = Math.min(1, Math.max(0, (t - ativ.t0) / 0.6));
if (!ativ.fim) return entra;
const sai = 1 - (t - ativ.fim) / 0.6;
if (sai <= 0) { ativ.nome = null; return 0; }
return Math.min(entra, sai);
}
// movimentos de cada cena, em função do tempo k (s) desde o início
const bola = (k) => { const p = (k % 1.1) / 1.1; return { p, h: 4 * p * (1 - p), x: 0.35 * Math.sin(k * 0.7) }; };
const pincel = (k) => ({ x: Math.sin(k * 3.1) * 0.6 + Math.sin(k * 1.3) * 0.4, y: Math.cos(k * 2.3) * 0.7 });
const forca = (k) => (1 - Math.cos(k * 2.2)) / 2; // 0 = halteres no ombro, 1 = braços em cima
/** Para onde os olhos grandes olham (unidades do rosto: dx ±24, dy ±12) e que cara fazem. */
function olharAtividade(t) {
const v = visibilidade(t);
if (!v) return null;
const k = t - ativ.t0, info = { nome: ativ.nome, v, x: 0, y: 0, saindo: !!ativ.fim };
const lado = innerWidth >= innerHeight * 1.2 ? 1 : 0; // deitado: a cena fica à direita
if (ativ.nome === 'ler') { // lê linha a linha: varre da esquerda para a direita
const linha = (k % 2.4) / 2.4;
info.x = (linha < 0.9 ? -8 + 16 * (linha / 0.9) : 8 - 16 * ((linha - 0.9) / 0.1)) + lado * 13;
info.y = 7 + (Math.floor(k / 2.4) % 3) * 1.5;
} else if (ativ.nome === 'cantar') {
info.x = 6 * Math.sin(k * 1.3) + lado * 12; info.y = -4 + 4 * Math.sin(k * 4);
info.forma = 'feliz'; info.boca = Math.sin(k * 7) > -0.2 ? 'aberta' : 'sorriso';
} else if (ativ.nome === 'bola') { // segue a bola
const b = bola(k); info.x = b.x * 16 + lado * 14; info.y = 9 - b.h * 18;
} else if (ativ.nome === 'videogame') { // vidrado na TV; de vez em quando comemora
info.x = 18 + lado * 5 + Math.sin(k * 9) * 1.5; info.y = 1;
if (k % 9 > 7.6) { info.forma = 'feliz'; info.boca = 'sorriso'; }
} else if (ativ.nome === 'pintar') {
const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5;
} else if (ativ.nome === 'malhar') {
const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1;
}
return info;
}
// --- o bonequinho: cabeça-tela com olhinhos, antena, tronco, braços e pernas articulados.
// Ângulos em radianos: 0 = apontando para baixo; positivo gira para a direita da tela.
function membro(c, x0, y0, a1, a2, l1, l2, w) {
const x1 = x0 + Math.sin(a1) * l1, y1 = y0 + Math.cos(a1) * l1;
const x2 = x1 + Math.sin(a1 + a2) * l2, y2 = y1 + Math.cos(a1 + a2) * l2;
c.beginPath(); capsula(c, x0, y0, x1, y1, w / 2); c.fill(); c.stroke();
c.beginPath(); capsula(c, x1, y1, x2, y2, w / 2); c.fill(); c.stroke();
return { x: x2, y: y2 };
}
function boneco(c, x, y, s, p, cor, k) {
c.fillStyle = css(cor.map((v) => v * 0.22), 0.96); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
c.lineJoin = 'round'; c.lineCap = 'round';
const w = s * 0.27, braco = [s * 0.62, s * 0.58];
const perna = p.sentado ? [s * 0.38, s * 0.62] : [s * 0.72, s * 0.7];
const hx = x + (p.dx || 0) * s, hy = p.sentado ? y - perna[1] - s * 0.12 : y - (perna[0] + perna[1]) * 0.97;
const pE = p.pE || [-0.08, 0], pD = p.pD || [0.08, 0];
membro(c, hx - s * 0.25, hy, pE[0], pE[1], perna[0], perna[1], w * 1.1);
membro(c, hx + s * 0.25, hy, pD[0], pD[1], perna[0], perna[1], w * 1.1);
c.save();
c.translate(hx, hy); c.rotate(-(p.inclina || 0));
// tronco
caixa(c, -s * 0.5, -s * 1.12, s, s * 1.12, s * 0.25); c.fill(); c.stroke();
c.save(); c.fillStyle = css(cor, 0.85); // luzinha no peito
c.beginPath(); c.arc(0, -s * 0.62, s * 0.09 * (0.8 + 0.2 * Math.sin(k * 3)), 0, Math.PI * 2); c.fill(); c.restore();
// cabeça (uma telinha, como o celular) com olhinhos e antena
c.save(); c.translate(0, -s * 1.25); c.rotate(-(p.cabeca || 0));
c.beginPath(); c.moveTo(0, -s * 0.95); c.lineTo(0, -s * 1.25); c.stroke();
c.save(); c.fillStyle = css(cor); c.beginPath(); c.arc(0, -s * 1.3, s * 0.1, 0, Math.PI * 2); c.fill(); c.restore();
caixa(c, -s * 0.66, -s * 0.98, s * 1.32, s * 0.98, s * 0.28); c.fill(); c.stroke();
const ox = (p.olhoX || 0) * s * 0.12, oy = (p.olhoY || 0) * s * 0.1;
const pisca = (k % 3.7) < 0.12 ? 0.15 : 1;
c.save(); c.fillStyle = css(cor);
for (const lado of [-1, 1]) {
c.beginPath(); c.ellipse(lado * s * 0.27 + ox, -s * 0.5 + oy, s * 0.11, s * 0.16 * pisca, 0, 0, Math.PI * 2); c.fill();
}
c.restore(); c.restore();
// braços (no mesmo quadro do tronco inclinado); as mãos voltam para segurar os objetos
const bE = p.bE || [-0.25, 0.15], bD = p.bD || [0.25, -0.15];
const mE = membro(c, -s * 0.58, -s * 0.98, bE[0], bE[1], braco[0], braco[1], w);
const mD = membro(c, s * 0.58, -s * 0.98, bD[0], bD[1], braco[0], braco[1], w);
c.restore();
const rot = (pt) => { // do quadro do tronco para a tela
const a = -(p.inclina || 0), cs = Math.cos(a), sn = Math.sin(a);
return { x: hx + pt.x * cs - pt.y * sn, y: hy + pt.x * sn + pt.y * cs };
};
return { mE: rot(mE), mD: rot(mD), quadril: { x: hx, y: hy }, cabeca: rot({ x: 0, y: -s * 1.75 }) };
}
const claro = (cor, a) => css(cor.map((v) => v + (255 - v) * 0.55), a);
const escuro = (cor) => css(cor.map((v) => v * 0.25));
function nota(c, x, y, s) { // ♪
c.beginPath(); c.ellipse(x, y, s * 0.2, s * 0.15, -0.4, 0, Math.PI * 2); c.fill();
c.fillRect(x + s * 0.14, y - s * 0.7, s * 0.06, s * 0.7);
c.beginPath(); c.moveTo(x + s * 0.2, y - s * 0.7); c.quadraticCurveTo(x + s * 0.5, y - s * 0.55, x + s * 0.42, y - s * 0.3); c.stroke();
}
const CENAS = {
ler(c, x, y, s, k, cor) {
const b = boneco(c, x, y, s, {
sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], cabeca: -0.08, olhoY: 0.8,
olhoX: -0.8 + 1.6 * ((k % 2.4) / 2.4), bE: [0.2, 2.4], bD: [-0.2, -2.4],
}, cor, k);
// livro aberto entre as mãos, com uma página virando de vez em quando
const cx = (b.mE.x + b.mD.x) / 2, cy = (b.mE.y + b.mD.y) / 2 + s * 0.14, lw = s * 0.72, lh = s * 0.55;
c.fillStyle = claro(cor, 0.95); c.strokeStyle = css(cor);
for (const lado of [-1, 1]) {
c.beginPath(); c.moveTo(cx, cy + lh * 0.1); c.lineTo(cx + lado * lw, cy); c.lineTo(cx + lado * lw, cy - lh);
c.lineTo(cx, cy - lh * 0.9); c.closePath(); c.fill(); c.stroke();
}
c.strokeStyle = css(cor, 0.5); c.lineWidth = Math.max(1, s * 0.03);
for (let i = 1; i <= 3; i++) for (const lado of [-1, 1]) {
const yy = cy - lh * (0.25 * i); c.beginPath(); c.moveTo(cx + lado * lw * 0.15, yy); c.lineTo(cx + lado * lw * 0.85, yy - lh * 0.03); c.stroke();
}
const virar = (k % 6) / 0.7; // vira em 0,7 s a cada 6 s
if (virar < 1) {
const px = cx + lw * Math.cos(virar * Math.PI), sobe = Math.sin(virar * Math.PI) * lh * 0.25;
c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
c.beginPath(); c.moveTo(cx, cy + lh * 0.1); c.lineTo(px, cy - sobe); c.lineTo(px, cy - lh - sobe);
c.lineTo(cx, cy - lh * 0.9); c.closePath(); c.fill(); c.stroke();
}
},
cantar(c, x, y, s, k, cor) {
const balanco = Math.sin(k * 2.2);
const b = boneco(c, x, y, s, {
dx: balanco * 0.12, inclina: balanco * 0.08, cabeca: balanco * 0.1, olhoY: -0.3,
pE: [-0.1 - Math.max(0, balanco) * 0.15, 0], pD: [0.1 - Math.min(0, balanco) * 0.15, 0],
bD: [2.45, 0.9], bE: [-0.9 - 0.4 * Math.sin(k * 4.4), -0.4],
}, cor, k);
// microfone na mão direita, perto da boca
c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
c.beginPath(); capsula(c, b.mD.x, b.mD.y, b.mD.x - s * 0.1, b.mD.y - s * 0.4, s * 0.07); c.fill(); c.stroke();
c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(b.mD.x - s * 0.12, b.mD.y - s * 0.5, s * 0.14, 0, Math.PI * 2); c.fill(); c.stroke();
// notas subindo
c.fillStyle = css(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
const alfa = c.globalAlpha;
for (let i = 0; i < 3; i++) {
const f = (k * 0.45 + i / 3) % 1;
c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f);
nota(c, b.cabeca.x + s * (0.9 + 0.5 * i) + Math.sin(f * 6 + i) * s * 0.2, b.cabeca.y + s * 0.4 - f * s * 1.6, s * 0.75);
}
c.globalAlpha = alfa;
},
bola(c, x, y, s, k, cor) {
const bl = bola(k), chute = Math.max(0, 1 - bl.p / 0.18) + Math.max(0, (bl.p - 0.9) / 0.1);
const b = boneco(c, x, y, s, {
dx: bl.x * 0.5, olhoX: bl.x * 2, olhoY: 0.6 - bl.h * 1.4,
pD: [0.08 + chute * 0.9, -chute * 0.6], pE: [-0.08, 0], bE: [-0.6, 0.3], bD: [0.6, -0.3],
}, cor, k);
const bx = b.quadril.x + s * 0.55 + bl.x * s * 0.3, by = y - s * 0.55 - bl.h * s * 2.6, r = s * 0.3;
const alfa = c.globalAlpha;
c.save(); c.globalAlpha = alfa * 0.35 * (1 - bl.h); c.fillStyle = css(cor); // sombra no chão
c.beginPath(); c.ellipse(bx, y, r * (1.1 - bl.h * 0.5), r * 0.18, 0, 0, Math.PI * 2); c.fill(); c.restore();
c.save(); c.translate(bx, by); c.rotate(k * 5);
c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke();
c.fillStyle = css(cor.map((v) => v * 0.3)); // gomo da bola
c.beginPath(); for (let i = 0; i < 5; i++) { const a = i * 1.2566; c.lineTo(Math.cos(a) * r * 0.38, Math.sin(a) * r * 0.38); } c.closePath(); c.fill();
c.restore();
},
videogame(c, x, y, s, k, cor) {
const aperta = Math.sin(k * 23) * 0.05;
const b = boneco(c, x - s * 1.3, y, s, {
sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], olhoX: 1, olhoY: 0.1, cabeca: -0.05,
bE: [0.15 + aperta, 0.95], bD: [-0.15 - aperta, -0.95],
}, cor, k);
// controle
const cx = (b.mE.x + b.mD.x) / 2, cy = (b.mE.y + b.mD.y) / 2;
c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
caixa(c, cx - s * 0.42, cy - s * 0.16, s * 0.84, s * 0.34, s * 0.16); c.fill(); c.stroke();
c.fillStyle = css(cor.map((v) => v * 0.3));
c.fillRect(cx - s * 0.3, cy - s * 0.02, s * 0.16, s * 0.05); c.fillRect(cx - s * 0.245, cy - s * 0.08, s * 0.05, s * 0.17);
for (const [dx, dy] of [[0.22, -0.05], [0.3, 0.04]]) { c.beginPath(); c.arc(cx + dx * s, cy + dy * s, s * 0.04, 0, Math.PI * 2); c.fill(); }
// TV com um joguinho: bonequinho pulando obstáculos
const tx = x + s * 1.15, tw = s * 2.1, th = s * 1.45, ty = y - s * 0.55 - th;
c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
c.beginPath(); c.moveTo(tx + tw / 2, ty + th); c.lineTo(tx + tw / 2, y); c.moveTo(tx + tw * 0.3, y); c.lineTo(tx + tw * 0.7, y); c.stroke();
caixa(c, tx, ty, tw, th, s * 0.12); c.fill(); c.stroke();
c.save(); caixa(c, tx + s * 0.1, ty + s * 0.1, tw - s * 0.2, th - s * 0.2, s * 0.06); c.clip();
const chao = ty + th - s * 0.32, px = s * 0.11;
c.fillStyle = css(cor, 0.6); c.fillRect(tx, chao, tw, s * 0.05);
const obst = tx + tw - ((k * 1.4) % 1) * (tw + s * 0.4);
c.fillStyle = css(cor); c.fillRect(obst, chao - px * 2, px * 1.4, px * 2);
const ox = tx + tw * 0.3, perto = (ox + px * 2 - obst) / (s * 0.9) + 0.5;
const pulo = perto > 0 && perto < 1 ? Math.sin(perto * Math.PI) : 0;
c.fillStyle = claro(cor, 1); c.fillRect(ox, chao - px * 2.4 - pulo * s * 0.45, px * 1.6, px * 2.4);
if (k % 9 > 7.6) { // vitória: estrelinhas na tela
c.fillStyle = claro(cor, 1); c.font = Math.round(s * 0.4) + 'px sans-serif'; c.textAlign = 'center';
c.fillText('★ ★ ★', tx + tw / 2, ty + th * 0.4);
}
c.restore();
},
pintar(c, x, y, s, k, cor) {
const pc = pincel(k);
const ex = x + s * 0.9, ew = s * 1.6, eh = s * 1.35, ey = y - s * 1.0 - eh;
// cavalete
c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
c.beginPath(); c.moveTo(ex + ew * 0.2, y); c.lineTo(ex + ew * 0.5, ey - s * 0.15); c.lineTo(ex + ew * 0.8, y); c.stroke();
c.fillStyle = claro(cor, 0.95); caixa(c, ex, ey, ew, eh, s * 0.05); c.fill(); c.stroke();
// pinceladas que vão aparecendo (sempre as mesmas para a mesma pintura)
const tintas = [cor, [255, 112, 176], [255, 218, 96], [120, 255, 160]];
const n = Math.min(40, Math.floor(k / 0.7));
c.save(); caixa(c, ex, ey, ew, eh, s * 0.05); c.clip();
for (let i = 0; i < n; i++) {
const a = Math.sin(i * 12.9898 + ativ.t0) * 43758.5453, rnd = a - Math.floor(a);
const b2 = Math.sin(i * 78.233 + ativ.t0) * 12345.678, rnd2 = b2 - Math.floor(b2);
c.fillStyle = css(tintas[i % 4], 0.9);
c.beginPath(); c.ellipse(ex + ew * (0.12 + rnd * 0.76), ey + eh * (0.12 + rnd2 * 0.76), s * 0.16, s * 0.07, rnd * 3, 0, Math.PI * 2); c.fill();
}
c.restore();
const b = boneco(c, x - s * 0.5, y, s, {
inclina: -0.06, olhoX: 1, olhoY: pc.y * 0.5, cabeca: -0.08,
bD: [1.75 + pc.y * 0.25, -0.35 + pc.x * 0.3], bE: [-0.7, 0.9], pE: [-0.12, 0], pD: [0.08, 0],
}, cor, k);
// pincel numa mão, paleta na outra
c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
c.beginPath(); c.moveTo(b.mD.x - s * 0.1, b.mD.y + s * 0.08); c.lineTo(b.mD.x + s * 0.4, b.mD.y - s * 0.12); c.stroke();
c.fillStyle = css(tintas[Math.floor(k / 0.7) % 4]); c.beginPath(); c.arc(b.mD.x + s * 0.43, b.mD.y - s * 0.13, s * 0.07, 0, Math.PI * 2); c.fill();
c.fillStyle = claro(cor, 0.95); c.beginPath(); c.ellipse(b.mE.x, b.mE.y, s * 0.32, s * 0.2, -0.3, 0, Math.PI * 2); c.fill(); c.stroke();
for (let i = 0; i < 3; i++) { c.fillStyle = css(tintas[i + 1]); c.beginPath(); c.arc(b.mE.x - s * 0.15 + i * s * 0.13, b.mE.y - s * 0.03, s * 0.05, 0, Math.PI * 2); c.fill(); }
},
malhar(c, x, y, s, k, cor) {
const f = suave(forca(k));
const b = boneco(c, x, y, s, {
olhoY: -0.4 + (1 - f) * 0.5, pE: [-0.22, 0.08], pD: [0.22, -0.08],
bE: [-1.25 - f * 1.75, -1.9 + f * 1.75], bD: [1.25 + f * 1.75, 1.9 - f * 1.75],
}, cor, k);
// halteres
c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
for (const m of [b.mE, b.mD]) {
c.fillRect(m.x - s * 0.25, m.y - s * 0.035, s * 0.5, s * 0.07);
for (const lado of [-1, 1]) { caixa(c, m.x + lado * s * 0.28 - s * 0.07, m.y - s * 0.17, s * 0.14, s * 0.34, s * 0.04); c.fill(); c.stroke(); }
}
if (f > 0.85) { // gotinha de suor
const g = (k * 2) % 1;
c.fillStyle = claro(cor, 1 - g);
c.beginPath(); c.arc(b.cabeca.x + s * 0.72, b.cabeca.y + s * 0.3 + g * s * 0.5, s * 0.07, 0, Math.PI * 2); c.fill();
}
},
};
/** Desenha a cena da atividade (camada atrás dos olhos). o = { t, cor }. */
function desenharPalco(o) {
const v = visibilidade(o.t);
if (!v) {
if (palcoTela && palcoTela.style.display !== 'none') palcoTela.style.display = 'none';
return false;
}
if (!palcoTela) {
palcoTela = document.createElement('canvas'); palcoTela.id = 'palcoAtividade';
palcoTela.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:1';
document.body.append(palcoTela); pctx = palcoTela.getContext('2d');
}
palcoTela.style.display = '';
const d = Math.min(1.5, window.devicePixelRatio || 1), W = Math.round(innerWidth * d), H = Math.round(innerHeight * d);
if (palcoTela.width !== W || palcoTela.height !== H) { palcoTela.width = W; palcoTela.height = H; }
const deitado = W >= H * 1.2;
const s = deitado ? H * 0.145 : Math.min(W * 0.075, H * 0.075);
const x = deitado ? W * 0.7 : W / 2, y = deitado ? H * 0.9 : H * 0.95;
const c = pctx, k = o.t - ativ.t0;
c.clearRect(0, 0, W, H);
c.save();
c.globalAlpha = v;
c.translate((1 - v) * (deitado ? W * 0.06 : 0), (1 - v) * (deitado ? 0 : H * 0.08)); // desliza ao entrar
c.strokeStyle = css(o.cor, 0.35); c.lineWidth = Math.max(1, s * 0.04); // chão
c.beginPath(); c.moveTo(x - s * 2.6, y); c.lineTo(Math.min(W * 0.98, x + s * 3.6), y); c.stroke();
c.shadowColor = css(o.cor, 0.55); c.shadowBlur = s * 0.35;
CENAS[ativ.nome](c, x, y, s, k, o.cor);
c.restore();
return true;
}
function esconder() { if (tela) tela.style.display = 'none'; antes = 0; }
window.MotorRosto = {
@@ -529,5 +830,10 @@
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) };
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS),
/** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */
atividade: (nome, segundos) => (nome ? iniciarAtividade(nome, segundos) : (pararAtividade(), false)),
atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null),
olharAtividade, palco: desenharPalco,
atividades: Object.fromEntries(Object.entries(ATIVIDADES).map(([k, v]) => [k, { nome: v.nome, emoji: v.emoji }])) };
})();