feat(rosto): esconder e espiar com maos inclinadas e movimento em arco
As maos sobem em arco a partir dos cantos de baixo, girando no caminho, com um leve passa-e-volta e uma chegando um instante depois da outra. Ficam inclinadas para dentro (~24 graus) mostrando o dorso (nos dos dedos, polegar para fora) e sem tapar o olho inteiro. Espiar gira a mao pelo punho como uma porta; acenar tambem balanca pelo punho. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -296,46 +296,73 @@
|
||||
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) {
|
||||
function mao(c, o, x, y, s, ang, espelho, dobra, mexe, indicador, pulso, costas) {
|
||||
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);
|
||||
if (pulso) c.translate(0, -0.7 * s); // (x, y) é o punho: gira como uma mão de verdade
|
||||
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();
|
||||
if (costas) { // dorso da mão: nós dos dedos em vez da linha da palma
|
||||
for (const nx of [-0.355, -0.12, 0.12, 0.355]) { c.beginPath(); c.arc(nx * s, -0.2 * s, 0.075 * s, Math.PI * 1.15, Math.PI * 1.85); c.stroke(); }
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
|
||||
// Sobe com um leve "passa e volta" no fim (como um braço que freia).
|
||||
const voltinha = (x) => { x = Math.max(0, Math.min(1, x)); const k = 1.6; return 1 + (k + 1) * Math.pow(x - 1, 3) + k * Math.pow(x - 1, 2); };
|
||||
|
||||
function desenharMaos(c, o, olhos, H) {
|
||||
if (!maos.modo) return;
|
||||
if (maos.t0 === null) maos.t0 = o.t;
|
||||
const k = (o.t - maos.t0) / DUR_MAOS[maos.modo];
|
||||
if (k >= 1) { maos.modo = null; return; }
|
||||
const [e, d] = olhos.slice().sort((a, b) => a.cx - b.cx); // olho da esquerda e da direita na tela
|
||||
const s = e.ry * 0.95, fora = H + s * 1.6;
|
||||
const subir = (y, a, b) => fora + (y - fora) * faixaDe(k, a, b);
|
||||
const s = e.ry * 0.95, fora = H + s * 1.8;
|
||||
if (maos.modo === 'esconder' || maos.modo === 'espiar') {
|
||||
const altura = (y) => (k < 0.8 ? subir(y, 0, 0.2) : y + (fora - y) * faixaDe(k, 0.82, 1));
|
||||
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.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);
|
||||
// Cada mão vem do canto de baixo, por fora, num arco, girando até ficar inclinada para dentro.
|
||||
// A da direita chega um pouco depois: duas mãos nunca se mexem igualzinho.
|
||||
const cobrir = (olho, lado, atraso) => {
|
||||
const ida = voltinha((k - atraso) / 0.24), volta = faixaDe(k, 0.8 + atraso * 0.5, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); // 0 = fora da tela, 1 = cobrindo
|
||||
const alvoX = olho.cx - lado * olho.ry * 0.05, alvoY = olho.cy + olho.ry * 0.38;
|
||||
const x = alvoX - lado * olho.ry * 1.6 * (1 - Math.min(1, v));
|
||||
const y = fora + (alvoY - fora) * v;
|
||||
const respira = Math.sin(o.t * 3 + lado) * s * 0.03 * Math.min(1, v);
|
||||
const ang = lado * (0.42 + 0.55 * (1 - Math.min(1, v))); // gira enquanto sobe, termina a ~24°
|
||||
return { x, y: y + respira, ang };
|
||||
};
|
||||
const pe = cobrir(e, 1, 0), pd = cobrir(d, -1, 0.05);
|
||||
if (maos.modo === 'espiar') {
|
||||
// a mão da esquerda gira pelo punho para fora, como uma porta, e o olho aparece
|
||||
const abre = faixaDe(k, 0.3, 0.44) - faixaDe(k, 0.64, 0.76);
|
||||
pe.ang -= abre * 1.05; pe.x -= abre * e.ry * 0.35; pe.y += abre * e.ry * 0.2;
|
||||
}
|
||||
const treme = Math.sin(o.t * 22) * s * 0.012; // tímido: tremidinha
|
||||
// dorso virado para quem olha: polegar para fora, perto da bochecha
|
||||
mao(c, o, pe.x + treme, pe.y, s * 1.1, pe.ang, -1, 0, 0, false, true, true);
|
||||
mao(c, o, pd.x - treme, pd.y, s * 1.1, pd.ang, 1, 0, 0, false, true, true);
|
||||
} 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.95, balanco, -1, 0, 1);
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
const x = d.cx + d.ry * 2.2 + d.ry * 0.6 * (1 - Math.min(1, v)), y0 = d.cy + d.ry * 1.55;
|
||||
const y = fora + (y0 - fora) * v;
|
||||
const balanco = k > 0.22 && k < 0.82 ? Math.sin((k - 0.22) * Math.PI * 7) * 0.32 : 0;
|
||||
mao(c, o, x, y, s * 0.95, -0.18 + balanco, -1, 0, 1, false, true);
|
||||
} else if (maos.modo === 'pensar') {
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
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;
|
||||
const y = fora + (y0 - fora) * v + Math.sin(o.t * 9) * s * 0.03;
|
||||
mao(c, o, x, y, s * 0.85, -0.5, -1, 0.95, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
+42
-15
@@ -296,46 +296,73 @@
|
||||
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) {
|
||||
function mao(c, o, x, y, s, ang, espelho, dobra, mexe, indicador, pulso, costas) {
|
||||
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);
|
||||
if (pulso) c.translate(0, -0.7 * s); // (x, y) é o punho: gira como uma mão de verdade
|
||||
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();
|
||||
if (costas) { // dorso da mão: nós dos dedos em vez da linha da palma
|
||||
for (const nx of [-0.355, -0.12, 0.12, 0.355]) { c.beginPath(); c.arc(nx * s, -0.2 * s, 0.075 * s, Math.PI * 1.15, Math.PI * 1.85); c.stroke(); }
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
|
||||
// Sobe com um leve "passa e volta" no fim (como um braço que freia).
|
||||
const voltinha = (x) => { x = Math.max(0, Math.min(1, x)); const k = 1.6; return 1 + (k + 1) * Math.pow(x - 1, 3) + k * Math.pow(x - 1, 2); };
|
||||
|
||||
function desenharMaos(c, o, olhos, H) {
|
||||
if (!maos.modo) return;
|
||||
if (maos.t0 === null) maos.t0 = o.t;
|
||||
const k = (o.t - maos.t0) / DUR_MAOS[maos.modo];
|
||||
if (k >= 1) { maos.modo = null; return; }
|
||||
const [e, d] = olhos.slice().sort((a, b) => a.cx - b.cx); // olho da esquerda e da direita na tela
|
||||
const s = e.ry * 0.95, fora = H + s * 1.6;
|
||||
const subir = (y, a, b) => fora + (y - fora) * faixaDe(k, a, b);
|
||||
const s = e.ry * 0.95, fora = H + s * 1.8;
|
||||
if (maos.modo === 'esconder' || maos.modo === 'espiar') {
|
||||
const altura = (y) => (k < 0.8 ? subir(y, 0, 0.2) : y + (fora - y) * faixaDe(k, 0.82, 1));
|
||||
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.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);
|
||||
// Cada mão vem do canto de baixo, por fora, num arco, girando até ficar inclinada para dentro.
|
||||
// A da direita chega um pouco depois: duas mãos nunca se mexem igualzinho.
|
||||
const cobrir = (olho, lado, atraso) => {
|
||||
const ida = voltinha((k - atraso) / 0.24), volta = faixaDe(k, 0.8 + atraso * 0.5, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta); // 0 = fora da tela, 1 = cobrindo
|
||||
const alvoX = olho.cx - lado * olho.ry * 0.05, alvoY = olho.cy + olho.ry * 0.38;
|
||||
const x = alvoX - lado * olho.ry * 1.6 * (1 - Math.min(1, v));
|
||||
const y = fora + (alvoY - fora) * v;
|
||||
const respira = Math.sin(o.t * 3 + lado) * s * 0.03 * Math.min(1, v);
|
||||
const ang = lado * (0.42 + 0.55 * (1 - Math.min(1, v))); // gira enquanto sobe, termina a ~24°
|
||||
return { x, y: y + respira, ang };
|
||||
};
|
||||
const pe = cobrir(e, 1, 0), pd = cobrir(d, -1, 0.05);
|
||||
if (maos.modo === 'espiar') {
|
||||
// a mão da esquerda gira pelo punho para fora, como uma porta, e o olho aparece
|
||||
const abre = faixaDe(k, 0.3, 0.44) - faixaDe(k, 0.64, 0.76);
|
||||
pe.ang -= abre * 1.05; pe.x -= abre * e.ry * 0.35; pe.y += abre * e.ry * 0.2;
|
||||
}
|
||||
const treme = Math.sin(o.t * 22) * s * 0.012; // tímido: tremidinha
|
||||
// dorso virado para quem olha: polegar para fora, perto da bochecha
|
||||
mao(c, o, pe.x + treme, pe.y, s * 1.1, pe.ang, -1, 0, 0, false, true, true);
|
||||
mao(c, o, pd.x - treme, pd.y, s * 1.1, pd.ang, 1, 0, 0, false, true, true);
|
||||
} 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.95, balanco, -1, 0, 1);
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
const x = d.cx + d.ry * 2.2 + d.ry * 0.6 * (1 - Math.min(1, v)), y0 = d.cy + d.ry * 1.55;
|
||||
const y = fora + (y0 - fora) * v;
|
||||
const balanco = k > 0.22 && k < 0.82 ? Math.sin((k - 0.22) * Math.PI * 7) * 0.32 : 0;
|
||||
mao(c, o, x, y, s * 0.95, -0.18 + balanco, -1, 0, 1, false, true);
|
||||
} else if (maos.modo === 'pensar') {
|
||||
const ida = voltinha(k / 0.22), volta = faixaDe(k, 0.82, 1);
|
||||
const v = Math.max(0, Math.min(1.08, ida)) * (1 - volta);
|
||||
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;
|
||||
const y = fora + (y0 - fora) * v + Math.sin(o.t * 9) * s * 0.03;
|
||||
mao(c, o, x, y, s * 0.85, -0.5, -1, 0.95, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user