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 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 02:45:17 -03:00
co-authored by Claude Opus 5.5
parent 628db8492f
commit 230ca991a2
2 changed files with 94 additions and 24 deletions
+47 -12
View File
@@ -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);
}
}