From 5fcd038861a5c13bc76fad99f669f625a50f55a9 Mon Sep 17 00:00:00 2001 From: julio Date: Tue, 6 Oct 2026 04:31:54 -0300 Subject: [PATCH] =?UTF-8?q?feat(rosto):=20microexpress=C3=B5es=20na=20boca?= =?UTF-8?q?=20e=20nas=20sobrancelhas=20acordado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit De vez em quando, fora dos gestos, um canto da boca sobe, uma sobrancelha arqueia, a testa franze pensando, a boca entreabre num suspiro, e tudo volta suave ao neutro. O humor do modo vida escolhe quais (carente fica preocupado, feliz dá sorrisinhos, cansado suspira). As sobrancelhas descem um pouco ao piscar. Vale para o SVG e para a Matriz de LEDs. Co-Authored-By: Claude Opus 5.5 --- android/app/assets/carinha.html | 80 ++++++++++++++++++++++++++------- carinha.html | 80 ++++++++++++++++++++++++++------- 2 files changed, 130 insertions(+), 30 deletions(-) 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) {