Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
319e63cc42 | ||
|
|
f550930f56 | ||
|
|
230ca991a2 | ||
|
|
628db8492f |
@@ -304,9 +304,17 @@ function sequencias() {
|
||||
[{ forma: "aberto", cor: CIANO, dy: 0 }, 0.3]];
|
||||
const piscadinha = [[{ forma2: "dormir", esc: 1.04, boca: "sorriso" }, 0.12], [{ esc: 1.08 }, 0.5],
|
||||
[{ forma2: "aberto", esc: 1.0 }, 0.18]];
|
||||
// Gestos com mãos: o motor desenha as mãos com a mesma duração (rosto-motor.js, DUR_MAOS).
|
||||
const esconder = [[{ forma: "aberto", esc: 0.95, dy: 4 }, 0.3], [{}, 1.6], [{ forma: "feliz", boca: "sorriso", esc: 1.05, dy: 0 }, 0.3], [{ forma: "aberto", esc: 1.0 }, 0.4]];
|
||||
const espiar = [[{ esc: 0.96 }, 0.4], [{}, 0.6], [{ dx: -14, forma: "aberto", ab: 1.08 }, 0.25], [{}, 0.9], [{ dx: 0, ab: 1.0 }, 0.35], [{ forma: "feliz", boca: "sorriso" }, 0.4], [{ forma: "aberto" }, 0.3]];
|
||||
const acenar = [[{ forma: "feliz", boca: "sorriso", esc: 1.05 }, 0.3], [{}, 1.7], [{ forma: "aberto", esc: 1.0 }, 0.4]];
|
||||
const pensar = [[{ dy: -10, dx: 14, boca: "neutra" }, 0.4], [{}, 2.0], [{ dy: 0, dx: 0 }, 0.4]];
|
||||
const musica = [[{ forma: "feliz", boca: "sorriso", dy: -5 }, 0.25], [{ dy: 5 }, 0.25], [{ dy: -5 }, 0.25], [{ dy: 5 }, 0.25],
|
||||
[{ 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 { piscar, piscar2, olhar_esq, olhar_dir, olhar_baixo, feliz, surpreso,
|
||||
return { esconder, espiar, acenar, pensar, musica,
|
||||
piscar, piscar2, olhar_esq, olhar_dir, olhar_baixo, feliz, surpreso,
|
||||
bravo, coracao, triste, piscadinha, alegre };
|
||||
}
|
||||
|
||||
@@ -394,6 +402,7 @@ function desenhar() {
|
||||
window.MotorRosto.desenhar({
|
||||
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
|
||||
icone: !r.dormindo && !window.carinhaOlhosDesenho ? window.carinhaOlhosIcone || null : null,
|
||||
carga: window.carinhaBateria || null,
|
||||
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
|
||||
parado: !r.kf && !seguindo && !r.dormindo,
|
||||
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
||||
@@ -468,6 +477,10 @@ function trata(ordem) {
|
||||
} else if (ordem === "aleatorio") {
|
||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||
} else if (SEQS[ordem]) {
|
||||
if (window.MotorRosto) {
|
||||
if (ordem === "musica") window.MotorRosto.faixa("equalizador", { segundos: 2.3 });
|
||||
else window.MotorRosto.maos(ordem);
|
||||
}
|
||||
animar(ordem);
|
||||
} else if (ordem === "calma") {
|
||||
// E-Stop do painel: interrompe a animação e volta ao rosto neutro.
|
||||
@@ -543,6 +556,7 @@ if (MODO_PAINEL) {
|
||||
const m = e.data;
|
||||
if (m.tipo === 'ordem' && typeof m.ordem === 'string') trata(m.ordem);
|
||||
else if (m.tipo === 'config' && m.config) configurarApp(m.config);
|
||||
else if (m.tipo === 'legenda' && typeof m.texto === 'string' && window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
||||
});
|
||||
} else if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
||||
|
||||
@@ -1051,6 +1065,7 @@ setTimeout(relatar, 1500);
|
||||
const s=await response.json(),m=s.ultima;if(!m){seen=null;return;}
|
||||
if(m.id===seen||Date.now()/1000-m.horario>60)return;seen=m.id;
|
||||
MIC.ultimoSom=performance.now()/1000;trata('acordar');trata(m.gesto);
|
||||
if(window.MotorRosto)window.MotorRosto.faixa('legenda',{texto:m.texto});
|
||||
}catch(e){}finally{fetching=false;}
|
||||
}
|
||||
setInterval(poll,1000);poll();
|
||||
@@ -1091,10 +1106,15 @@ if(new URLSearchParams(location.search).get('aparelho')==='1' && new URLSearchPa
|
||||
ouvir('recarregar', () => { if (aparelho) trata('recarregar'); });
|
||||
ouvir('app_config', (c) => configurarApp(c));
|
||||
ouvir('conversa', (s) => {
|
||||
if (window.MotorRosto) {
|
||||
if (s.ocupado) window.MotorRosto.faixa('digitando');
|
||||
else window.MotorRosto.faixa(null, { so: 'digitando' });
|
||||
}
|
||||
const m = s.ultima;
|
||||
if (!m || m.id === ultimaConversa || Date.now() / 1000 - m.horario > 60) return;
|
||||
ultimaConversa = m.id;
|
||||
MIC.ultimoSom = performance.now() / 1000; trata('acordar'); trata(m.gesto);
|
||||
if (window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
||||
});
|
||||
ouvir('reacao', (d) => {
|
||||
if (!d || d.id === ultimaReacao || Date.now() / 1000 - d.horario > 60) return;
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
bctx.clearRect(0, 0, colunas, linhas);
|
||||
bctx.save(); bctx.scale(f, f);
|
||||
olhos.forEach((p, i) => olhoPlano(bctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(bctx, o, olhos, tela.height / d);
|
||||
bctx.restore();
|
||||
const px = bctx.getImageData(0, 0, colunas, linhas).data;
|
||||
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, tela.width, tela.height);
|
||||
@@ -239,15 +240,198 @@
|
||||
ry: p.ry * d,
|
||||
}));
|
||||
ctx.clearRect(0, 0, tela.width, tela.height);
|
||||
if (estilo.pontos) return desenharPontos(estilo, o, olhos.map((p) => Object.assign({}, p, { cx: p.cx / d, cy: p.cy / d, ry: p.ry / d })), d);
|
||||
if (estilo.lente) { olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i])); return; }
|
||||
if (estilo.pontos) {
|
||||
desenharPontos(estilo, o, olhos.map((p) => Object.assign({}, p, { cx: p.cx / d, cy: p.cy / d, ry: p.ry / d })), d);
|
||||
} else if (estilo.lente) {
|
||||
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(ctx, o, olhos, tela.height);
|
||||
} else {
|
||||
cctx.clearRect(0, 0, camada.width, camada.height);
|
||||
olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(cctx, o, olhos, tela.height);
|
||||
ctx.save();
|
||||
if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); }
|
||||
ctx.drawImage(camada, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
desenharFaixa(ctx, o, tela.width, tela.height);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ mãos
|
||||
// Mãozinhas na cor dos olhos que sobem da borda da tela. Duração casada com as sequências
|
||||
// de rosto de mesmo nome no carinha.html.
|
||||
const DUR_MAOS = { esconder: 2.6, espiar: 3.2, acenar: 2.4, pensar: 2.8 };
|
||||
const maos = { modo: null, t0: null };
|
||||
const suave = (x) => { x = Math.max(0, Math.min(1, x)); return x * x * (3 - 2 * x); };
|
||||
const faixaDe = (k, a, b) => suave((k - a) / (b - a)); // 0→1 entre a e b
|
||||
|
||||
// Mão de desenho animado: palma, quatro dedos (o do meio maior), polegar para o lado do rosto
|
||||
// e um punho de robô. dobra (0–1) fecha os dedos; mexe (0–1) agita os dedos ao acenar.
|
||||
// O contorno é traçado antes do preenchimento: assim só a borda externa aparece.
|
||||
function capsula(c, x0, y0, x1, y1, r) {
|
||||
const a = Math.atan2(y1 - y0, x1 - x0) + Math.PI / 2;
|
||||
c.moveTo(x0 + Math.cos(a) * r, y0 + Math.sin(a) * r);
|
||||
c.arc(x0, y0, r, a, a + Math.PI);
|
||||
c.arc(x1, y1, r, a + Math.PI, a + 2 * Math.PI);
|
||||
c.closePath();
|
||||
}
|
||||
|
||||
function formaMao(c, s, dobra, mexe, t, indicador) {
|
||||
c.beginPath();
|
||||
// punho (manga do robô)
|
||||
caixa(c, -0.42 * s, 0.5 * s, 0.84 * s, 0.38 * s, 0.12 * s);
|
||||
// palma
|
||||
caixa(c, -0.5 * s, -0.36 * s, 1.0 * s, 0.95 * s, 0.34 * s);
|
||||
// dedos: x da base, comprimento e abertura
|
||||
// mindinho, anelar, médio e indicador (o indicador fica do lado do polegar)
|
||||
const dedos = [[-0.355, 0.5, -0.16], [-0.12, 0.68, -0.05], [0.12, 0.74, 0.05], [0.355, 0.66, 0.14]];
|
||||
dedos.forEach(([x, comp, abre], i) => {
|
||||
const onda = mexe ? Math.sin(t * 14 + i * 0.9) * 0.06 * mexe : 0;
|
||||
const fecha = indicador && i === 3 ? 0 : dobra; // "pensando": só o indicador esticado
|
||||
const ang = abre + onda, l = comp * (1 - 0.7 * fecha) * s;
|
||||
const bx = x * s, by = -0.26 * s;
|
||||
capsula(c, bx, by, bx + Math.sin(ang) * l, by - Math.cos(ang) * l, 0.115 * s);
|
||||
});
|
||||
// polegar, saindo para o lado
|
||||
capsula(c, 0.38 * s, 0.2 * s, (0.38 + 0.42 * (1 - 0.4 * dobra)) * s, (0.2 - 0.34) * s, 0.13 * s);
|
||||
}
|
||||
|
||||
function mao(c, o, x, y, s, ang, espelho, dobra, mexe, indicador, pulso, costas) {
|
||||
dobra = dobra || 0; mexe = mexe || 0;
|
||||
s *= 0.82;
|
||||
const escura = o.cor.map((v) => v * 0.34);
|
||||
c.save();
|
||||
c.translate(x, y); c.rotate(ang); c.scale(espelho, 1);
|
||||
if (pulso) c.translate(0, -0.7 * s); // (x, y) é o punho: gira como uma mão de verdade
|
||||
c.lineJoin = 'round'; c.lineCap = 'round';
|
||||
formaMao(c, s, dobra, mexe, o.t || 0, indicador);
|
||||
c.strokeStyle = css(o.cor); c.lineWidth = s * 0.17; c.stroke(); // contorno (metade some sob o preenchimento)
|
||||
c.fillStyle = css(escura); c.fill();
|
||||
// detalhes por dentro, finos: linha da palma, vincos dos dedos e a costura do punho
|
||||
c.strokeStyle = css(o.cor, 0.55); c.lineWidth = s * 0.045;
|
||||
if (costas) { // dorso da mão: nós dos dedos em vez da linha da palma
|
||||
for (const nx of [-0.355, -0.12, 0.12, 0.355]) { c.beginPath(); c.arc(nx * s, -0.2 * s, 0.075 * s, Math.PI * 1.15, Math.PI * 1.85); c.stroke(); }
|
||||
} else {
|
||||
c.beginPath(); c.moveTo(-0.3 * s, 0.12 * s); c.quadraticCurveTo(0.02 * s, 0.26 * s, 0.3 * s, 0.06 * s); c.stroke();
|
||||
}
|
||||
c.beginPath(); c.moveTo(-0.36 * s, 0.6 * s); c.lineTo(0.36 * s, 0.6 * s); c.stroke();
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// Sobe com um leve "passa e volta" no fim (como um braço que freia).
|
||||
const voltinha = (x) => { x = Math.max(0, Math.min(1, x)); const k = 1.6; return 1 + (k + 1) * Math.pow(x - 1, 3) + k * Math.pow(x - 1, 2); };
|
||||
|
||||
function desenharMaos(c, o, olhos, H) {
|
||||
if (!maos.modo) return;
|
||||
if (maos.t0 === null) maos.t0 = o.t;
|
||||
const k = (o.t - maos.t0) / DUR_MAOS[maos.modo];
|
||||
if (k >= 1) { maos.modo = null; return; }
|
||||
const [e, d] = olhos.slice().sort((a, b) => a.cx - b.cx); // olho da esquerda e da direita na tela
|
||||
const s = e.ry * 0.95, fora = H + s * 1.8;
|
||||
if (maos.modo === 'esconder' || maos.modo === 'espiar') {
|
||||
// Cada mão vem do canto de baixo, por fora, num arco, girando até ficar inclinada para dentro.
|
||||
// A da direita chega um pouco depois: duas mãos nunca se mexem igualzinho.
|
||||
const cobrir = (olho, lado, atraso) => {
|
||||
const ida = voltinha((k - atraso) / 0.24), volta = faixaDe(k, 0.8 + atraso * 0.5, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); // 0 = fora da tela, 1 = cobrindo
|
||||
const alvoX = olho.cx - lado * olho.ry * 0.05, alvoY = olho.cy + olho.ry * 0.38;
|
||||
const x = alvoX - lado * olho.ry * 1.6 * (1 - Math.min(1, v));
|
||||
const y = fora + (alvoY - fora) * v;
|
||||
const respira = Math.sin(o.t * 3 + lado) * s * 0.03 * Math.min(1, v);
|
||||
const ang = lado * (0.42 + 0.55 * (1 - Math.min(1, v))); // gira enquanto sobe, termina a ~24°
|
||||
return { x, y: y + respira, ang };
|
||||
};
|
||||
const pe = cobrir(e, 1, 0), pd = cobrir(d, -1, 0.05);
|
||||
if (maos.modo === 'espiar') {
|
||||
// a mão da esquerda gira pelo punho para fora, como uma porta, e o olho aparece
|
||||
const abre = faixaDe(k, 0.3, 0.44) - faixaDe(k, 0.64, 0.76);
|
||||
pe.ang -= abre * 1.05; pe.x -= abre * e.ry * 0.35; pe.y += abre * e.ry * 0.2;
|
||||
}
|
||||
const treme = Math.sin(o.t * 22) * s * 0.012; // tímido: tremidinha
|
||||
// dorso virado para quem olha: polegar para fora, perto da bochecha
|
||||
mao(c, o, pe.x + treme, pe.y, s * 1.1, pe.ang, -1, 0, 0, false, true, true);
|
||||
mao(c, o, pd.x - treme, pd.y, s * 1.1, pd.ang, 1, 0, 0, false, true, true);
|
||||
} else if (maos.modo === 'acenar') {
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
const x = d.cx + d.ry * 2.2 + d.ry * 0.6 * (1 - Math.min(1, v)), y0 = d.cy + d.ry * 1.55;
|
||||
const y = fora + (y0 - fora) * v;
|
||||
const balanco = k > 0.22 && k < 0.82 ? Math.sin((k - 0.22) * Math.PI * 7) * 0.32 : 0;
|
||||
mao(c, o, x, y, s * 0.95, -0.18 + balanco, -1, 0, 1, false, true);
|
||||
} else if (maos.modo === 'pensar') {
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
const x = (e.cx + d.cx) / 2 + e.ry * 1.05, y0 = e.cy + e.ry * 1.4;
|
||||
const y = fora + (y0 - fora) * v + Math.sin(o.t * 9) * s * 0.03;
|
||||
mao(c, o, x, y, s * 0.85, -0.5, -1, 0.95, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- faixa de baixo
|
||||
// A parte de baixo da tela vira palco: legenda da conversa, pensando, equalizador e carga.
|
||||
const faixa = { modo: null, texto: '', t0: null, dur: 0 };
|
||||
|
||||
function definirFaixa(modo, opcoes) {
|
||||
opcoes = opcoes || {};
|
||||
if (!modo) {
|
||||
if (!opcoes.so || faixa.modo === opcoes.so) faixa.modo = null; // so: só apaga esse modo
|
||||
return;
|
||||
}
|
||||
faixa.modo = modo; faixa.t0 = performance.now() / 1000; faixa.texto = String(opcoes.texto || '').replace(/\s+/g, ' ').trim().slice(0, 400);
|
||||
faixa.dur = opcoes.segundos || (modo === 'legenda' ? Math.max(3, Math.min(14, faixa.texto.length / 14 + 2.5)) : modo === 'digitando' ? 20 : 2.6);
|
||||
}
|
||||
|
||||
function desenharFaixa(c, o, W, H) {
|
||||
const y = H * 0.955, fonte = Math.round(H * 0.042);
|
||||
c.save();
|
||||
c.shadowColor = css(o.cor, 0.6); c.shadowBlur = H * 0.02;
|
||||
if (o.carga && o.carga.carregando && typeof o.carga.nivel === 'number') { // carregando: canto direito
|
||||
const n = Math.max(0, Math.min(100, o.carga.nivel)), bw = H * 0.075, bh = H * 0.036, bx = W - H * 0.13, by = y - bh / 2;
|
||||
c.globalAlpha = 0.55 + 0.25 * Math.sin(o.t * 2.5);
|
||||
c.strokeStyle = css(o.cor); c.lineWidth = Math.max(1, H * 0.005);
|
||||
caixa(c, bx, by, bw, bh, bh * 0.25); c.stroke();
|
||||
c.fillStyle = css(o.cor); c.fillRect(bx + bw, y - bh * 0.2, bh * 0.15, bh * 0.4);
|
||||
c.fillRect(bx + bh * 0.12, by + bh * 0.15, (bw - bh * 0.24) * n / 100, bh * 0.7);
|
||||
c.font = '600 ' + Math.round(fonte * 0.8) + 'px system-ui,sans-serif'; c.textAlign = 'right'; c.textBaseline = 'middle';
|
||||
const tx = bx - H * 0.012;
|
||||
c.fillText(Math.round(n) + '%', tx, y);
|
||||
const rx = tx - c.measureText(Math.round(n) + '%').width - H * 0.022, r = H * 0.016; // raio desenhado na cor dos olhos
|
||||
c.beginPath(); c.moveTo(rx + r * 0.3, y - r); c.lineTo(rx - r * 0.5, y + r * 0.15); c.lineTo(rx, y + r * 0.15);
|
||||
c.lineTo(rx - r * 0.3, y + r); c.lineTo(rx + r * 0.5, y - r * 0.15); c.lineTo(rx, y - r * 0.15); c.closePath(); c.fill();
|
||||
c.globalAlpha = 1;
|
||||
}
|
||||
if (faixa.modo) {
|
||||
if (faixa.t0 === null) faixa.t0 = o.t;
|
||||
const k = (o.t - faixa.t0) / faixa.dur;
|
||||
if (k >= 1) faixa.modo = null;
|
||||
else {
|
||||
const some = Math.min(1, (faixa.dur - (o.t - faixa.t0)) / 0.4);
|
||||
c.globalAlpha = Math.max(0, some);
|
||||
c.fillStyle = css(o.cor);
|
||||
if (faixa.modo === 'legenda') {
|
||||
// efeito de digitação; se não couber, mostra o final do que já foi "dito"
|
||||
let txt = faixa.texto.slice(0, Math.floor((o.t - faixa.t0) * 30));
|
||||
c.font = '500 ' + fonte + 'px system-ui,sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle';
|
||||
const max = W * 0.8;
|
||||
while (txt.length > 1 && c.measureText(txt).width > max) txt = txt.slice(1);
|
||||
c.fillText(txt, W / 2, y);
|
||||
} else if (faixa.modo === 'digitando') {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const pulo = Math.max(0, Math.sin(o.t * 7 - i * 0.7)) * H * 0.012;
|
||||
c.beginPath(); c.arc(W / 2 + (i - 1) * H * 0.035, y - pulo, H * 0.011, 0, Math.PI * 2); c.fill();
|
||||
}
|
||||
} else if (faixa.modo === 'equalizador') {
|
||||
const n = 15, larg = H * 0.016, passo = H * 0.026;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = 0.25 + 0.75 * Math.abs(Math.sin(o.t * (5 + i % 4) + i * 1.3)) * Math.sin(Math.PI * (i + 0.5) / n);
|
||||
const h = H * 0.04 * a;
|
||||
caixa(c, W / 2 + (i - (n - 1) / 2) * passo - larg / 2, H * 0.988 - h, larg, h, larg / 2); c.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrato parado num canvas qualquer (miniaturas da biblioteca no painel).
|
||||
@@ -285,5 +469,8 @@
|
||||
|
||||
function esconder() { if (tela) tela.style.display = 'none'; antes = 0; }
|
||||
|
||||
window.MotorRosto = { desenhar, retrato, esconder, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
||||
window.MotorRosto = {
|
||||
desenhar, retrato, esconder,
|
||||
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
|
||||
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
||||
})();
|
||||
|
||||
+21
-1
@@ -304,9 +304,17 @@ function sequencias() {
|
||||
[{ forma: "aberto", cor: CIANO, dy: 0 }, 0.3]];
|
||||
const piscadinha = [[{ forma2: "dormir", esc: 1.04, boca: "sorriso" }, 0.12], [{ esc: 1.08 }, 0.5],
|
||||
[{ forma2: "aberto", esc: 1.0 }, 0.18]];
|
||||
// Gestos com mãos: o motor desenha as mãos com a mesma duração (rosto-motor.js, DUR_MAOS).
|
||||
const esconder = [[{ forma: "aberto", esc: 0.95, dy: 4 }, 0.3], [{}, 1.6], [{ forma: "feliz", boca: "sorriso", esc: 1.05, dy: 0 }, 0.3], [{ forma: "aberto", esc: 1.0 }, 0.4]];
|
||||
const espiar = [[{ esc: 0.96 }, 0.4], [{}, 0.6], [{ dx: -14, forma: "aberto", ab: 1.08 }, 0.25], [{}, 0.9], [{ dx: 0, ab: 1.0 }, 0.35], [{ forma: "feliz", boca: "sorriso" }, 0.4], [{ forma: "aberto" }, 0.3]];
|
||||
const acenar = [[{ forma: "feliz", boca: "sorriso", esc: 1.05 }, 0.3], [{}, 1.7], [{ forma: "aberto", esc: 1.0 }, 0.4]];
|
||||
const pensar = [[{ dy: -10, dx: 14, boca: "neutra" }, 0.4], [{}, 2.0], [{ dy: 0, dx: 0 }, 0.4]];
|
||||
const musica = [[{ forma: "feliz", boca: "sorriso", dy: -5 }, 0.25], [{ dy: 5 }, 0.25], [{ dy: -5 }, 0.25], [{ dy: 5 }, 0.25],
|
||||
[{ 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 { piscar, piscar2, olhar_esq, olhar_dir, olhar_baixo, feliz, surpreso,
|
||||
return { esconder, espiar, acenar, pensar, musica,
|
||||
piscar, piscar2, olhar_esq, olhar_dir, olhar_baixo, feliz, surpreso,
|
||||
bravo, coracao, triste, piscadinha, alegre };
|
||||
}
|
||||
|
||||
@@ -394,6 +402,7 @@ function desenhar() {
|
||||
window.MotorRosto.desenhar({
|
||||
estilo: ROSTO.motor, formato: FORMATO, t: tAgora, cor: r.cor, dormindo: r.dormindo,
|
||||
icone: !r.dormindo && !window.carinhaOlhosDesenho ? window.carinhaOlhosIcone || null : null,
|
||||
carga: window.carinhaBateria || null,
|
||||
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
|
||||
parado: !r.kf && !seguindo && !r.dormindo,
|
||||
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
||||
@@ -468,6 +477,10 @@ function trata(ordem) {
|
||||
} else if (ordem === "aleatorio") {
|
||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||
} else if (SEQS[ordem]) {
|
||||
if (window.MotorRosto) {
|
||||
if (ordem === "musica") window.MotorRosto.faixa("equalizador", { segundos: 2.3 });
|
||||
else window.MotorRosto.maos(ordem);
|
||||
}
|
||||
animar(ordem);
|
||||
} else if (ordem === "calma") {
|
||||
// E-Stop do painel: interrompe a animação e volta ao rosto neutro.
|
||||
@@ -543,6 +556,7 @@ if (MODO_PAINEL) {
|
||||
const m = e.data;
|
||||
if (m.tipo === 'ordem' && typeof m.ordem === 'string') trata(m.ordem);
|
||||
else if (m.tipo === 'config' && m.config) configurarApp(m.config);
|
||||
else if (m.tipo === 'legenda' && typeof m.texto === 'string' && window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
||||
});
|
||||
} else if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
||||
|
||||
@@ -1051,6 +1065,7 @@ setTimeout(relatar, 1500);
|
||||
const s=await response.json(),m=s.ultima;if(!m){seen=null;return;}
|
||||
if(m.id===seen||Date.now()/1000-m.horario>60)return;seen=m.id;
|
||||
MIC.ultimoSom=performance.now()/1000;trata('acordar');trata(m.gesto);
|
||||
if(window.MotorRosto)window.MotorRosto.faixa('legenda',{texto:m.texto});
|
||||
}catch(e){}finally{fetching=false;}
|
||||
}
|
||||
setInterval(poll,1000);poll();
|
||||
@@ -1091,10 +1106,15 @@ if(new URLSearchParams(location.search).get('aparelho')==='1' && new URLSearchPa
|
||||
ouvir('recarregar', () => { if (aparelho) trata('recarregar'); });
|
||||
ouvir('app_config', (c) => configurarApp(c));
|
||||
ouvir('conversa', (s) => {
|
||||
if (window.MotorRosto) {
|
||||
if (s.ocupado) window.MotorRosto.faixa('digitando');
|
||||
else window.MotorRosto.faixa(null, { so: 'digitando' });
|
||||
}
|
||||
const m = s.ultima;
|
||||
if (!m || m.id === ultimaConversa || Date.now() / 1000 - m.horario > 60) return;
|
||||
ultimaConversa = m.id;
|
||||
MIC.ultimoSom = performance.now() / 1000; trata('acordar'); trata(m.gesto);
|
||||
if (window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
||||
});
|
||||
ouvir('reacao', (d) => {
|
||||
if (!d || d.id === ultimaReacao || Date.now() / 1000 - d.horario > 60) return;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import time
|
||||
from urllib.request import Request,urlopen
|
||||
from comum import gravar_atomico, ler_json
|
||||
|
||||
GESTOS=('piscar','piscadinha','feliz','surpreso','coracao','olhar_esq','olhar_dir','olhar_baixo')
|
||||
GESTOS=('piscar','piscadinha','feliz','surpreso','coracao','olhar_esq','olhar_dir','olhar_baixo','acenar','esconder','pensar')
|
||||
EVENTOS=('som','movimento','saudacao','sozinho')
|
||||
def validar_intencao(data):
|
||||
if not isinstance(data,dict) or set(data)!= {'gesto'} or data['gesto'] not in GESTOS:
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@ FORMATOS = ["padrao", "redondo", "pilula", "quadrado", "largo"]
|
||||
# Espelho dos gestos do carinha.html (para o painel montar os botoes)
|
||||
GESTOS = ["piscar", "piscar2", "olhar_esq", "olhar_dir", "olhar_baixo", "feliz",
|
||||
"surpreso", "bravo", "coracao", "triste", "piscadinha", "alegre",
|
||||
"dormir", "acordar", "aleatorio", "parar"]
|
||||
"dormir", "acordar", "aleatorio", "parar",
|
||||
"esconder", "espiar", "acenar", "pensar", "musica"]
|
||||
# Ordens internas que o rosto entende mas não viram botão.
|
||||
ORDENS_INTERNAS = {"calma"}
|
||||
|
||||
|
||||
+190
-3
@@ -205,6 +205,7 @@
|
||||
bctx.clearRect(0, 0, colunas, linhas);
|
||||
bctx.save(); bctx.scale(f, f);
|
||||
olhos.forEach((p, i) => olhoPlano(bctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(bctx, o, olhos, tela.height / d);
|
||||
bctx.restore();
|
||||
const px = bctx.getImageData(0, 0, colunas, linhas).data;
|
||||
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, tela.width, tela.height);
|
||||
@@ -239,15 +240,198 @@
|
||||
ry: p.ry * d,
|
||||
}));
|
||||
ctx.clearRect(0, 0, tela.width, tela.height);
|
||||
if (estilo.pontos) return desenharPontos(estilo, o, olhos.map((p) => Object.assign({}, p, { cx: p.cx / d, cy: p.cy / d, ry: p.ry / d })), d);
|
||||
if (estilo.lente) { olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i])); return; }
|
||||
if (estilo.pontos) {
|
||||
desenharPontos(estilo, o, olhos.map((p) => Object.assign({}, p, { cx: p.cx / d, cy: p.cy / d, ry: p.ry / d })), d);
|
||||
} else if (estilo.lente) {
|
||||
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(ctx, o, olhos, tela.height);
|
||||
} else {
|
||||
cctx.clearRect(0, 0, camada.width, camada.height);
|
||||
olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(cctx, o, olhos, tela.height);
|
||||
ctx.save();
|
||||
if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); }
|
||||
ctx.drawImage(camada, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
desenharFaixa(ctx, o, tela.width, tela.height);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ mãos
|
||||
// Mãozinhas na cor dos olhos que sobem da borda da tela. Duração casada com as sequências
|
||||
// de rosto de mesmo nome no carinha.html.
|
||||
const DUR_MAOS = { esconder: 2.6, espiar: 3.2, acenar: 2.4, pensar: 2.8 };
|
||||
const maos = { modo: null, t0: null };
|
||||
const suave = (x) => { x = Math.max(0, Math.min(1, x)); return x * x * (3 - 2 * x); };
|
||||
const faixaDe = (k, a, b) => suave((k - a) / (b - a)); // 0→1 entre a e b
|
||||
|
||||
// Mão de desenho animado: palma, quatro dedos (o do meio maior), polegar para o lado do rosto
|
||||
// e um punho de robô. dobra (0–1) fecha os dedos; mexe (0–1) agita os dedos ao acenar.
|
||||
// O contorno é traçado antes do preenchimento: assim só a borda externa aparece.
|
||||
function capsula(c, x0, y0, x1, y1, r) {
|
||||
const a = Math.atan2(y1 - y0, x1 - x0) + Math.PI / 2;
|
||||
c.moveTo(x0 + Math.cos(a) * r, y0 + Math.sin(a) * r);
|
||||
c.arc(x0, y0, r, a, a + Math.PI);
|
||||
c.arc(x1, y1, r, a + Math.PI, a + 2 * Math.PI);
|
||||
c.closePath();
|
||||
}
|
||||
|
||||
function formaMao(c, s, dobra, mexe, t, indicador) {
|
||||
c.beginPath();
|
||||
// punho (manga do robô)
|
||||
caixa(c, -0.42 * s, 0.5 * s, 0.84 * s, 0.38 * s, 0.12 * s);
|
||||
// palma
|
||||
caixa(c, -0.5 * s, -0.36 * s, 1.0 * s, 0.95 * s, 0.34 * s);
|
||||
// dedos: x da base, comprimento e abertura
|
||||
// mindinho, anelar, médio e indicador (o indicador fica do lado do polegar)
|
||||
const dedos = [[-0.355, 0.5, -0.16], [-0.12, 0.68, -0.05], [0.12, 0.74, 0.05], [0.355, 0.66, 0.14]];
|
||||
dedos.forEach(([x, comp, abre], i) => {
|
||||
const onda = mexe ? Math.sin(t * 14 + i * 0.9) * 0.06 * mexe : 0;
|
||||
const fecha = indicador && i === 3 ? 0 : dobra; // "pensando": só o indicador esticado
|
||||
const ang = abre + onda, l = comp * (1 - 0.7 * fecha) * s;
|
||||
const bx = x * s, by = -0.26 * s;
|
||||
capsula(c, bx, by, bx + Math.sin(ang) * l, by - Math.cos(ang) * l, 0.115 * s);
|
||||
});
|
||||
// polegar, saindo para o lado
|
||||
capsula(c, 0.38 * s, 0.2 * s, (0.38 + 0.42 * (1 - 0.4 * dobra)) * s, (0.2 - 0.34) * s, 0.13 * s);
|
||||
}
|
||||
|
||||
function mao(c, o, x, y, s, ang, espelho, dobra, mexe, indicador, pulso, costas) {
|
||||
dobra = dobra || 0; mexe = mexe || 0;
|
||||
s *= 0.82;
|
||||
const escura = o.cor.map((v) => v * 0.34);
|
||||
c.save();
|
||||
c.translate(x, y); c.rotate(ang); c.scale(espelho, 1);
|
||||
if (pulso) c.translate(0, -0.7 * s); // (x, y) é o punho: gira como uma mão de verdade
|
||||
c.lineJoin = 'round'; c.lineCap = 'round';
|
||||
formaMao(c, s, dobra, mexe, o.t || 0, indicador);
|
||||
c.strokeStyle = css(o.cor); c.lineWidth = s * 0.17; c.stroke(); // contorno (metade some sob o preenchimento)
|
||||
c.fillStyle = css(escura); c.fill();
|
||||
// detalhes por dentro, finos: linha da palma, vincos dos dedos e a costura do punho
|
||||
c.strokeStyle = css(o.cor, 0.55); c.lineWidth = s * 0.045;
|
||||
if (costas) { // dorso da mão: nós dos dedos em vez da linha da palma
|
||||
for (const nx of [-0.355, -0.12, 0.12, 0.355]) { c.beginPath(); c.arc(nx * s, -0.2 * s, 0.075 * s, Math.PI * 1.15, Math.PI * 1.85); c.stroke(); }
|
||||
} else {
|
||||
c.beginPath(); c.moveTo(-0.3 * s, 0.12 * s); c.quadraticCurveTo(0.02 * s, 0.26 * s, 0.3 * s, 0.06 * s); c.stroke();
|
||||
}
|
||||
c.beginPath(); c.moveTo(-0.36 * s, 0.6 * s); c.lineTo(0.36 * s, 0.6 * s); c.stroke();
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// Sobe com um leve "passa e volta" no fim (como um braço que freia).
|
||||
const voltinha = (x) => { x = Math.max(0, Math.min(1, x)); const k = 1.6; return 1 + (k + 1) * Math.pow(x - 1, 3) + k * Math.pow(x - 1, 2); };
|
||||
|
||||
function desenharMaos(c, o, olhos, H) {
|
||||
if (!maos.modo) return;
|
||||
if (maos.t0 === null) maos.t0 = o.t;
|
||||
const k = (o.t - maos.t0) / DUR_MAOS[maos.modo];
|
||||
if (k >= 1) { maos.modo = null; return; }
|
||||
const [e, d] = olhos.slice().sort((a, b) => a.cx - b.cx); // olho da esquerda e da direita na tela
|
||||
const s = e.ry * 0.95, fora = H + s * 1.8;
|
||||
if (maos.modo === 'esconder' || maos.modo === 'espiar') {
|
||||
// Cada mão vem do canto de baixo, por fora, num arco, girando até ficar inclinada para dentro.
|
||||
// A da direita chega um pouco depois: duas mãos nunca se mexem igualzinho.
|
||||
const cobrir = (olho, lado, atraso) => {
|
||||
const ida = voltinha((k - atraso) / 0.24), volta = faixaDe(k, 0.8 + atraso * 0.5, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); // 0 = fora da tela, 1 = cobrindo
|
||||
const alvoX = olho.cx - lado * olho.ry * 0.05, alvoY = olho.cy + olho.ry * 0.38;
|
||||
const x = alvoX - lado * olho.ry * 1.6 * (1 - Math.min(1, v));
|
||||
const y = fora + (alvoY - fora) * v;
|
||||
const respira = Math.sin(o.t * 3 + lado) * s * 0.03 * Math.min(1, v);
|
||||
const ang = lado * (0.42 + 0.55 * (1 - Math.min(1, v))); // gira enquanto sobe, termina a ~24°
|
||||
return { x, y: y + respira, ang };
|
||||
};
|
||||
const pe = cobrir(e, 1, 0), pd = cobrir(d, -1, 0.05);
|
||||
if (maos.modo === 'espiar') {
|
||||
// a mão da esquerda gira pelo punho para fora, como uma porta, e o olho aparece
|
||||
const abre = faixaDe(k, 0.3, 0.44) - faixaDe(k, 0.64, 0.76);
|
||||
pe.ang -= abre * 1.05; pe.x -= abre * e.ry * 0.35; pe.y += abre * e.ry * 0.2;
|
||||
}
|
||||
const treme = Math.sin(o.t * 22) * s * 0.012; // tímido: tremidinha
|
||||
// dorso virado para quem olha: polegar para fora, perto da bochecha
|
||||
mao(c, o, pe.x + treme, pe.y, s * 1.1, pe.ang, -1, 0, 0, false, true, true);
|
||||
mao(c, o, pd.x - treme, pd.y, s * 1.1, pd.ang, 1, 0, 0, false, true, true);
|
||||
} else if (maos.modo === 'acenar') {
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
const x = d.cx + d.ry * 2.2 + d.ry * 0.6 * (1 - Math.min(1, v)), y0 = d.cy + d.ry * 1.55;
|
||||
const y = fora + (y0 - fora) * v;
|
||||
const balanco = k > 0.22 && k < 0.82 ? Math.sin((k - 0.22) * Math.PI * 7) * 0.32 : 0;
|
||||
mao(c, o, x, y, s * 0.95, -0.18 + balanco, -1, 0, 1, false, true);
|
||||
} else if (maos.modo === 'pensar') {
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
const x = (e.cx + d.cx) / 2 + e.ry * 1.05, y0 = e.cy + e.ry * 1.4;
|
||||
const y = fora + (y0 - fora) * v + Math.sin(o.t * 9) * s * 0.03;
|
||||
mao(c, o, x, y, s * 0.85, -0.5, -1, 0.95, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- faixa de baixo
|
||||
// A parte de baixo da tela vira palco: legenda da conversa, pensando, equalizador e carga.
|
||||
const faixa = { modo: null, texto: '', t0: null, dur: 0 };
|
||||
|
||||
function definirFaixa(modo, opcoes) {
|
||||
opcoes = opcoes || {};
|
||||
if (!modo) {
|
||||
if (!opcoes.so || faixa.modo === opcoes.so) faixa.modo = null; // so: só apaga esse modo
|
||||
return;
|
||||
}
|
||||
faixa.modo = modo; faixa.t0 = performance.now() / 1000; faixa.texto = String(opcoes.texto || '').replace(/\s+/g, ' ').trim().slice(0, 400);
|
||||
faixa.dur = opcoes.segundos || (modo === 'legenda' ? Math.max(3, Math.min(14, faixa.texto.length / 14 + 2.5)) : modo === 'digitando' ? 20 : 2.6);
|
||||
}
|
||||
|
||||
function desenharFaixa(c, o, W, H) {
|
||||
const y = H * 0.955, fonte = Math.round(H * 0.042);
|
||||
c.save();
|
||||
c.shadowColor = css(o.cor, 0.6); c.shadowBlur = H * 0.02;
|
||||
if (o.carga && o.carga.carregando && typeof o.carga.nivel === 'number') { // carregando: canto direito
|
||||
const n = Math.max(0, Math.min(100, o.carga.nivel)), bw = H * 0.075, bh = H * 0.036, bx = W - H * 0.13, by = y - bh / 2;
|
||||
c.globalAlpha = 0.55 + 0.25 * Math.sin(o.t * 2.5);
|
||||
c.strokeStyle = css(o.cor); c.lineWidth = Math.max(1, H * 0.005);
|
||||
caixa(c, bx, by, bw, bh, bh * 0.25); c.stroke();
|
||||
c.fillStyle = css(o.cor); c.fillRect(bx + bw, y - bh * 0.2, bh * 0.15, bh * 0.4);
|
||||
c.fillRect(bx + bh * 0.12, by + bh * 0.15, (bw - bh * 0.24) * n / 100, bh * 0.7);
|
||||
c.font = '600 ' + Math.round(fonte * 0.8) + 'px system-ui,sans-serif'; c.textAlign = 'right'; c.textBaseline = 'middle';
|
||||
const tx = bx - H * 0.012;
|
||||
c.fillText(Math.round(n) + '%', tx, y);
|
||||
const rx = tx - c.measureText(Math.round(n) + '%').width - H * 0.022, r = H * 0.016; // raio desenhado na cor dos olhos
|
||||
c.beginPath(); c.moveTo(rx + r * 0.3, y - r); c.lineTo(rx - r * 0.5, y + r * 0.15); c.lineTo(rx, y + r * 0.15);
|
||||
c.lineTo(rx - r * 0.3, y + r); c.lineTo(rx + r * 0.5, y - r * 0.15); c.lineTo(rx, y - r * 0.15); c.closePath(); c.fill();
|
||||
c.globalAlpha = 1;
|
||||
}
|
||||
if (faixa.modo) {
|
||||
if (faixa.t0 === null) faixa.t0 = o.t;
|
||||
const k = (o.t - faixa.t0) / faixa.dur;
|
||||
if (k >= 1) faixa.modo = null;
|
||||
else {
|
||||
const some = Math.min(1, (faixa.dur - (o.t - faixa.t0)) / 0.4);
|
||||
c.globalAlpha = Math.max(0, some);
|
||||
c.fillStyle = css(o.cor);
|
||||
if (faixa.modo === 'legenda') {
|
||||
// efeito de digitação; se não couber, mostra o final do que já foi "dito"
|
||||
let txt = faixa.texto.slice(0, Math.floor((o.t - faixa.t0) * 30));
|
||||
c.font = '500 ' + fonte + 'px system-ui,sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle';
|
||||
const max = W * 0.8;
|
||||
while (txt.length > 1 && c.measureText(txt).width > max) txt = txt.slice(1);
|
||||
c.fillText(txt, W / 2, y);
|
||||
} else if (faixa.modo === 'digitando') {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const pulo = Math.max(0, Math.sin(o.t * 7 - i * 0.7)) * H * 0.012;
|
||||
c.beginPath(); c.arc(W / 2 + (i - 1) * H * 0.035, y - pulo, H * 0.011, 0, Math.PI * 2); c.fill();
|
||||
}
|
||||
} else if (faixa.modo === 'equalizador') {
|
||||
const n = 15, larg = H * 0.016, passo = H * 0.026;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = 0.25 + 0.75 * Math.abs(Math.sin(o.t * (5 + i % 4) + i * 1.3)) * Math.sin(Math.PI * (i + 0.5) / n);
|
||||
const h = H * 0.04 * a;
|
||||
caixa(c, W / 2 + (i - (n - 1) / 2) * passo - larg / 2, H * 0.988 - h, larg, h, larg / 2); c.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrato parado num canvas qualquer (miniaturas da biblioteca no painel).
|
||||
@@ -285,5 +469,8 @@
|
||||
|
||||
function esconder() { if (tela) tela.style.display = 'none'; antes = 0; }
|
||||
|
||||
window.MotorRosto = { desenhar, retrato, esconder, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
||||
window.MotorRosto = {
|
||||
desenhar, retrato, esconder,
|
||||
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
|
||||
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS) };
|
||||
})();
|
||||
|
||||
@@ -106,6 +106,10 @@ 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
|
||||
self.assertEqual(self.pedir('POST', '/comando', {'tipo': 'gesto', 'valor': novo})[0], 200)
|
||||
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], [])
|
||||
|
||||
|
||||
@@ -20,17 +20,34 @@ export const GESTOS_INFO: Record<string, { nome: string; emoji: string }> = {
|
||||
dormir: { nome: 'Dormir', emoji: '😴' },
|
||||
acordar: { nome: 'Acordar', emoji: '🌅' },
|
||||
aleatorio: { nome: 'Aleatório', emoji: '🎲' },
|
||||
acenar: { nome: 'Acenar', emoji: '👋' },
|
||||
esconder: { nome: 'Esconder o rosto', emoji: '🙈' },
|
||||
espiar: { nome: 'Espiar', emoji: '🫣' },
|
||||
pensar: { nome: 'Pensar', emoji: '🤔' },
|
||||
musica: { nome: 'Dançar', emoji: '🎵' },
|
||||
};
|
||||
|
||||
export const gestosDisponiveis = () => estado.gestos.filter((g) => g in GESTOS_INFO);
|
||||
|
||||
const CHAVE_FAVORITOS = 'carinha_favoritos';
|
||||
const PADRAO_FAVORITOS = ['feliz', 'coracao', 'piscadinha', 'surpreso', 'triste', 'olhar_esq', 'olhar_dir', 'dormir', 'acordar'];
|
||||
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 GESTOS_MAOS = ['acenar', 'esconder', 'espiar', 'pensar', 'musica'];
|
||||
const PADRAO_FAVORITOS = ['feliz', 'coracao', 'acenar', 'esconder', 'espiar', 'pensar', 'musica', 'piscadinha', 'surpreso', 'triste', 'dormir', 'acordar'];
|
||||
|
||||
function lerFavoritos(): string[] {
|
||||
try {
|
||||
const v = JSON.parse(localStorage.getItem(CHAVE_FAVORITOS) || 'null');
|
||||
if (Array.isArray(v) && v.every((x) => typeof x === 'string')) return v.slice(0, 9);
|
||||
if (Array.isArray(v) && v.every((x) => typeof x === 'string')) {
|
||||
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));
|
||||
const lista = [...v.slice(0, 2), ...novos, ...v.slice(2)].slice(0, MAX_FAVORITOS);
|
||||
localStorage.setItem(CHAVE_FAVORITOS, JSON.stringify(lista));
|
||||
localStorage.setItem(CHAVE_MAOS, '1');
|
||||
return lista;
|
||||
}
|
||||
} catch {
|
||||
/* sem armazenamento */
|
||||
}
|
||||
@@ -40,9 +57,10 @@ function lerFavoritos(): string[] {
|
||||
class Favoritos {
|
||||
lista = $state<string[]>(lerFavoritos());
|
||||
alternar(g: string) {
|
||||
this.lista = this.lista.includes(g) ? this.lista.filter((x) => x !== g) : [...this.lista, g].slice(0, 9);
|
||||
this.lista = this.lista.includes(g) ? this.lista.filter((x) => x !== g) : [...this.lista, g].slice(0, MAX_FAVORITOS);
|
||||
try {
|
||||
localStorage.setItem(CHAVE_FAVORITOS, JSON.stringify(this.lista));
|
||||
localStorage.setItem(CHAVE_MAOS, '1');
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
{#each lista as g, i (g)}
|
||||
{@const info = GESTOS_INFO[g]}
|
||||
<div class="item">
|
||||
<button class="gesto" onclick={() => gesto(g)} title={completo ? info.nome : `${info.nome} (tecla ${i + 1})`}>
|
||||
<button class="gesto" onclick={() => gesto(g)} title={completo || i > 8 ? info.nome : `${info.nome} (tecla ${i + 1})`}>
|
||||
<span class="emoji" aria-hidden="true">{info.emoji}</span>
|
||||
<span class="nome">{info.nome}</span>
|
||||
{#if !completo}<kbd>{i + 1}</kbd>{/if}
|
||||
{#if !completo && i < 9}<kbd>{i + 1}</kbd>{/if}
|
||||
</button>
|
||||
{#if completo}
|
||||
<button
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
ultimaConversa = c.ultima.id;
|
||||
enviar({ tipo: 'ordem', ordem: 'acordar' });
|
||||
enviar({ tipo: 'ordem', ordem: c.ultima.gesto });
|
||||
enviar({ tipo: 'legenda', texto: c.ultima.texto });
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
<Linha icone={Settings2} cor="#8e8e93" rotulo="Qualidade, câmera frontal/traseira e sensores" href="#/ajustes" />
|
||||
</Grupo>
|
||||
|
||||
<Grupo titulo="Gestos" rodape="Teclas 1 a 9 no teclado. Ctrl+K busca qualquer gesto.">
|
||||
<Grupo titulo="Gestos" rodape="Teclas 1 a 9 disparam os nove primeiros. Ctrl+K busca qualquer gesto.">
|
||||
<Linha icone={Smile} cor="#ffcc00" rotulo="Favoritos">
|
||||
<Gestos />
|
||||
</Linha>
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</Card>
|
||||
</div>
|
||||
{:else if aba === 'gestos'}
|
||||
<Card titulo="Gestos do rosto" descricao="Toque para mandar ao celular. A estrela fixa o gesto nos favoritos (atalhos 1 a 9).">
|
||||
<Card titulo="Gestos do rosto" descricao="Toque para mandar ao celular. A estrela fixa o gesto nos favoritos (até 12; as teclas 1 a 9 valem para os nove primeiros).">
|
||||
<Gestos completo />
|
||||
</Card>
|
||||
{:else}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
const classicos = $derived(estado.rostos.filter((r) => !ROSTOS_INFO[r]?.novo));
|
||||
const formatos = $derived(estado.formatos.length ? estado.formatos : Object.keys(FORMATOS_INFO));
|
||||
|
||||
const EMOCOES = ['feliz', 'coracao', 'surpreso', 'triste', 'bravo', 'piscadinha', 'olhar_esq', 'olhar_dir', 'dormir', 'acordar'];
|
||||
const EMOCOES = ['feliz', 'coracao', 'surpreso', 'triste', 'bravo', 'piscadinha', 'acenar', 'esconder', 'espiar', 'pensar', 'musica', 'dormir', 'acordar'];
|
||||
function emocao(g: string) {
|
||||
if (noCelular) gesto(g);
|
||||
else previa?.ordem(g);
|
||||
@@ -28,6 +28,8 @@
|
||||
{ titulo: 'Pálpebras no lugar de formas prontas', texto: 'Cada emoção é posição de pálpebra; as trocas ficam suaves em vez de saltar.', feito: true, onde: 'rosto' },
|
||||
{ titulo: 'Nunca parado', texto: 'Respiração, sacadas e piscada que achata o olho. Também protege a tela AMOLED.', feito: true, onde: 'rosto' },
|
||||
{ titulo: 'Toque na tela', texto: 'Cutucar dá susto e risada; deslizar o dedo é carinho; tocar acorda.', feito: true, onde: 'rosto' },
|
||||
{ titulo: 'Mãos', texto: 'Mãozinhas na cor dos olhos: esconde o rosto, espia, acena e pensa com a mão no queixo.', feito: true, onde: 'rosto' },
|
||||
{ titulo: 'Faixa de baixo', texto: 'Legenda da conversa sendo digitada, pontinhos enquanto pensa, equalizador ao dançar e carga quando está no carregador.', feito: true, onde: 'rosto' },
|
||||
{ titulo: 'Fala pelo próprio celular', texto: 'A resposta da conversa sai no alto-falante do S10, com os olhos pulsando na voz.', feito: false, onde: 'app' },
|
||||
{ titulo: 'Ritmo do dia', texto: 'Boceja à noite, fica sonolento com bateria baixa, mostra um raio quando carrega.', feito: false, onde: 'rosto e servidor' },
|
||||
{ titulo: 'Contato visual', texto: 'Sustenta o olhar de quem a câmera vê e desvia quando fica tímido.', feito: false, onde: 'rosto' },
|
||||
|
||||
Reference in New Issue
Block a user