diff --git a/android/app/assets/carinha.html b/android/app/assets/carinha.html
index 3b2acaa..a94269b 100644
--- a/android/app/assets/carinha.html
+++ b/android/app/assets/carinha.html
@@ -203,8 +203,24 @@ function caminhoBoca(tipo) {
return "M " + (cx - meia) + "," + (cy - l * 0.10) +
" Q " + cx + "," + (cy + l * 0.42) + " " + (cx + meia) + "," + (cy - l * 0.10);
}
- return "M " + (cx - l * 0.30) + "," + cy + // repouso: leve
- " q " + (l * 0.30) + "," + (l * 0.18) + " " + (l * 0.60) + ",0";
+ // repouso: leve, com as microexpressões (canto que sobe, boca de lado, entreaberta)
+ const f = FEICAO.atual, w = l * 0.30, mx = cx + f.dx * l * 0.10;
+ const yE = cy - Math.max(0, f.lado) * l * 0.12, yD = cy - Math.max(0, -f.lado) * l * 0.12;
+ const curva = l * (0.18 + f.curva * 0.35);
+ let d = "M " + (mx - w) + "," + yE + " Q " + mx + "," + (cy + curva - f.abre * l * 0.12) + " " + (mx + w) + "," + yD;
+ if (f.abre > 0.04) d += " Q " + mx + "," + (cy + curva + f.abre * l * 0.30) + " " + (mx - w) + "," + yE;
+ return d;
+}
+
+/* Sobrancelha i (0 = esquerda): humor do gesto + microexpressão + desce um pouco ao piscar. */
+function caminhoSobrancelha(i, esc) {
+ const c = i === 0 ? E1 : E2, lado = i === 0 ? 1 : -1, largura = RX * esc * 0.85, f = FEICAO.atual;
+ const ab = i === 0 ? r.ab : (r.ab2 === null ? r.ab : r.ab2);
+ const piscando = 1 - Math.min(1, Math.max(0, ab));
+ const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC - (f.sob[i] - piscando * 3) * ESC;
+ const inc = ((r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : 0) + f.inc[i]) * lado * ESC;
+ const ele = r.esc > 1.1 ? -15 * ESC : 0;
+ return 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele);
}
// ------------------------------------------------------------------ desenho
@@ -369,6 +385,50 @@ const LIVRES_HUMOR = {
cansado: ["olhar_baixo", "piscar2"],
};
let HUMOR = "normal";
+
+/* Microexpressões acordado: de vez em quando a boca e as sobrancelhas mexem sozinhas (um canto
+ da boca sobe, uma sobrancelha arqueia, um suspiro) e voltam devagar ao neutro. O humor do modo
+ vida escolhe quais. Valores em "pixels de projeto" (multiplicados por ESC no desenho). */
+function feicaoNeutra() { return { curva: 0, lado: 0, abre: 0, dx: 0, sob: [0, 0], inc: [0, 0] }; }
+const FEICAO = { atual: feicaoNeutra(), alvo: feicaoNeutra(), prox: 0, volta: 0, t: 0 };
+const ao = () => (Math.random() < 0.5 ? 1 : -1);
+const MICRO = {
+ arquear: () => { const f = feicaoNeutra(), i = Math.random() < 0.5 ? 0 : 1; f.sob[i] = 12; f.inc[i] = -4; return f; },
+ interesse: () => Object.assign(feicaoNeutra(), { sob: [8, 8], abre: 0.35 }),
+ meioSorriso: () => Object.assign(feicaoNeutra(), { lado: ao(), curva: 0.1 }),
+ sorrisinho: () => Object.assign(feicaoNeutra(), { curva: 0.35, sob: [4, 4] }),
+ ladinho: () => { const d = ao(); return Object.assign(feicaoNeutra(), { dx: d, lado: d * 0.4, curva: -0.08 }); },
+ pensativo: () => Object.assign(feicaoNeutra(), { inc: [8, 8], sob: [-3, -3], dx: ao() * 0.7, curva: -0.15 }),
+ preocupado: () => Object.assign(feicaoNeutra(), { inc: [-9, -9], sob: [4, 4], curva: -0.3 }),
+ suspiro: () => Object.assign(feicaoNeutra(), { sob: [5, 5], abre: 0.6, curva: -0.05 }),
+};
+const MICRO_HUMOR = {
+ feliz: ["sorrisinho", "sorrisinho", "meioSorriso", "arquear", "interesse"],
+ tranquilo: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse"],
+ entediado: ["ladinho", "ladinho", "pensativo", "suspiro", "arquear"],
+ carente: ["preocupado", "preocupado", "suspiro", "meioSorriso"],
+ triste: ["preocupado", "suspiro", "pensativo"],
+ cansado: ["suspiro", "suspiro", "ladinho"],
+ normal: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse", "pensativo", "suspiro"],
+};
+function atualizarFeicao(s) {
+ const dt = Math.min(0.1, Math.max(0, s - (FEICAO.t || s)));
+ FEICAO.t = s;
+ // só no rosto "de repouso": gesto com boca própria (sorriso, triste...) ou dormindo = neutro
+ const livre = !r.dormindo && !congelado && r.boca === "neutra";
+ if (!livre) {
+ FEICAO.alvo = feicaoNeutra();
+ FEICAO.prox = Math.max(FEICAO.prox, s + 1.5);
+ } else if (s >= FEICAO.prox) {
+ const lista = MICRO_HUMOR[HUMOR] || MICRO_HUMOR.normal;
+ FEICAO.alvo = MICRO[lista[Math.floor(Math.random() * lista.length)]]();
+ FEICAO.volta = s + 0.9 + Math.random() * 1.6;
+ FEICAO.prox = s + (2.5 + Math.random() * 5) * ({ cansado: 1.6, triste: 1.4, feliz: 0.8 }[HUMOR] || 1);
+ } else if (s >= FEICAO.volta) FEICAO.alvo = feicaoNeutra();
+ const k = 1 - Math.exp(-dt * 6), a = FEICAO.atual, b = FEICAO.alvo;
+ for (const c of ["curva", "lado", "abre", "dx"]) a[c] += (b[c] - a[c]) * k;
+ for (let i = 0; i < 2; i++) { a.sob[i] += (b.sob[i] - a.sob[i]) * k; a.inc[i] += (b.inc[i] - a.inc[i]) * k; }
+}
function gestoSolto() {
const lista = LIVRES_HUMOR[HUMOR] || LIVRES;
return lista[Math.floor(Math.random() * lista.length)];
@@ -472,13 +532,7 @@ function desenhar() {
const brow = document.getElementById('sobrancelha' + (i + 1));
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] - 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));
+ brow.setAttribute('d', caminhoSobrancelha(i, esc));
brow.setAttribute('stroke', rgb(r.cor));
brow.setAttribute('stroke-width', 7 * ESC);
}
@@ -495,12 +549,7 @@ function tracosDoRosto(esc) {
} 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 });
- });
+ for (let i = 0; i < 2; i++) tracos.push({ d: caminhoSobrancelha(i, esc), largura: 9 * ESC });
}
return tracos;
}
@@ -1120,6 +1169,7 @@ function laco(t) {
if (window.innerWidth !== W || window.innerHeight !== H) ajustar();
checarSilencio(t / 1000);
+ atualizarFeicao(t / 1000);
const ocupado = congelado ? true : passo(dt);
if (!ocupado) {
diff --git a/carinha.html b/carinha.html
index 3b2acaa..a94269b 100644
--- a/carinha.html
+++ b/carinha.html
@@ -203,8 +203,24 @@ function caminhoBoca(tipo) {
return "M " + (cx - meia) + "," + (cy - l * 0.10) +
" Q " + cx + "," + (cy + l * 0.42) + " " + (cx + meia) + "," + (cy - l * 0.10);
}
- return "M " + (cx - l * 0.30) + "," + cy + // repouso: leve
- " q " + (l * 0.30) + "," + (l * 0.18) + " " + (l * 0.60) + ",0";
+ // repouso: leve, com as microexpressões (canto que sobe, boca de lado, entreaberta)
+ const f = FEICAO.atual, w = l * 0.30, mx = cx + f.dx * l * 0.10;
+ const yE = cy - Math.max(0, f.lado) * l * 0.12, yD = cy - Math.max(0, -f.lado) * l * 0.12;
+ const curva = l * (0.18 + f.curva * 0.35);
+ let d = "M " + (mx - w) + "," + yE + " Q " + mx + "," + (cy + curva - f.abre * l * 0.12) + " " + (mx + w) + "," + yD;
+ if (f.abre > 0.04) d += " Q " + mx + "," + (cy + curva + f.abre * l * 0.30) + " " + (mx - w) + "," + yE;
+ return d;
+}
+
+/* Sobrancelha i (0 = esquerda): humor do gesto + microexpressão + desce um pouco ao piscar. */
+function caminhoSobrancelha(i, esc) {
+ const c = i === 0 ? E1 : E2, lado = i === 0 ? 1 : -1, largura = RX * esc * 0.85, f = FEICAO.atual;
+ const ab = i === 0 ? r.ab : (r.ab2 === null ? r.ab : r.ab2);
+ const piscando = 1 - Math.min(1, Math.max(0, ab));
+ const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC - (f.sob[i] - piscando * 3) * ESC;
+ const inc = ((r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : 0) + f.inc[i]) * lado * ESC;
+ const ele = r.esc > 1.1 ? -15 * ESC : 0;
+ return 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele);
}
// ------------------------------------------------------------------ desenho
@@ -369,6 +385,50 @@ const LIVRES_HUMOR = {
cansado: ["olhar_baixo", "piscar2"],
};
let HUMOR = "normal";
+
+/* Microexpressões acordado: de vez em quando a boca e as sobrancelhas mexem sozinhas (um canto
+ da boca sobe, uma sobrancelha arqueia, um suspiro) e voltam devagar ao neutro. O humor do modo
+ vida escolhe quais. Valores em "pixels de projeto" (multiplicados por ESC no desenho). */
+function feicaoNeutra() { return { curva: 0, lado: 0, abre: 0, dx: 0, sob: [0, 0], inc: [0, 0] }; }
+const FEICAO = { atual: feicaoNeutra(), alvo: feicaoNeutra(), prox: 0, volta: 0, t: 0 };
+const ao = () => (Math.random() < 0.5 ? 1 : -1);
+const MICRO = {
+ arquear: () => { const f = feicaoNeutra(), i = Math.random() < 0.5 ? 0 : 1; f.sob[i] = 12; f.inc[i] = -4; return f; },
+ interesse: () => Object.assign(feicaoNeutra(), { sob: [8, 8], abre: 0.35 }),
+ meioSorriso: () => Object.assign(feicaoNeutra(), { lado: ao(), curva: 0.1 }),
+ sorrisinho: () => Object.assign(feicaoNeutra(), { curva: 0.35, sob: [4, 4] }),
+ ladinho: () => { const d = ao(); return Object.assign(feicaoNeutra(), { dx: d, lado: d * 0.4, curva: -0.08 }); },
+ pensativo: () => Object.assign(feicaoNeutra(), { inc: [8, 8], sob: [-3, -3], dx: ao() * 0.7, curva: -0.15 }),
+ preocupado: () => Object.assign(feicaoNeutra(), { inc: [-9, -9], sob: [4, 4], curva: -0.3 }),
+ suspiro: () => Object.assign(feicaoNeutra(), { sob: [5, 5], abre: 0.6, curva: -0.05 }),
+};
+const MICRO_HUMOR = {
+ feliz: ["sorrisinho", "sorrisinho", "meioSorriso", "arquear", "interesse"],
+ tranquilo: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse"],
+ entediado: ["ladinho", "ladinho", "pensativo", "suspiro", "arquear"],
+ carente: ["preocupado", "preocupado", "suspiro", "meioSorriso"],
+ triste: ["preocupado", "suspiro", "pensativo"],
+ cansado: ["suspiro", "suspiro", "ladinho"],
+ normal: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse", "pensativo", "suspiro"],
+};
+function atualizarFeicao(s) {
+ const dt = Math.min(0.1, Math.max(0, s - (FEICAO.t || s)));
+ FEICAO.t = s;
+ // só no rosto "de repouso": gesto com boca própria (sorriso, triste...) ou dormindo = neutro
+ const livre = !r.dormindo && !congelado && r.boca === "neutra";
+ if (!livre) {
+ FEICAO.alvo = feicaoNeutra();
+ FEICAO.prox = Math.max(FEICAO.prox, s + 1.5);
+ } else if (s >= FEICAO.prox) {
+ const lista = MICRO_HUMOR[HUMOR] || MICRO_HUMOR.normal;
+ FEICAO.alvo = MICRO[lista[Math.floor(Math.random() * lista.length)]]();
+ FEICAO.volta = s + 0.9 + Math.random() * 1.6;
+ FEICAO.prox = s + (2.5 + Math.random() * 5) * ({ cansado: 1.6, triste: 1.4, feliz: 0.8 }[HUMOR] || 1);
+ } else if (s >= FEICAO.volta) FEICAO.alvo = feicaoNeutra();
+ const k = 1 - Math.exp(-dt * 6), a = FEICAO.atual, b = FEICAO.alvo;
+ for (const c of ["curva", "lado", "abre", "dx"]) a[c] += (b[c] - a[c]) * k;
+ for (let i = 0; i < 2; i++) { a.sob[i] += (b.sob[i] - a.sob[i]) * k; a.inc[i] += (b.inc[i] - a.inc[i]) * k; }
+}
function gestoSolto() {
const lista = LIVRES_HUMOR[HUMOR] || LIVRES;
return lista[Math.floor(Math.random() * lista.length)];
@@ -472,13 +532,7 @@ function desenhar() {
const brow = document.getElementById('sobrancelha' + (i + 1));
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] - 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));
+ brow.setAttribute('d', caminhoSobrancelha(i, esc));
brow.setAttribute('stroke', rgb(r.cor));
brow.setAttribute('stroke-width', 7 * ESC);
}
@@ -495,12 +549,7 @@ function tracosDoRosto(esc) {
} 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 });
- });
+ for (let i = 0; i < 2; i++) tracos.push({ d: caminhoSobrancelha(i, esc), largura: 9 * ESC });
}
return tracos;
}
@@ -1120,6 +1169,7 @@ function laco(t) {
if (window.innerWidth !== W || window.innerHeight !== H) ajustar();
checarSilencio(t / 1000);
+ atualizarFeicao(t / 1000);
const ocupado = congelado ? true : passo(dt);
if (!ocupado) {