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:
@@ -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) {
|
||||
|
||||
@@ -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 }])) };
|
||||
})();
|
||||
|
||||
+58
-8
@@ -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) {
|
||||
|
||||
+16
-2
@@ -54,7 +54,10 @@ FORMATOS = ["padrao", "redondo", "pilula", "quadrado", "largo"]
|
||||
GESTOS = ["piscar", "piscar2", "olhar_esq", "olhar_dir", "olhar_baixo", "feliz",
|
||||
"surpreso", "bravo", "coracao", "triste", "piscadinha", "alegre",
|
||||
"dormir", "acordar", "aleatorio", "parar",
|
||||
"esconder", "espiar", "acenar", "pensar", "musica"]
|
||||
"esconder", "espiar", "acenar", "pensar", "musica",
|
||||
"rir", "bocejar", "confuso", "tonto", "timido", "comemorar"]
|
||||
# Atividades (momentos dele): o bonequinho faz algo embaixo dos olhos (rosto-motor.js).
|
||||
ATIVIDADES = ["ler", "cantar", "bola", "videogame", "pintar", "malhar"]
|
||||
# Ordens internas que o rosto entende mas não viram botão.
|
||||
ORDENS_INTERNAS = {"calma"}
|
||||
|
||||
@@ -187,6 +190,11 @@ def ordem_de_comando(tipo, valor):
|
||||
if nome not in GESTOS and nome not in ORDENS_INTERNAS:
|
||||
raise ValueError("Gesto desconhecido: " + nome)
|
||||
return nome
|
||||
if tipo == "atividade":
|
||||
nome = str(valor or "").strip().lower()
|
||||
if nome not in ATIVIDADES and nome != "parar":
|
||||
raise ValueError("Atividade desconhecida: " + nome)
|
||||
return "atividade " + nome
|
||||
if tipo == "rosto":
|
||||
nome = str(valor or "").strip().lower()
|
||||
if nome not in ROSTOS:
|
||||
@@ -247,7 +255,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
with TRAVA:
|
||||
aparencia = dict(APARENCIA)
|
||||
dados = dict(versao=versao(), seq=EVENTOS.seq, autenticacao=bool(getattr(s, "token", None)),
|
||||
estop=dict(ESTOP), aparencia=aparencia, gestos=GESTOS, rostos=ROSTOS, formatos=FORMATOS)
|
||||
estop=dict(ESTOP), aparencia=aparencia, gestos=GESTOS, rostos=ROSTOS, formatos=FORMATOS,
|
||||
atividades=ATIVIDADES)
|
||||
if hasattr(s, "controle"):
|
||||
dados.update(app=s.controle.resumo(host), app_config=s.controle.config.copy())
|
||||
if hasattr(s, "servos"):
|
||||
@@ -504,6 +513,11 @@ def main():
|
||||
vida.treinamento = servidor.treinamento
|
||||
vida.celular_dormindo = celular_dormindo
|
||||
vida.celular_online = lambda: servidor.controle.resumo()['online']
|
||||
|
||||
def celular_atividade():
|
||||
resumo = servidor.controle.resumo()
|
||||
return resumo['estado'].get('atividade') if resumo['online'] else None
|
||||
vida.celular_atividade = celular_atividade
|
||||
vida.alguem_perto = alguem_perto
|
||||
vida.madrugada = lambda agora: madrugada(servidor.controle.config, agora)
|
||||
# E-Stop parado, ou modo presença com ninguém em casa (dormindo em silêncio): não faz nada
|
||||
|
||||
+307
-1
@@ -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 }])) };
|
||||
})();
|
||||
|
||||
@@ -106,9 +106,16 @@ class ServidorTest(unittest.TestCase):
|
||||
self.assertEqual(self.pedir('POST', '/comando', {'tipo': 'gesto', 'valor': 'rm -rf'})[0], 400)
|
||||
self.assertEqual(self.pedir('POST', '/comando', {'tipo': 'rosto', 'valor': 'nada'})[0], 400)
|
||||
self.assertEqual(kiosk.FILA.ordens(), ['feliz', 'rosto miudo'])
|
||||
for novo in ('acenar', 'esconder', 'espiar', 'pensar', 'musica'): # gestos com mãos e faixa
|
||||
for novo in ('acenar', 'esconder', 'espiar', 'pensar', 'musica', # gestos com mãos e faixa
|
||||
'rir', 'bocejar', 'confuso', 'tonto', 'timido', 'comemorar', 'bravo'):
|
||||
self.assertEqual(self.pedir('POST', '/comando', {'tipo': 'gesto', 'valor': novo})[0], 200)
|
||||
kiosk.FILA.limpar()
|
||||
# atividades (momentos dele)
|
||||
self.assertEqual(self.pedir('POST', '/comando', {'tipo': 'atividade', 'valor': 'videogame'})[0], 200)
|
||||
self.assertEqual(self.pedir('POST', '/comando', {'tipo': 'atividade', 'valor': 'parar'})[0], 200)
|
||||
self.assertEqual(self.pedir('POST', '/comando', {'tipo': 'atividade', 'valor': 'voar'})[0], 400)
|
||||
self.assertEqual(kiosk.FILA.ordens(), ['atividade videogame', 'atividade parar'])
|
||||
kiosk.FILA.limpar()
|
||||
kiosk.FILA.adicionar('feliz'); kiosk.FILA.adicionar('rosto miudo')
|
||||
self.assertEqual(self.pedir('GET', '/fila?aparelho=1')[1], ['feliz', 'rosto miudo'])
|
||||
self.assertEqual(self.pedir('GET', '/fila?aparelho=1')[1], [])
|
||||
|
||||
@@ -96,6 +96,23 @@ class VidaTest(unittest.TestCase):
|
||||
self.assertIn('ritmo normal', self.ordens)
|
||||
self.assertTrue(self.v.pir_acorda(DIA + 600)) # desligado: comportamento de sempre
|
||||
|
||||
def test_atividades_pelo_humor(self):
|
||||
self.v.celular_online = lambda: True
|
||||
self.v.necessidades.update(energia=80, alegria=70, social=70, diversao=10) # entediado
|
||||
self.v.rng.random = lambda: 0.0 # sorteio sempre "sim"
|
||||
ato = self.v.agir(DIA)
|
||||
self.assertTrue(ato.startswith('atividade '))
|
||||
self.assertIn(ato.split()[1], ('videogame', 'bola', 'pintar', 'ler'))
|
||||
self.assertEqual(self.ordens[-1], ato)
|
||||
self.assertEqual(self.v.diario[0]['tipo'], 'atividade')
|
||||
self.assertGreater(self.v.necessidades['diversao'], 10)
|
||||
self.v.celular_atividade = lambda: 'videogame' # ocupado: não interrompe
|
||||
self.assertIsNone(self.v.agir(DIA + 60))
|
||||
self.v.celular_atividade = lambda: None
|
||||
noite = time.mktime((2026, 10, 6, 2, 0, 0, 0, 0, -1))
|
||||
self.v.config['noite'] = False
|
||||
self.assertFalse(str(self.v.agir(noite)).startswith('atividade')) # de madrugada, nunca
|
||||
|
||||
def test_horario_da_madrugada(self):
|
||||
from controle_pc import madrugada
|
||||
h = lambda hora: time.mktime((2026, 10, 6, hora, 10, 0, 0, 0, -1))
|
||||
|
||||
@@ -55,20 +55,32 @@ HUMORES = {
|
||||
# Gestos de cada humor: (gesto, peso, o que vai no diário)
|
||||
ATOS = {
|
||||
'feliz': [('alegre', 3, 'Saltitou de alegria'), ('musica', 2, 'Cantarolou'), ('feliz', 3, 'Sorriu à toa'),
|
||||
('piscadinha', 2, 'Deu uma piscadinha'), ('coracao', 1, 'Ficou apaixonado pelo dia')],
|
||||
('piscadinha', 2, 'Deu uma piscadinha'), ('coracao', 1, 'Ficou apaixonado pelo dia'),
|
||||
('rir', 2, 'Deu risada sozinho'), ('comemorar', 1, 'Comemorou à toa')],
|
||||
'tranquilo': [('olhar_esq', 2, 'Olhou em volta'), ('olhar_dir', 2, 'Olhou em volta'), ('piscar2', 2, 'Piscou devagar'),
|
||||
('feliz', 1, 'Sorriu'), ('pensar', 1, 'Ficou pensativo')],
|
||||
('feliz', 1, 'Sorriu'), ('pensar', 1, 'Ficou pensativo'), ('timido', 1, 'Ficou tímido')],
|
||||
'entediado': [('pensar', 3, 'Ficou pensando na vida'), ('olhar_esq', 2, 'Procurou algo para fazer'),
|
||||
('olhar_dir', 2, 'Procurou algo para fazer'), ('espiar', 2, 'Espiou por trás das mãos'),
|
||||
('musica', 1, 'Cantou para passar o tempo')],
|
||||
('musica', 1, 'Cantou para passar o tempo'), ('bocejar', 2, 'Bocejou de tédio'),
|
||||
('confuso', 1, 'Ficou confuso com alguma coisa')],
|
||||
'carente': [('triste', 2, 'Sentiu falta de alguém'), ('olhar_baixo', 2, 'Ficou cabisbaixo'),
|
||||
('esconder', 1, 'Escondeu o rosto')],
|
||||
'carente_perto': [('acenar', 3, 'Acenou chamando atenção'), ('espiar', 2, 'Espiou quem está aí'),
|
||||
('coracao', 2, 'Pediu carinho'), ('surpreso', 1, 'Fez cara de "oi!"')],
|
||||
('coracao', 2, 'Pediu carinho'), ('surpreso', 1, 'Fez cara de "oi!"'), ('timido', 1, 'Ficou tímido')],
|
||||
'triste': [('triste', 3, 'Ficou tristinho'), ('olhar_baixo', 2, 'Olhou para baixo'), ('pensar', 1, 'Ficou pensativo')],
|
||||
'cansado': [('piscar2', 3, 'Bocejou'), ('olhar_baixo', 2, 'Cochilou de olho aberto')],
|
||||
'cansado': [('bocejar', 3, 'Bocejou'), ('piscar2', 1, 'Piscou devagar'), ('olhar_baixo', 2, 'Cochilou de olho aberto'),
|
||||
('tonto', 1, 'Ficou meio tonto de sono')],
|
||||
}
|
||||
|
||||
# Atividades (momentos dele): o bonequinho faz algo na tela. Por humor: (atividade, peso, diário, efeito)
|
||||
ATIVIDADES = {
|
||||
'feliz': [('cantar', 3, 'Foi cantar'), ('bola', 2, 'Foi jogar bola'), ('malhar', 1, 'Foi malhar')],
|
||||
'tranquilo': [('ler', 3, 'Foi ler um livro'), ('pintar', 2, 'Foi pintar um quadro'), ('cantar', 1, 'Foi cantar')],
|
||||
'entediado': [('videogame', 3, 'Foi jogar videogame'), ('bola', 2, 'Foi jogar bola'),
|
||||
('pintar', 1, 'Foi pintar um quadro'), ('ler', 1, 'Foi ler um livro')],
|
||||
}
|
||||
GASTO_ATIVIDADE = {'malhar': 8, 'bola': 5, 'cantar': 3} # energia gasta
|
||||
|
||||
# Assuntos das figurinhas que ele inventa sozinho, por humor e por hora do dia.
|
||||
TEMAS = {
|
||||
'feliz': ['um sol sorridente', 'um balão colorido', 'um arco-íris', 'um sorvete de casquinha', 'confete de festa'],
|
||||
@@ -102,6 +114,7 @@ class Vida:
|
||||
self.celular_dormindo = lambda: None # True/False com o celular online, None sem ele
|
||||
self.celular_online = lambda: False
|
||||
self.alguem_perto = lambda: False # PIR/movimento recente
|
||||
self.celular_atividade = lambda: None # atividade que o celular está fazendo agora
|
||||
self.bloqueado = lambda: False # E-Stop ou modo presença com ninguém em casa
|
||||
self.pode_gemini = lambda: bool(os.environ.get('GEMINI_API_KEY'))
|
||||
self.madrugada = self.noite_padrao # o main troca pelo horário dos ajustes
|
||||
@@ -375,6 +388,18 @@ class Vida:
|
||||
self.dormir(agora, 'Ficou cansado e foi tirar um cochilo')
|
||||
return 'dormir'
|
||||
humor = self.humor()
|
||||
if self.celular_atividade():
|
||||
return None # ocupado com uma atividade: deixa terminar
|
||||
# momentos dele: ler, cantar, jogar bola, videogame... (de dia, com o celular online)
|
||||
if humor in ATIVIDADES and self.celular_online() and not self.madrugada(agora) and self.rng.random() < 0.18:
|
||||
nome, texto = self.escolher(ATIVIDADES[humor])
|
||||
self.enfileirar('atividade ' + nome)
|
||||
with self.lock:
|
||||
n['diversao'] = _limitar(n['diversao'] + 18)
|
||||
n['alegria'] = _limitar(n['alegria'] + 5)
|
||||
n['energia'] = _limitar(n['energia'] - GASTO_ATIVIDADE.get(nome, 2))
|
||||
self.anotar('atividade', texto, agora)
|
||||
return 'atividade ' + nome
|
||||
# habilidade: inventar uma figurinha nova (raro, com limite por dia e intervalo)
|
||||
if not self.motivo_sem_emoji(agora) and self.celular_online() and self.rng.random() < 0.08:
|
||||
if self.criar_figurinha(agora, humor):
|
||||
|
||||
@@ -25,21 +25,49 @@ export const GESTOS_INFO: Record<string, { nome: string; emoji: string }> = {
|
||||
espiar: { nome: 'Espiar', emoji: '🫣' },
|
||||
pensar: { nome: 'Pensar', emoji: '🤔' },
|
||||
musica: { nome: 'Dançar', emoji: '🎵' },
|
||||
rir: { nome: 'Rir', emoji: '😂' },
|
||||
comemorar: { nome: 'Comemorar', emoji: '🥳' },
|
||||
timido: { nome: 'Tímido', emoji: '😳' },
|
||||
confuso: { nome: 'Confuso', emoji: '😕' },
|
||||
bocejar: { nome: 'Bocejar', emoji: '🥱' },
|
||||
tonto: { nome: 'Tonto', emoji: '😵💫' },
|
||||
};
|
||||
|
||||
/** Atividades (momentos dele): o bonequinho do Carinha faz algo embaixo dos olhos. */
|
||||
export const ATIVIDADES_INFO: Record<string, { nome: string; emoji: string }> = {
|
||||
ler: { nome: 'Ler', emoji: '📖' },
|
||||
cantar: { nome: 'Cantar', emoji: '🎤' },
|
||||
bola: { nome: 'Jogar bola', emoji: '⚽' },
|
||||
videogame: { nome: 'Videogame', emoji: '🎮' },
|
||||
pintar: { nome: 'Pintar', emoji: '🎨' },
|
||||
malhar: { nome: 'Malhar', emoji: '🏋️' },
|
||||
};
|
||||
export const atividade = (nome: string) => tentar(() => postar('/comando', { tipo: 'atividade', valor: nome }));
|
||||
|
||||
export const gestosDisponiveis = () => estado.gestos.filter((g) => g in GESTOS_INFO);
|
||||
|
||||
const CHAVE_FAVORITOS = 'carinha_favoritos';
|
||||
const CHAVE_MAOS = 'carinha_favoritos_maos'; // marca que os gestos com mãos já entraram
|
||||
/** Até 12 favoritos; as teclas 1 a 9 valem para os nove primeiros. */
|
||||
export const MAX_FAVORITOS = 12;
|
||||
const CHAVE_V3 = 'carinha_favoritos_v3'; // marca que o bravo e os gestos novos já entraram
|
||||
/** Até 20 favoritos; as teclas 1 a 9 valem para os nove primeiros. */
|
||||
export const MAX_FAVORITOS = 20;
|
||||
const GESTOS_MAOS = ['acenar', 'esconder', 'espiar', 'pensar', 'musica'];
|
||||
const PADRAO_FAVORITOS = ['feliz', 'coracao', 'acenar', 'esconder', 'espiar', 'pensar', 'musica', 'piscadinha', 'surpreso', 'triste', 'dormir', 'acordar'];
|
||||
const GESTOS_V3 = ['rir', 'comemorar', 'timido', 'confuso', 'bocejar', 'tonto', 'bravo'];
|
||||
const PADRAO_FAVORITOS = ['feliz', 'coracao', 'acenar', 'esconder', 'espiar', 'pensar', 'musica', 'piscadinha', 'surpreso', 'triste',
|
||||
'dormir', 'acordar', ...GESTOS_V3];
|
||||
|
||||
function lerFavoritos(): string[] {
|
||||
try {
|
||||
const v = JSON.parse(localStorage.getItem(CHAVE_FAVORITOS) || 'null');
|
||||
if (Array.isArray(v) && v.every((x) => typeof x === 'string')) {
|
||||
if (!localStorage.getItem(CHAVE_V3)) {
|
||||
// Quem já tinha favoritos ganha o bravo de volta e os gestos novos no fim da lista.
|
||||
const lista = [...v, ...GESTOS_V3.filter((g) => !v.includes(g))].slice(0, MAX_FAVORITOS);
|
||||
localStorage.setItem(CHAVE_FAVORITOS, JSON.stringify(lista));
|
||||
localStorage.setItem(CHAVE_V3, '1');
|
||||
localStorage.setItem(CHAVE_MAOS, '1');
|
||||
return lista;
|
||||
}
|
||||
if (localStorage.getItem(CHAVE_MAOS)) return v.slice(0, MAX_FAVORITOS);
|
||||
// Quem já tinha favoritos salvos ganha os gestos com mãos logo depois dos dois primeiros.
|
||||
const novos = GESTOS_MAOS.filter((g) => !v.includes(g));
|
||||
@@ -61,6 +89,7 @@ class Favoritos {
|
||||
try {
|
||||
localStorage.setItem(CHAVE_FAVORITOS, JSON.stringify(this.lista));
|
||||
localStorage.setItem(CHAVE_MAOS, '1');
|
||||
localStorage.setItem(CHAVE_V3, '1');
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
+26
-3
@@ -134,9 +134,15 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
titulo: 'Gestos',
|
||||
emoji: '😉',
|
||||
onde: 'Ao vivo › Gestos e Reações › Gestos do rosto',
|
||||
resumo: 'Animações prontas do rosto: feliz, surpreso, acenar, esconder o rosto com as mãos, dançar e outras.',
|
||||
resumo: 'Animações prontas do rosto: feliz, surpreso, bravo, acenar, esconder o rosto com as mãos, dançar e outras.',
|
||||
itens: [
|
||||
{ emoji: '⭐', nome: 'Favoritos', texto: 'Até 12 gestos fixados com a estrela. Aparecem no Ao vivo.' },
|
||||
{ emoji: '⭐', nome: 'Favoritos', texto: 'Até 20 gestos fixados com a estrela. Aparecem no Ao vivo.' },
|
||||
{ emoji: '😂', nome: 'Rir', texto: 'Olhos fechadinhos e boca aberta, pulando de rir.' },
|
||||
{ emoji: '🥳', nome: 'Comemorar', texto: 'Pula trocando de cor.' },
|
||||
{ emoji: '😳', nome: 'Tímido', texto: 'Olha para baixo e para o lado, cor-de-rosa.' },
|
||||
{ emoji: '😕', nome: 'Confuso', texto: 'Um olho meio fechado e a cabeça indo de um lado para o outro.' },
|
||||
{ emoji: '🥱', nome: 'Bocejar', texto: 'Olhos pesados e boca aberta. Aparece sozinho quando ele está cansado ou entediado.' },
|
||||
{ emoji: '😵💫', nome: 'Tonto', texto: 'Os olhos dão voltas.' },
|
||||
{ emoji: '⌨️', nome: 'Teclas 1 a 9', texto: 'Disparam os nove primeiros favoritos.' },
|
||||
{ emoji: '🔍', nome: 'Ctrl+K', texto: 'Busca qualquer gesto, rosto, cor ou tela pelo nome.' },
|
||||
],
|
||||
@@ -157,6 +163,23 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
],
|
||||
dica: 'Boca, sobrancelhas e íris ligam e desligam em Ajustes › Tela e personalidade.',
|
||||
},
|
||||
atividades: {
|
||||
titulo: 'Atividades (momentos dele)',
|
||||
emoji: '🎮',
|
||||
onde: 'Ao vivo › Gestos › Atividades',
|
||||
resumo:
|
||||
'De vez em quando ele faz alguma coisa: os olhos sobem e encolhem, e embaixo aparece um bonequinho dele, na cor dos olhos, fazendo a atividade. Os olhos grandes acompanham (seguem a bola, leem as linhas, olham a TV).',
|
||||
itens: [
|
||||
{ emoji: '📖', nome: 'Ler', texto: 'Sentado com um livro; vira a página de vez em quando e os olhos leem linha a linha.' },
|
||||
{ emoji: '🎤', nome: 'Cantar', texto: 'Com microfone, balançando, notas subindo e boca cantando.' },
|
||||
{ emoji: '⚽', nome: 'Jogar bola', texto: 'Faz embaixadinha; os olhos seguem a bola.' },
|
||||
{ emoji: '🎮', nome: 'Videogame', texto: 'Sentado com o controle na frente de uma TV com um joguinho; comemora quando ganha.' },
|
||||
{ emoji: '🎨', nome: 'Pintar', texto: 'No cavalete; o quadro vai ganhando pinceladas coloridas.' },
|
||||
{ emoji: '🏋️', nome: 'Malhar', texto: 'Levanta halteres e sua.' },
|
||||
{ emoji: '🐣', nome: 'No modo vida', texto: 'Ele escolhe sozinho pelo humor: feliz canta e joga bola; tranquilo lê e pinta; entediado joga videogame. Nunca de madrugada.' },
|
||||
{ emoji: '👆', nome: 'Parar', texto: 'Botão Parar no painel ou um toque na tela do celular. Dormir também encerra a atividade.' },
|
||||
],
|
||||
},
|
||||
personalidade: {
|
||||
titulo: 'IA local (Qwen)',
|
||||
emoji: '🧠',
|
||||
@@ -280,7 +303,7 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
|
||||
/** Ordem do tutorial (página Guia). */
|
||||
export const ORDEM_GUIA = [
|
||||
'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'aparencia',
|
||||
'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'aparencia',
|
||||
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
|
||||
'reacoes', 'conversa', 'movimento', 'atalhos',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Registro da command palette (Ctrl+K): cada parte do painel contribui com itens. */
|
||||
import {
|
||||
cor, CORES, formato, FORMATOS_INFO, gesto, GESTOS_INFO, gestosDisponiveis, nomeRosto, pararTudo, rearmar, recarregarCelular, rosto,
|
||||
atividade, ATIVIDADES_INFO, cor, CORES, formato, FORMATOS_INFO, gesto, GESTOS_INFO, gestosDisponiveis, nomeRosto, pararTudo, rearmar, recarregarCelular, rosto,
|
||||
} from './comandos.svelte';
|
||||
import { estado } from './estado.svelte';
|
||||
import { ORDEM_GUIA, TOPICOS } from './guia';
|
||||
@@ -37,6 +37,10 @@ export function itensDaPaleta(): ItemPaleta[] {
|
||||
registrarNaPaleta(() => [
|
||||
...ROTAS.map((r) => ({ id: 'ir-' + r.id, grupo: 'Ir para', titulo: r.titulo, fazer: () => nav.ir(r.id) })),
|
||||
{ id: 'ir-rosto', grupo: 'Ir para', titulo: 'Rosto (estilo, formato e cor)', fazer: () => nav.ir('rosto') },
|
||||
...Object.entries(ATIVIDADES_INFO).map(([id, a]) => ({
|
||||
id: 'atividade-' + id, grupo: 'Atividades', titulo: a.emoji + ' ' + a.nome, fazer: () => atividade(id),
|
||||
})),
|
||||
{ id: 'atividade-parar', grupo: 'Atividades', titulo: '⏹ Parar a atividade', fazer: () => atividade('parar') },
|
||||
...ORDEM_GUIA.map((t) => ({
|
||||
id: 'guia-' + t,
|
||||
grupo: 'Guia',
|
||||
|
||||
@@ -99,5 +99,5 @@ export interface Vida {
|
||||
ultimo_emoji: number | null;
|
||||
/** '' quando pode inventar figurinha agora; senão, o motivo. */
|
||||
pode_criar: string;
|
||||
diario: { horario: number; tipo: 'acao' | 'sono' | 'emoji' | 'cuidado' | 'vida'; texto: string }[];
|
||||
diario: { horario: number; tipo: 'acao' | 'sono' | 'emoji' | 'cuidado' | 'vida' | 'atividade'; texto: string }[];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Brain, Camera, Download, Eye, Footprints, Hand, History, Palette, PenLine, RefreshCw, Repeat, ScanFace, Settings2, Smile, Sparkles, Video,
|
||||
Brain, Camera, Download, Eye, Footprints, Gamepad2, Hand, History, Palette, PenLine, RefreshCw, Repeat, ScanFace, Settings2, Smile, Sparkles, Video,
|
||||
} from '@lucide/svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { postar } from '../lib/api';
|
||||
import { salvarApp } from '../lib/ajustes.svelte';
|
||||
import { tentar } from '../lib/avisos.svelte';
|
||||
import { FORMATOS_INFO, hex, nomeRosto, recarregarCelular, ROSTOS_INFO } from '../lib/comandos.svelte';
|
||||
import { atividade, ATIVIDADES_INFO, FORMATOS_INFO, hex, nomeRosto, recarregarCelular, ROSTOS_INFO } from '../lib/comandos.svelte';
|
||||
import Celular from '../lib/componentes/Celular.svelte';
|
||||
import Gestos from '../lib/componentes/Gestos.svelte';
|
||||
import Grupo from '../lib/componentes/Grupo.svelte';
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
// Palco: câmera grande e rosto no quadrinho do canto; tocar no quadrinho troca os dois.
|
||||
let rostoGrande = $state(false);
|
||||
// Atividade que o celular está fazendo agora (vem na telemetria).
|
||||
const atividadeAgora = $derived((estado.app?.online && (estado.app.estado?.atividade as string | null)) || null);
|
||||
|
||||
onMount(() => {
|
||||
video = new VideoAoVivo(elVideo);
|
||||
@@ -165,6 +167,23 @@ const pirAtivo = $derived(
|
||||
<Linha icone={Smile} cor="#ffcc00" rotulo="Favoritos">
|
||||
<Gestos />
|
||||
</Linha>
|
||||
<Linha
|
||||
icone={Gamepad2}
|
||||
cor="#30b0c7"
|
||||
rotulo="Atividades"
|
||||
detalhe={atividadeAgora ? `Agora: ${ATIVIDADES_INFO[atividadeAgora]?.emoji ?? ''} ${ATIVIDADES_INFO[atividadeAgora]?.nome ?? atividadeAgora}` : 'Um bonequinho dele faz algo embaixo dos olhos'}
|
||||
>
|
||||
<div class="atividades">
|
||||
{#each Object.entries(ATIVIDADES_INFO) as [id, a] (id)}
|
||||
<button class="btn pequeno" class:ativo={atividadeAgora === id} onclick={() => atividade(id)}>
|
||||
<span aria-hidden="true">{a.emoji}</span> {a.nome}
|
||||
</button>
|
||||
{/each}
|
||||
{#if atividadeAgora}
|
||||
<button class="btn pequeno" onclick={() => atividade('parar')}>⏹ Parar</button>
|
||||
{/if}
|
||||
</div>
|
||||
</Linha>
|
||||
<Linha icone={Sparkles} cor="#ff2d55" rotulo="Todos os gestos e reações" href="#/reacoes" />
|
||||
</Grupo>
|
||||
|
||||
@@ -219,6 +238,15 @@ const pirAtivo = $derived(
|
||||
gap: var(--e-4);
|
||||
align-items: start;
|
||||
}
|
||||
.atividades {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--e-2);
|
||||
}
|
||||
.atividades .btn.ativo {
|
||||
border-color: var(--destaque);
|
||||
background: var(--destaque-suave);
|
||||
}
|
||||
/* ---------- palco: sempre visível */
|
||||
.palco {
|
||||
position: sticky;
|
||||
|
||||
Reference in New Issue
Block a user