From 230ca991a253f10860ce10dd32d80079ac986049 Mon Sep 17 00:00:00 2001 From: julio Date: Tue, 6 Oct 2026 02:45:17 -0300 Subject: [PATCH] feat(rosto): mao com dedos, polegar e punho no lugar da luva Quatro dedos separados (medio maior, levemente abertos), polegar para o lado do rosto, punho de robo e contorno so na borda externa. Ao acenar os dedos balancam; ao pensar o punho fecha com o indicador no rosto; para esconder e espiar as maos cobrem o olho inteiro. Co-Authored-By: Claude Opus 5.5 --- android/app/assets/rosto-motor.js | 59 ++++++++++++++++++++++++------- rosto-motor.js | 59 ++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 24 deletions(-) diff --git a/android/app/assets/rosto-motor.js b/android/app/assets/rosto-motor.js index cbe18ea..204bca7 100644 --- a/android/app/assets/rosto-motor.js +++ b/android/app/assets/rosto-motor.js @@ -265,16 +265,51 @@ 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 - // Luva simples: polegar atrás da palma e dois vincos marcando os dedos. - function mao(c, o, x, y, s, ang, espelho) { - const escura = o.cor.map((v) => v * 0.36); + // 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) { + 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); - c.fillStyle = css(escura); c.strokeStyle = css(o.cor); c.lineWidth = s * 0.1; c.lineJoin = 'round'; c.lineCap = 'round'; - c.beginPath(); c.ellipse(0.55 * s, 0.12 * s, 0.24 * s, 0.4 * s, -0.55, 0, Math.PI * 2); c.fill(); c.stroke(); // polegar - caixa(c, -0.6 * s, -0.75 * s, 1.2 * s, 1.55 * s, 0.55 * s); c.fill(); c.stroke(); // palma e dedos - c.lineWidth = s * 0.07; - for (const dx of [-0.2, 0.2]) { c.beginPath(); c.moveTo(dx * s, -0.72 * s); c.lineTo(dx * s, -0.34 * s); c.stroke(); } // vincos + 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; + 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(); } @@ -291,17 +326,17 @@ const treme = Math.sin(o.t * 24) * s * 0.025; // tímido: a mão treme um pouco let afasta = 0; if (maos.modo === 'espiar') afasta = s * 1.35 * (faixaDe(k, 0.3, 0.42) - faixaDe(k, 0.66, 0.76)); - mao(c, o, e.cx - afasta + treme, altura(e.cy + e.ry * 0.15), s, -0.12 - afasta / s * 0.3, 1); - mao(c, o, d.cx - treme, altura(d.cy + d.ry * 0.15), s, 0.12, -1); + mao(c, o, e.cx - afasta + treme, altura(e.cy + e.ry * 0.3), s * 1.18, -0.12 - afasta / s * 0.3, 1); + mao(c, o, d.cx - treme, altura(d.cy + d.ry * 0.3), s * 1.18, 0.12, -1); } else if (maos.modo === 'acenar') { const x = d.cx + d.ry * 2.1, y0 = d.cy + d.ry * 0.7; const y = k < 0.8 ? subir(y0, 0, 0.2) : y0 + (fora - y0) * faixaDe(k, 0.82, 1); const balanco = k > 0.2 && k < 0.8 ? Math.sin((k - 0.2) * Math.PI * 7) * 0.38 : 0; - mao(c, o, x, y, s * 0.9, balanco, -1); + mao(c, o, x, y, s * 0.95, balanco, -1, 0, 1); } else if (maos.modo === 'pensar') { const x = (e.cx + d.cx) / 2 + e.ry * 1.05, y0 = e.cy + e.ry * 1.4; const y = (k < 0.8 ? subir(y0, 0, 0.2) : y0 + (fora - y0) * faixaDe(k, 0.82, 1)) + Math.sin(o.t * 9) * s * 0.03; - mao(c, o, x, y, s * 0.75, -0.35, -1); + mao(c, o, x, y, s * 0.85, -0.5, -1, 0.95, 0, true); } } diff --git a/rosto-motor.js b/rosto-motor.js index cbe18ea..204bca7 100644 --- a/rosto-motor.js +++ b/rosto-motor.js @@ -265,16 +265,51 @@ 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 - // Luva simples: polegar atrás da palma e dois vincos marcando os dedos. - function mao(c, o, x, y, s, ang, espelho) { - const escura = o.cor.map((v) => v * 0.36); + // 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) { + 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); - c.fillStyle = css(escura); c.strokeStyle = css(o.cor); c.lineWidth = s * 0.1; c.lineJoin = 'round'; c.lineCap = 'round'; - c.beginPath(); c.ellipse(0.55 * s, 0.12 * s, 0.24 * s, 0.4 * s, -0.55, 0, Math.PI * 2); c.fill(); c.stroke(); // polegar - caixa(c, -0.6 * s, -0.75 * s, 1.2 * s, 1.55 * s, 0.55 * s); c.fill(); c.stroke(); // palma e dedos - c.lineWidth = s * 0.07; - for (const dx of [-0.2, 0.2]) { c.beginPath(); c.moveTo(dx * s, -0.72 * s); c.lineTo(dx * s, -0.34 * s); c.stroke(); } // vincos + 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; + 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(); } @@ -291,17 +326,17 @@ const treme = Math.sin(o.t * 24) * s * 0.025; // tímido: a mão treme um pouco let afasta = 0; if (maos.modo === 'espiar') afasta = s * 1.35 * (faixaDe(k, 0.3, 0.42) - faixaDe(k, 0.66, 0.76)); - mao(c, o, e.cx - afasta + treme, altura(e.cy + e.ry * 0.15), s, -0.12 - afasta / s * 0.3, 1); - mao(c, o, d.cx - treme, altura(d.cy + d.ry * 0.15), s, 0.12, -1); + mao(c, o, e.cx - afasta + treme, altura(e.cy + e.ry * 0.3), s * 1.18, -0.12 - afasta / s * 0.3, 1); + mao(c, o, d.cx - treme, altura(d.cy + d.ry * 0.3), s * 1.18, 0.12, -1); } else if (maos.modo === 'acenar') { const x = d.cx + d.ry * 2.1, y0 = d.cy + d.ry * 0.7; const y = k < 0.8 ? subir(y0, 0, 0.2) : y0 + (fora - y0) * faixaDe(k, 0.82, 1); const balanco = k > 0.2 && k < 0.8 ? Math.sin((k - 0.2) * Math.PI * 7) * 0.38 : 0; - mao(c, o, x, y, s * 0.9, balanco, -1); + mao(c, o, x, y, s * 0.95, balanco, -1, 0, 1); } else if (maos.modo === 'pensar') { const x = (e.cx + d.cx) / 2 + e.ry * 1.05, y0 = e.cy + e.ry * 1.4; const y = (k < 0.8 ? subir(y0, 0, 0.2) : y0 + (fora - y0) * faixaDe(k, 0.82, 1)) + Math.sin(o.t * 9) * s * 0.03; - mao(c, o, x, y, s * 0.75, -0.35, -1); + mao(c, o, x, y, s * 0.85, -0.5, -1, 0.95, 0, true); } }