Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dab45604c | ||
|
|
982bf56cd0 | ||
|
|
1029b5fa99 |
@@ -92,7 +92,7 @@ addEventListener("resize", ajustar);
|
||||
|
||||
const PRETO = [6, 8, 14];
|
||||
const CIANO = [92, 214, 255];
|
||||
const EXTRAS = { boca: true, sobrancelhas: false };
|
||||
const EXTRAS = { boca: true, sobrancelhas: false, iris: false, subirBoca: 0, distSobrancelha: 16 };
|
||||
const CIANO_E = [150, 232, 255];
|
||||
const VERM = [255, 78, 78];
|
||||
const ROSA = [255, 112, 176];
|
||||
@@ -150,8 +150,18 @@ function caminhoLinha(x0, y0, x1, y1) {
|
||||
return "M " + x0 + "," + y0 + " L " + x1 + "," + y1;
|
||||
}
|
||||
/* formas da boca (o humor escolhe qual) */
|
||||
function deslocamentoBoca() {
|
||||
const auto = ROSTO && ROSTO.motor ? 22 : 0; // rostos novos têm olhos mais baixos
|
||||
return -(auto + (EXTRAS.subirBoca || 0)) * ESC;
|
||||
}
|
||||
/* Topo do olho atual (para a sobrancelha ficar perto dele, em qualquer estilo/formato). */
|
||||
function metadeOlho(esc) {
|
||||
const m = ROSTO && ROSTO.motor && window.MotorRosto && window.MotorRosto.metadeAltura
|
||||
? window.MotorRosto.metadeAltura(ROSTO.motor, FORMATO) : 1;
|
||||
return m * RY * esc;
|
||||
}
|
||||
function caminhoBoca(tipo) {
|
||||
const cx = BOCA[0], cy = BOCA[1], l = BOCA_L, meia = l / 2;
|
||||
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca(), l = BOCA_L, meia = l / 2;
|
||||
if (tipo === "aberta") { // surpresa: um "O"
|
||||
const rx = l * 0.17, ry = l * 0.22;
|
||||
return caminhoElipse(cx, cy, rx, ry);
|
||||
@@ -404,6 +414,8 @@ function 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,
|
||||
iris: EXTRAS.iris,
|
||||
tracos: ROSTO.motor === "matriz" ? tracosDoRosto(esc) : null,
|
||||
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
|
||||
parado: !r.kf && !seguindo && !r.dormindo,
|
||||
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
||||
@@ -417,16 +429,17 @@ function desenhar() {
|
||||
olho(i, c[0] + dx * mover * ESC, c[1] + dy * mover * ESC, RX * esc, RY * esc,
|
||||
abx, fx, r.cor, lado, dx, dy * 0.6);
|
||||
}
|
||||
if (ROSTO.motor && !ROSTO.boca) elBoca.style.display = "none";
|
||||
const matriz = comMotor && ROSTO.motor === "matriz";
|
||||
if (matriz) elBoca.style.display = "none"; // a Matriz desenha a boca em LEDs
|
||||
else desenharBoca();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const brow = document.getElementById('sobrancelha' + (i + 1));
|
||||
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo ? '' : 'none';
|
||||
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo && !matriz ? '' : 'none';
|
||||
if (!EXTRAS.sobrancelhas || r.dormindo) continue;
|
||||
const c = i === 0 ? E1 : E2;
|
||||
const lado = i === 0 ? 1 : -1;
|
||||
const largura = RX * esc * 0.85;
|
||||
const y = c[1] - RY * esc - 20 * ESC;
|
||||
const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
|
||||
const inclinacao = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
|
||||
const elevacao = r.esc > 1.1 ? -15 * ESC : 0;
|
||||
brow.setAttribute('d', 'M ' + (c[0]-largura) + ',' + (y-inclinacao+elevacao) + ' Q ' + c[0] + ',' + (y-10*ESC+elevacao) + ' ' + (c[0]+largura) + ',' + (y+inclinacao+elevacao));
|
||||
@@ -435,6 +448,27 @@ function desenhar() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Boca e sobrancelhas como caminhos SVG, para a Matriz acender em LEDs (mesmas formas do SVG). */
|
||||
function tracosDoRosto(esc) {
|
||||
const tracos = [];
|
||||
if (EXTRAS.boca) {
|
||||
const forma = r.boca;
|
||||
if (r.dormindo) { // dormindo na Matriz: "– _ –"
|
||||
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca() + BOCA_L * 0.08, l = BOCA_L * 0.32;
|
||||
tracos.push({ d: 'M ' + (cx - l) + ',' + cy + ' L ' + (cx + l) + ',' + cy, largura: BOCA_L * 0.16 });
|
||||
} else tracos.push({ d: caminhoBoca(forma), largura: BOCA_L * 0.18, cheio: forma === "aberta" });
|
||||
}
|
||||
if (EXTRAS.sobrancelhas && !r.dormindo) {
|
||||
[E1, E2].forEach((c, i) => {
|
||||
const lado = i === 0 ? 1 : -1, largura = RX * esc * 0.85, y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
|
||||
const inc = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
|
||||
const ele = r.esc > 1.1 ? -15 * ESC : 0;
|
||||
tracos.push({ d: 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele), largura: 9 * ESC });
|
||||
});
|
||||
}
|
||||
return tracos;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ controle
|
||||
/* Troca de rosto (biblioteca acima). Vale na hora, sem recarregar a pagina. */
|
||||
function trocarRosto(nome) {
|
||||
@@ -894,7 +928,9 @@ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
|
||||
|
||||
let configRecebida = null;
|
||||
function configurarApp(c) {
|
||||
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true;
|
||||
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true;
|
||||
if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca;
|
||||
if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha;
|
||||
AUTO = c.auto;
|
||||
AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume;
|
||||
COMPORTAMENTO.ouvir = c.ouvir; COMPORTAMENTO.reagir = c.reagir;
|
||||
|
||||
@@ -118,6 +118,11 @@
|
||||
const h = est.h * p.ry * respira * abre * (1 - amassa * 0.6);
|
||||
const x = p.cx - w / 2, y = p.cy - h / 2;
|
||||
c.fillStyle = css(o.cor);
|
||||
if (o.dormindo && !temConteudo) { // dormindo: "–" (olho fechado)
|
||||
const esp = Math.max(2, p.ry * 0.2);
|
||||
caixa(c, p.cx - w * 0.5, p.cy + h * 0.08 - esp / 2, w, esp, esp / 2); c.fill();
|
||||
return;
|
||||
}
|
||||
if (e.coracao > 0.5) { coracao(c, p.cx, p.cy, Math.min(w, h) * 0.55 * respira); return; }
|
||||
caixa(c, x, y, w, h, est.r * p.ry * respira);
|
||||
c.fill();
|
||||
@@ -132,7 +137,21 @@
|
||||
c.beginPath(); c.ellipse(p.cx, y + h * (1.62 - e.bot), w * 0.75, h * 0.62, 0, 0, Math.PI * 2); c.fill();
|
||||
c.globalCompositeOperation = 'source-over';
|
||||
if (temConteudo && fecha < 0.6) conteudo(c, o, p.cx, p.cy, Math.min(w, h) * 0.74);
|
||||
else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
|
||||
else if (o.iris && fecha < 0.6 && e.bot < 0.35) {
|
||||
const R = Math.min(w, h) * 0.3;
|
||||
const ix = Math.max(x + R * 1.1, Math.min(x + w - R * 1.1, p.cx + (p.lx + sacada.x) * w * 0.016));
|
||||
const iy = Math.max(y + R * 1.1, Math.min(y + h - R * 1.1, p.cy + h * 0.04 + (p.ly + sacada.y) * h * 0.016));
|
||||
c.fillStyle = css(o.cor.map((v) => v * 0.42));
|
||||
c.beginPath(); c.arc(ix, iy, R, 0, Math.PI * 2); c.fill();
|
||||
c.globalCompositeOperation = 'destination-out';
|
||||
const pupila = p.ab > 1.05 ? 0.38 : e.bot > 0.2 ? 0.62 : 0.5; // susto contrai, alegria dilata
|
||||
c.beginPath(); c.arc(ix, iy, R * pupila, 0, Math.PI * 2); c.fill();
|
||||
c.globalCompositeOperation = 'source-over';
|
||||
if (est.brilho) {
|
||||
c.fillStyle = 'rgba(245,250,255,0.9)';
|
||||
c.beginPath(); c.arc(ix - R * 0.42, iy - R * 0.42, R * 0.2, 0, Math.PI * 2); c.fill();
|
||||
}
|
||||
} else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
|
||||
const hx = Math.max(x + w * 0.18, Math.min(x + w * 0.62, x + w * 0.3 + (p.lx + sacada.x) * w * 0.012));
|
||||
const hy = Math.max(y + h * 0.18, Math.min(y + h * 0.5, y + h * 0.26 + (p.ly + sacada.y) * h * 0.012));
|
||||
c.fillStyle = 'rgba(245,250,255,0.9)';
|
||||
@@ -205,7 +224,15 @@
|
||||
bctx.clearRect(0, 0, colunas, linhas);
|
||||
bctx.save(); bctx.scale(f, f);
|
||||
olhos.forEach((p, i) => olhoPlano(bctx, o, p, estilo, estado[i]));
|
||||
for (const traco of o.tracos || []) {
|
||||
if (typeof traco.d !== 'string') continue;
|
||||
const caminho = new Path2D(traco.d);
|
||||
bctx.strokeStyle = bctx.fillStyle = css(o.cor);
|
||||
bctx.lineWidth = traco.largura; bctx.lineCap = 'round'; bctx.lineJoin = 'round';
|
||||
if (traco.cheio) bctx.fill(caminho); else bctx.stroke(caminho);
|
||||
}
|
||||
desenharMaos(bctx, o, olhos, tela.height / d);
|
||||
desenharZzz(bctx, o, olhos, 1.6); // em LEDs o Z precisa ser maior para ler
|
||||
bctx.restore();
|
||||
const px = bctx.getImageData(0, 0, colunas, linhas).data;
|
||||
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, tela.width, tela.height);
|
||||
@@ -245,10 +272,12 @@
|
||||
} else if (estilo.lente) {
|
||||
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(ctx, o, olhos, tela.height);
|
||||
desenharZzz(ctx, o, olhos);
|
||||
} 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);
|
||||
desenharZzz(cctx, o, olhos);
|
||||
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);
|
||||
@@ -367,6 +396,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Zzz
|
||||
function desenharZzz(c, o, olhos, fator) {
|
||||
fator = fator || 1;
|
||||
if (!o.dormindo) return;
|
||||
const d = olhos.slice().sort((a, b) => a.cx - b.cx)[1]; // olho da direita na tela
|
||||
const x0 = d.cx + d.ry * 0.7, y0 = d.cy - d.ry * 0.55;
|
||||
c.save();
|
||||
c.strokeStyle = css(o.cor); c.lineCap = 'round'; c.lineJoin = 'round';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const k = ((o.t || 0) * 0.32 + i / 3) % 1; // cada Z num ponto do ciclo
|
||||
const tam = d.ry * (0.28 + 0.3 * k) * fator;
|
||||
const x = x0 + d.ry * 0.7 * k + Math.sin(k * Math.PI * 2) * d.ry * 0.08, y = y0 - d.ry * 1.7 * k;
|
||||
c.globalAlpha = Math.min(1, k / 0.15) * (1 - k);
|
||||
c.lineWidth = Math.max(1.5, tam * 0.2);
|
||||
c.beginPath();
|
||||
c.moveTo(x - tam / 2, y - tam / 2); c.lineTo(x + tam / 2, y - tam / 2);
|
||||
c.lineTo(x - tam / 2, y + tam / 2); c.lineTo(x + tam / 2, y + tam / 2);
|
||||
c.stroke();
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- 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 };
|
||||
@@ -471,6 +522,12 @@
|
||||
|
||||
window.MotorRosto = {
|
||||
desenhar, retrato, esconder,
|
||||
/** Metade da altura do olho (em frações de RY) para o estilo/formato: posiciona as sobrancelhas. */
|
||||
metadeAltura: (estilo, formato) => {
|
||||
const base = ESTILOS[estilo]; if (!base) return 1;
|
||||
if (base.lente) return base.w * 0.5 * 1.12;
|
||||
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) };
|
||||
})();
|
||||
|
||||
+42
-6
@@ -92,7 +92,7 @@ addEventListener("resize", ajustar);
|
||||
|
||||
const PRETO = [6, 8, 14];
|
||||
const CIANO = [92, 214, 255];
|
||||
const EXTRAS = { boca: true, sobrancelhas: false };
|
||||
const EXTRAS = { boca: true, sobrancelhas: false, iris: false, subirBoca: 0, distSobrancelha: 16 };
|
||||
const CIANO_E = [150, 232, 255];
|
||||
const VERM = [255, 78, 78];
|
||||
const ROSA = [255, 112, 176];
|
||||
@@ -150,8 +150,18 @@ function caminhoLinha(x0, y0, x1, y1) {
|
||||
return "M " + x0 + "," + y0 + " L " + x1 + "," + y1;
|
||||
}
|
||||
/* formas da boca (o humor escolhe qual) */
|
||||
function deslocamentoBoca() {
|
||||
const auto = ROSTO && ROSTO.motor ? 22 : 0; // rostos novos têm olhos mais baixos
|
||||
return -(auto + (EXTRAS.subirBoca || 0)) * ESC;
|
||||
}
|
||||
/* Topo do olho atual (para a sobrancelha ficar perto dele, em qualquer estilo/formato). */
|
||||
function metadeOlho(esc) {
|
||||
const m = ROSTO && ROSTO.motor && window.MotorRosto && window.MotorRosto.metadeAltura
|
||||
? window.MotorRosto.metadeAltura(ROSTO.motor, FORMATO) : 1;
|
||||
return m * RY * esc;
|
||||
}
|
||||
function caminhoBoca(tipo) {
|
||||
const cx = BOCA[0], cy = BOCA[1], l = BOCA_L, meia = l / 2;
|
||||
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca(), l = BOCA_L, meia = l / 2;
|
||||
if (tipo === "aberta") { // surpresa: um "O"
|
||||
const rx = l * 0.17, ry = l * 0.22;
|
||||
return caminhoElipse(cx, cy, rx, ry);
|
||||
@@ -404,6 +414,8 @@ function 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,
|
||||
iris: EXTRAS.iris,
|
||||
tracos: ROSTO.motor === "matriz" ? tracosDoRosto(esc) : null,
|
||||
desenho: !r.dormindo ? window.carinhaOlhosDesenho || null : null,
|
||||
parado: !r.kf && !seguindo && !r.dormindo,
|
||||
olhos: olhos.map(([c, abx, fx, lado]) => ({
|
||||
@@ -417,16 +429,17 @@ function desenhar() {
|
||||
olho(i, c[0] + dx * mover * ESC, c[1] + dy * mover * ESC, RX * esc, RY * esc,
|
||||
abx, fx, r.cor, lado, dx, dy * 0.6);
|
||||
}
|
||||
if (ROSTO.motor && !ROSTO.boca) elBoca.style.display = "none";
|
||||
const matriz = comMotor && ROSTO.motor === "matriz";
|
||||
if (matriz) elBoca.style.display = "none"; // a Matriz desenha a boca em LEDs
|
||||
else desenharBoca();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const brow = document.getElementById('sobrancelha' + (i + 1));
|
||||
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo ? '' : 'none';
|
||||
brow.style.display = EXTRAS.sobrancelhas && !r.dormindo && !matriz ? '' : 'none';
|
||||
if (!EXTRAS.sobrancelhas || r.dormindo) continue;
|
||||
const c = i === 0 ? E1 : E2;
|
||||
const lado = i === 0 ? 1 : -1;
|
||||
const largura = RX * esc * 0.85;
|
||||
const y = c[1] - RY * esc - 20 * ESC;
|
||||
const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
|
||||
const inclinacao = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
|
||||
const elevacao = r.esc > 1.1 ? -15 * ESC : 0;
|
||||
brow.setAttribute('d', 'M ' + (c[0]-largura) + ',' + (y-inclinacao+elevacao) + ' Q ' + c[0] + ',' + (y-10*ESC+elevacao) + ' ' + (c[0]+largura) + ',' + (y+inclinacao+elevacao));
|
||||
@@ -435,6 +448,27 @@ function desenhar() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Boca e sobrancelhas como caminhos SVG, para a Matriz acender em LEDs (mesmas formas do SVG). */
|
||||
function tracosDoRosto(esc) {
|
||||
const tracos = [];
|
||||
if (EXTRAS.boca) {
|
||||
const forma = r.boca;
|
||||
if (r.dormindo) { // dormindo na Matriz: "– _ –"
|
||||
const cx = BOCA[0], cy = BOCA[1] + deslocamentoBoca() + BOCA_L * 0.08, l = BOCA_L * 0.32;
|
||||
tracos.push({ d: 'M ' + (cx - l) + ',' + cy + ' L ' + (cx + l) + ',' + cy, largura: BOCA_L * 0.16 });
|
||||
} else tracos.push({ d: caminhoBoca(forma), largura: BOCA_L * 0.18, cheio: forma === "aberta" });
|
||||
}
|
||||
if (EXTRAS.sobrancelhas && !r.dormindo) {
|
||||
[E1, E2].forEach((c, i) => {
|
||||
const lado = i === 0 ? 1 : -1, largura = RX * esc * 0.85, y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC;
|
||||
const inc = r.forma === 'bravo' ? 18 * lado * ESC : r.forma === 'triste' ? -14 * lado * ESC : 0;
|
||||
const ele = r.esc > 1.1 ? -15 * ESC : 0;
|
||||
tracos.push({ d: 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele), largura: 9 * ESC });
|
||||
});
|
||||
}
|
||||
return tracos;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ controle
|
||||
/* Troca de rosto (biblioteca acima). Vale na hora, sem recarregar a pagina. */
|
||||
function trocarRosto(nome) {
|
||||
@@ -894,7 +928,9 @@ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
|
||||
|
||||
let configRecebida = null;
|
||||
function configurarApp(c) {
|
||||
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true;
|
||||
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true;
|
||||
if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca;
|
||||
if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha;
|
||||
AUTO = c.auto;
|
||||
AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume;
|
||||
COMPORTAMENTO.ouvir = c.ouvir; COMPORTAMENTO.reagir = c.reagir;
|
||||
|
||||
+3
-2
@@ -20,7 +20,7 @@ PADRAO = dict(preview=False, auto=True, audio=True, volume=0.35,
|
||||
somaltolim=0.55, sompausa=20, somcooldown=5,
|
||||
movlim=0.05, movforte=0.12, movcooldown=6, movolhar=40,
|
||||
economia=True, brilho=0.7, brilhoRepouso=0.08, repousoTela=30,
|
||||
boca=True, sobrancelhas=False, videoRTC=True,
|
||||
boca=True, sobrancelhas=False, iris=False, subirBoca=0, distSobrancelha=16, videoRTC=True,
|
||||
sensorMic=True, sensorCamera=True, micSono=True, cameraSono=False,
|
||||
cameraLado='user', videoResolucao='auto', videoFPS=0,
|
||||
rastrearRosto=True, rastrearMaos=True, seguirRastreamento=True, mostrarRastreamento=True)
|
||||
@@ -30,7 +30,8 @@ ESCOLHAS = dict(cameraLado=('user', 'environment'),
|
||||
LIMITES = dict(volume=(0, 1), somlim=(0, 1), somganho=(0, 1),
|
||||
somaltolim=(0, 1), sompausa=(1, 3600), somcooldown=(0, 120),
|
||||
movlim=(0, 1), movforte=(0, 1), movcooldown=(0, 120), movolhar=(0, 100),
|
||||
brilho=(0.01, 1), brilhoRepouso=(0.01, 1), repousoTela=(5, 3600))
|
||||
brilho=(0.01, 1), brilhoRepouso=(0.01, 1), repousoTela=(5, 3600),
|
||||
subirBoca=(-20, 60), distSobrancelha=(0, 60))
|
||||
|
||||
|
||||
def validar(dados):
|
||||
|
||||
+58
-1
@@ -118,6 +118,11 @@
|
||||
const h = est.h * p.ry * respira * abre * (1 - amassa * 0.6);
|
||||
const x = p.cx - w / 2, y = p.cy - h / 2;
|
||||
c.fillStyle = css(o.cor);
|
||||
if (o.dormindo && !temConteudo) { // dormindo: "–" (olho fechado)
|
||||
const esp = Math.max(2, p.ry * 0.2);
|
||||
caixa(c, p.cx - w * 0.5, p.cy + h * 0.08 - esp / 2, w, esp, esp / 2); c.fill();
|
||||
return;
|
||||
}
|
||||
if (e.coracao > 0.5) { coracao(c, p.cx, p.cy, Math.min(w, h) * 0.55 * respira); return; }
|
||||
caixa(c, x, y, w, h, est.r * p.ry * respira);
|
||||
c.fill();
|
||||
@@ -132,7 +137,21 @@
|
||||
c.beginPath(); c.ellipse(p.cx, y + h * (1.62 - e.bot), w * 0.75, h * 0.62, 0, 0, Math.PI * 2); c.fill();
|
||||
c.globalCompositeOperation = 'source-over';
|
||||
if (temConteudo && fecha < 0.6) conteudo(c, o, p.cx, p.cy, Math.min(w, h) * 0.74);
|
||||
else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
|
||||
else if (o.iris && fecha < 0.6 && e.bot < 0.35) {
|
||||
const R = Math.min(w, h) * 0.3;
|
||||
const ix = Math.max(x + R * 1.1, Math.min(x + w - R * 1.1, p.cx + (p.lx + sacada.x) * w * 0.016));
|
||||
const iy = Math.max(y + R * 1.1, Math.min(y + h - R * 1.1, p.cy + h * 0.04 + (p.ly + sacada.y) * h * 0.016));
|
||||
c.fillStyle = css(o.cor.map((v) => v * 0.42));
|
||||
c.beginPath(); c.arc(ix, iy, R, 0, Math.PI * 2); c.fill();
|
||||
c.globalCompositeOperation = 'destination-out';
|
||||
const pupila = p.ab > 1.05 ? 0.38 : e.bot > 0.2 ? 0.62 : 0.5; // susto contrai, alegria dilata
|
||||
c.beginPath(); c.arc(ix, iy, R * pupila, 0, Math.PI * 2); c.fill();
|
||||
c.globalCompositeOperation = 'source-over';
|
||||
if (est.brilho) {
|
||||
c.fillStyle = 'rgba(245,250,255,0.9)';
|
||||
c.beginPath(); c.arc(ix - R * 0.42, iy - R * 0.42, R * 0.2, 0, Math.PI * 2); c.fill();
|
||||
}
|
||||
} else if (est.brilho && fecha < 0.55 && e.bot < 0.3) { // reflexo que acompanha o olhar
|
||||
const hx = Math.max(x + w * 0.18, Math.min(x + w * 0.62, x + w * 0.3 + (p.lx + sacada.x) * w * 0.012));
|
||||
const hy = Math.max(y + h * 0.18, Math.min(y + h * 0.5, y + h * 0.26 + (p.ly + sacada.y) * h * 0.012));
|
||||
c.fillStyle = 'rgba(245,250,255,0.9)';
|
||||
@@ -205,7 +224,15 @@
|
||||
bctx.clearRect(0, 0, colunas, linhas);
|
||||
bctx.save(); bctx.scale(f, f);
|
||||
olhos.forEach((p, i) => olhoPlano(bctx, o, p, estilo, estado[i]));
|
||||
for (const traco of o.tracos || []) {
|
||||
if (typeof traco.d !== 'string') continue;
|
||||
const caminho = new Path2D(traco.d);
|
||||
bctx.strokeStyle = bctx.fillStyle = css(o.cor);
|
||||
bctx.lineWidth = traco.largura; bctx.lineCap = 'round'; bctx.lineJoin = 'round';
|
||||
if (traco.cheio) bctx.fill(caminho); else bctx.stroke(caminho);
|
||||
}
|
||||
desenharMaos(bctx, o, olhos, tela.height / d);
|
||||
desenharZzz(bctx, o, olhos, 1.6); // em LEDs o Z precisa ser maior para ler
|
||||
bctx.restore();
|
||||
const px = bctx.getImageData(0, 0, colunas, linhas).data;
|
||||
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, tela.width, tela.height);
|
||||
@@ -245,10 +272,12 @@
|
||||
} else if (estilo.lente) {
|
||||
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
|
||||
desenharMaos(ctx, o, olhos, tela.height);
|
||||
desenharZzz(ctx, o, olhos);
|
||||
} 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);
|
||||
desenharZzz(cctx, o, olhos);
|
||||
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);
|
||||
@@ -367,6 +396,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Zzz
|
||||
function desenharZzz(c, o, olhos, fator) {
|
||||
fator = fator || 1;
|
||||
if (!o.dormindo) return;
|
||||
const d = olhos.slice().sort((a, b) => a.cx - b.cx)[1]; // olho da direita na tela
|
||||
const x0 = d.cx + d.ry * 0.7, y0 = d.cy - d.ry * 0.55;
|
||||
c.save();
|
||||
c.strokeStyle = css(o.cor); c.lineCap = 'round'; c.lineJoin = 'round';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const k = ((o.t || 0) * 0.32 + i / 3) % 1; // cada Z num ponto do ciclo
|
||||
const tam = d.ry * (0.28 + 0.3 * k) * fator;
|
||||
const x = x0 + d.ry * 0.7 * k + Math.sin(k * Math.PI * 2) * d.ry * 0.08, y = y0 - d.ry * 1.7 * k;
|
||||
c.globalAlpha = Math.min(1, k / 0.15) * (1 - k);
|
||||
c.lineWidth = Math.max(1.5, tam * 0.2);
|
||||
c.beginPath();
|
||||
c.moveTo(x - tam / 2, y - tam / 2); c.lineTo(x + tam / 2, y - tam / 2);
|
||||
c.lineTo(x - tam / 2, y + tam / 2); c.lineTo(x + tam / 2, y + tam / 2);
|
||||
c.stroke();
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- 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 };
|
||||
@@ -471,6 +522,12 @@
|
||||
|
||||
window.MotorRosto = {
|
||||
desenhar, retrato, esconder,
|
||||
/** Metade da altura do olho (em frações de RY) para o estilo/formato: posiciona as sobrancelhas. */
|
||||
metadeAltura: (estilo, formato) => {
|
||||
const base = ESTILOS[estilo]; if (!base) return 1;
|
||||
if (base.lente) return base.w * 0.5 * 1.12;
|
||||
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) };
|
||||
})();
|
||||
|
||||
@@ -46,6 +46,9 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
|
||||
{ chave: 'auto', rotulo: 'Gestos espontâneos', ajuda: 'Pisca e olha em volta sem comando', tipo: 'toggle' },
|
||||
{ chave: 'boca', rotulo: 'Mostrar boca', tipo: 'toggle' },
|
||||
{ chave: 'sobrancelhas', rotulo: 'Mostrar sobrancelhas', tipo: 'toggle' },
|
||||
{ chave: 'iris', rotulo: 'Mostrar íris', ajuda: 'Pupila que acompanha o olhar (Viva, Bloco e Matriz)', tipo: 'toggle' },
|
||||
{ chave: 'subirBoca', rotulo: 'Subir a boca', tipo: 'slider', min: -20, max: 60, passo: 2 },
|
||||
{ chave: 'distSobrancelha', rotulo: 'Distância das sobrancelhas', tipo: 'slider', min: 0, max: 60, passo: 2 },
|
||||
{ chave: 'audio', rotulo: 'Sons no celular', ajuda: 'Bipes do próprio robô', tipo: 'toggle' },
|
||||
{ chave: 'volume', rotulo: 'Volume dos sons', tipo: 'slider', min: 0, max: 1, passo: 0.05 },
|
||||
{ chave: 'brilho', rotulo: 'Brilho normal', tipo: 'slider', min: 0.01, max: 1, passo: 0.01 },
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
cor, CORES, FORMATOS_INFO, formato, gesto, GESTOS_INFO, hex, nomeRosto, rgb, rosto, ROSTOS_INFO,
|
||||
} from '../lib/comandos.svelte';
|
||||
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
||||
import Slider from '../lib/componentes/Slider.svelte';
|
||||
import Toggle from '../lib/componentes/Toggle.svelte';
|
||||
import { salvarApp } from '../lib/ajustes.svelte';
|
||||
import { estado } from '../lib/estado.svelte';
|
||||
|
||||
let previa = $state<{ ordem: (t: string) => void; legenda: (t: string) => void }>();
|
||||
@@ -24,8 +26,8 @@
|
||||
const c = voz();
|
||||
if (c) (window as any).VozRobo.balbuciar(c, FRASE, 0.5);
|
||||
}
|
||||
// Por padrão as emoções só aparecem aqui na prévia; ligado, vão também para o celular.
|
||||
let noCelular = $state(false);
|
||||
// As emoções vão para o celular (e a prévia mostra junto); "só na prévia" serve para testar quieto.
|
||||
let soPrevia = $state(false);
|
||||
|
||||
const atual = $derived(estado.aparencia.rosto);
|
||||
const formatoAtual = $derived(estado.aparencia.formato ?? 'padrao');
|
||||
@@ -36,8 +38,8 @@
|
||||
|
||||
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);
|
||||
if (soPrevia) previa?.ordem(g);
|
||||
else gesto(g);
|
||||
const c = voz();
|
||||
if (c) (window as any).VozRobo.tocar(c, g, 0.5);
|
||||
}
|
||||
@@ -88,10 +90,10 @@
|
||||
onchange={(v) => (ouvirAqui = v)}
|
||||
/>
|
||||
<Toggle
|
||||
rotulo="Mandar as emoções também para o celular"
|
||||
ajuda="Desligado, elas aparecem só na prévia"
|
||||
checked={noCelular}
|
||||
onchange={(v) => (noCelular = v)}
|
||||
rotulo="Testar só na prévia, sem mandar ao celular"
|
||||
ajuda="Desligado, cada emoção aparece no celular na hora"
|
||||
checked={soPrevia}
|
||||
onchange={(v) => (soPrevia = v)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -130,6 +132,39 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="bloco-escolha" aria-labelledby="t-detalhes">
|
||||
<h2 id="t-detalhes">Detalhes do rosto</h2>
|
||||
<div class="detalhes">
|
||||
<Toggle rotulo="Boca" checked={estado.appConfig?.boca !== false} disabled={!estado.appConfig} onchange={(v) => salvarApp({ boca: v })} />
|
||||
<Toggle rotulo="Sobrancelhas" checked={!!estado.appConfig?.sobrancelhas} disabled={!estado.appConfig} onchange={(v) => salvarApp({ sobrancelhas: v })} />
|
||||
<Toggle
|
||||
rotulo="Íris"
|
||||
ajuda={aceitaFormato ? 'Pupila que acompanha o olhar' : 'Vale para Viva, Bloco e Matriz'}
|
||||
checked={!!estado.appConfig?.iris}
|
||||
disabled={!estado.appConfig}
|
||||
onchange={(v) => salvarApp({ iris: v })}
|
||||
/>
|
||||
</div>
|
||||
<div class="detalhes">
|
||||
<Slider
|
||||
rotulo="Subir a boca"
|
||||
valor={Number(estado.appConfig?.subirBoca ?? 0)}
|
||||
min={-20}
|
||||
max={60}
|
||||
passo={2}
|
||||
onchange={(v) => salvarApp({ subirBoca: v })}
|
||||
/>
|
||||
<Slider
|
||||
rotulo="Distância das sobrancelhas"
|
||||
valor={Number(estado.appConfig?.distSobrancelha ?? 16)}
|
||||
min={0}
|
||||
max={60}
|
||||
passo={2}
|
||||
onchange={(v) => salvarApp({ distSobrancelha: v })}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bloco-escolha" aria-labelledby="t-cor">
|
||||
<h2 id="t-cor">Cor dos olhos</h2>
|
||||
<div class="cores">
|
||||
@@ -356,6 +391,12 @@
|
||||
height: 18px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.detalhes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
|
||||
gap: var(--e-2) var(--e-6);
|
||||
max-width: 780px;
|
||||
}
|
||||
.cores {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user