O boneco da nossa página do boneco de palito não tem folha de sprites nem arquivos de animação. A cada quadro, algumas centenas de linhas de JavaScript calculam onde devem estar o quadril, os joelhos, as mãos e a cabeça, e desenham o resultado num <canvas> HTML. No laboratório de movimento ele anda, corre, faz um arremesso desarmado, pula e executa uma forma de lança de 17 poses, e você pode trocar de movimento a qualquer momento sem nenhum salto visível.
Essa página agora é um joguinho, o Stickman Spear Run: uma corrida de stickman em que o boneco carrega uma lança longa e ataca com uma estocada para a frente, segurando com as duas mãos, enquanto corre (Espaço ou ↑ para pular, F ou → para atacar). O laboratório de movimento original continua lá como segundo modo, então você ainda pode passar pelos movimentos — inclusive o arremesso desarmado, que só existe ali — e ver o esqueleto trabalhando. Este post explica as técnicas por trás disso, com trechos reais do código-fonte: cinemática inversa de dois ossos com a lei dos cossenos, trilhas de quadros-chave com easing, fixação dos pés no chão, squash and stretch e um pouco de física Verlet para o movimento secundário.
Um esqueleto feito de números
O boneco inteiro é um punhado de comprimentos de osso em unidades do canvas:
const SPINE=54, NECK=13, HEADR=12;
const UARM=27, FARM=25, THIGH=37, SHIN=37;
const HIP_H=66; // standing hip height: knees always slightly bent
Repare que HIP_H (66) é menor que THIGH + SHIN (74). O boneco nunca fica com os joelhos travados, e é isso que o faz parecer vivo mesmo parado. Como esses tamanhos são fixos, o script escala o canvas: ele escolhe uma escala para que o espaço de desenho tenha sempre cerca de 440 unidades de altura, seja qual for o tamanho do elemento na tela. A composição e os espaçamentos ficam idênticos num celular e num monitor 4K.
A cada quadro, uma função chamada buildPose(dt) devolve um conjunto de pontos em coordenadas de tela — pelve, peito, cabeça, ombro, quadril, os dois joelhos, os dois pés, os dois cotovelos e as duas mãos — e uma etapa de desenho separada os renderiza. Manter "onde estão as articulações" separado de "como desenhá-las" é o que nos deixa mudar o estilo da arte sem mexer no movimento.
IK de dois ossos com a lei dos cossenos
A cinemática inversa responde à pergunta: se o quadril está aqui e eu quero o pé ali, onde fica o joelho? Para uma cadeia de apenas dois ossos existe uma resposta exata, em forma fechada, e ela tem só algumas linhas:
function ik(root, target, L1, L2, sign){
let dx=target.x-root.x, dy=target.y-root.y;
let d=Math.hypot(dx,dy);
d=clamp(d, Math.abs(L1-L2)+.01, L1+L2-.01);
const base=Math.atan2(dy,dx);
let ca=clamp((L1*L1+d*d-L2*L2)/(2*L1*d),-1,1);
const a=Math.acos(ca);
const jointAng=base+sign*a;
return V(root.x+Math.cos(jointAng)*L1, root.y+Math.sin(jointAng)*L1);
}
O solver de dois ossos completo do stickman.js. Ele devolve a articulação do meio (joelho ou cotovelo).
O raciocínio é este. A coxa (L1), a canela (L2) e a linha reta do quadril ao pé (d) formam um triângulo. Conhecemos os três lados, então a lei dos cossenos nos dá qualquer ângulo:
L2² = L1² + d² − 2·L1·d·cos(a) ⇒ cos(a) = (L1² + d² − L2²) / (2·L1·d)
a é o ângulo no quadril entre a linha quadril–pé e a coxa. A linha quadril–pé aponta na direção base = atan2(dy, dx), então a coxa aponta para base ± a. Ande L1 nessa direção a partir do quadril e você chega ao joelho. O argumento sign escolhe qual das duas soluções espelhadas você recebe — os joelhos dobram para a frente, e quando as duas mãos seguram a lança, os cotovelos do lado de perto e do lado de longe usam sinais opostos para dobrarem em sentidos contrários.
Duas limitações (clamps) deixam o solver robusto:
dé limitado a um valor logo dentro de[|L1 − L2|, L1 + L2]. Se o alvo estiver fora de alcance, não existe triângulo eacosdevolveriaNaN. Com o clamp, o resultado vira "estique o máximo que puder na direção dele".- O cosseno é limitado a
[−1, 1]. Erros de ponto flutuante podem empurrá-lo para 1.0000001, o que também é território deNaN.
O código da perna acrescenta mais uma rede de segurança antes de chamar ik(): se o alvo do pé estiver mais longe do quadril do que THIGH + SHIN − 1.5, ele é puxado de volta pela mesma linha. A perna quase estica por completo, mas nunca trava, o que evita o "estalo" visível que aparece quando uma cadeia de dois ossos chega à extensão total.
Pernas usam IK, braços usam quase sempre FK
Dá vontade de usar IK em tudo. Nós não usamos. Os pés precisam estar em posições exatas — no chão —, então as pernas são resolvidas com IK. Já os braços balançando durante a caminhada não têm um alvo; eles têm um ritmo. Para isso, a cinemática direta (escolher ângulos e calcular posições) é mais simples e parece mais natural:
function armFK(aUp, flex){
const e=add(shoulder,{x:Math.sin(aUp)*UARM, y:Math.cos(aUp)*UARM});
const h=add(e, {x:Math.sin(aUp+flex)*FARM, y:Math.cos(aUp+flex)*FARM});
return {elbow:e, hand:h};
}
A convenção de ângulo aqui é "0 aponta reto para baixo, positivo balança para a frente", o que facilita pensar nas poses de caminhada. Durante a caminhada, cada braço balança no sentido oposto ao da perna do mesmo lado, e o cotovelo dobra mais no balanço para a frente e estica no balanço para trás — um detalhe pequeno que elimina o aspecto robótico de pêndulo. Os braços só passam para IK quando as mãos precisam estar num lugar específico: segurando a haste da lança.
Um ciclo de marcha numa única variável de fase
Andar e correr são comandados por um único número, phase, que vai de 0 a 1 em loop. O pé de perto usa phase e o de longe usa phase + 0.5, então eles estão sempre meio ciclo defasados. Um segundo número, gait, mistura de 0 (andar) a 1 (correr), e todos os parâmetros da marcha são interpolados por ele:
| Parâmetro | Andar (gait = 0) | Correr (gait = 1) |
|---|---|---|
Cadência cyc (ciclos por segundo) | 1.5 | 2.55 |
| Comprimento da passada | 44 | 60 |
| Altura do passo | 15 | 26 |
| Fator de apoio (fração do ciclo em que o pé fica no chão) | 0.62 | 0.40 |
| Amplitude do sobe e desce do quadril | 5 | 12 |
O fator de apoio é o mais importante. Acima de 0,5, os dois pés ficam no chão durante parte de cada ciclo — essa é a definição de andar. Abaixo de 0,5 existem momentos sem nenhum pé no chão: uma fase de voo, que é o que faz a corrida parecer corrida. Misturar gait suavemente (ele se aproxima do valor-alvo com easing a cada quadro) faz a troca entre andar e correr nunca dar salto.
A trajetória de cada pé é dividida em duas partes. Durante o apoio (a primeira fração duty do ciclo), ele desliza linearmente de meia passada à frente do quadril até meia passada atrás, rente ao chão. Durante o balanço, ele volta para a frente numa curva smoothstep, erguido por meia onda senoidal:
if(pph<duty){ const u=pph/duty; fx=lerp(stride/2,-stride/2,u); fy=groundY; }
else{ const u=(pph-duty)/(1-duty); fx=lerp(-stride/2,stride/2,smooth(u));
fy=groundY - stepH*Math.sin(PI*u); }
O quadril sobe e desce duas vezes por ciclo (uma vez por passo) usando -|sin(phase·2π)|; na corrida, essa curva é defasada para que o ponto mais alto coincida com a fase de voo. Os pés também são empurrados um pouco para a frente, proporcionalmente à inclinação do corpo, para continuarem debaixo do centro de massa.
Pés fixos no chão: acabando com o moonwalk
O boneco fica num ponto fixo da tela enquanto o chão rola por baixo. Se a velocidade de rolagem estiver errada, o pé apoiado escorrega pelo chão — o clássico bug do moonwalk. A correção é uma linha:
const scroll = stride*cyc/duty; // ground speed = stride × cadence ÷ duty factor
Por que funciona? Durante o apoio, um pé percorre uma stride (passada) inteira em relação ao quadril. O apoio dura a fração duty de um ciclo, e um ciclo dura 1/cyc segundos, então o apoio dura duty/cyc segundos. Portanto, o pé se move para trás em relação ao corpo a
stride ÷ (duty ÷ cyc) = stride × cyc ÷ duty
Se o chão andar para trás exatamente nessa velocidade, pé e chão se movem juntos, e o pé fica fixo. Com os números da tabela, a caminhada rola a 44 × 1,5 ÷ 0,62 ≈ 106 unidades por segundo e a corrida a 60 × 2,55 ÷ 0,40 ≈ 383. Repare que a corrida é mais de três vezes mais rápida, embora a passada seja só um terço mais longa: o fator de apoio menor faz a maior parte do trabalho. Um erro comum é usar stride × cadence, que ignora o fator de apoio e faz o pé de apoio deslizar.
O chão da cena é uma fileira de pontinhos suaves que se move scroll × dt a cada quadro. Quando o boneco para de se deslocar para arremessar, pular ou executar a forma de lança, a rolagem cai para zero e os pontinhos param também.
Trilhas de quadros-chave com easing
Arremessar e pular não são ciclos; são movimentos únicos, com antecipação, ação e recuperação. Para eles usamos trilhas de quadros-chave minúsculas: arrays de pares [tempo, valor] amostrados num tempo normalizado p de 0 a 1:
function track(t, keys, ease){
ease=ease||smooth;
if(t<=keys[0][0]) return keys[0][1];
for(let i=0;i<keys.length-1;i++){
const [t0,v0]=keys[i],[t1,v1]=keys[i+1];
if(t<=t1){ let u=(t-t0)/((t1-t0)||1); return lerp(v0,v1,ease(u)); }
}
return keys[keys.length-1][1];
}
Cada propriedade animada tem a sua própria trilha. No pulo, o deslocamento vertical do corpo é:
rootY = track(p,[[0,0],[0.2,10],[0.32,-6],[0.5,-160],[0.68,-70],[0.82,4],[0.9,-2],[1,0]],
(p<0.5?easeOutCubic:easeInCubic));
Leia como uma história: agacha (+10 — lembre que no canvas o y aponta para baixo), dá impulso, sobe 160 unidades até o ápice, cai, amortece a aterrissagem um pouco abaixo do nível do chão e se acomoda. O easing muda no meio do caminho: easeOutCubic na subida (saída rápida, desacelerando perto do ápice) e easeInCubic na descida (começo lento, acelerando). É uma imitação barata, mas convincente, da parábola da gravidade.
A forma de lança leva a mesma ideia mais longe: 18 pontos de passagem (as 17 poses mais uma chave final igual à de abertura, para a forma fazer loop sem emenda), cada um descrevendo o corpo inteiro — ângulo da lança, posição da pegada, inclinação, transferência de peso, agachamento, largura da base, elevação e squash —, com um easing opcional por segmento. Dois truques fazem a leitura funcionar. Dois pontos idênticos em sequência criam uma pausa, o momento congelado de "pose" de uma forma de artes marciais. E as estocadas usam easeOutCubic, então a lança sai disparada e desacelera até a pose, o que passa uma sensação seca e potente.
Squash and stretch
O pulo também anima uma escala vertical, sy: achatada para 0,84 no agachamento, esticada para 1,14 na decolagem e achatada de novo para 0,82 na aterrissagem. A escala horizontal é derivada dela para que o boneco pareça manter o volume:
sx = 1+(1-sy)*0.7; // squash down → get wider; stretch up → get thinner
const pivot=V(anchorX, groundY);
const S=p=>V(pivot.x+(p.x-pivot.x)*sx, pivot.y+(p.y-pivot.y)*sy);
Cada articulação passa por S() no fim de buildPose. Escalar em torno do ponto do chão logo abaixo do boneco (e não em torno do seu centro) mantém os pés no chão enquanto o corpo se achata — exatamente o que um animador desenharia à mão. O raio da cabeça é escalado pela média de sx e sy.
Movimento secundário: uma mola e uma cadeia Verlet
Movimento primário é o que o personagem faz. Movimento secundário é o que vem atrás — cabelo, tecido, uma cabeça que se atrasa um instante em relação ao corpo. É barato de adicionar e faz uma diferença enorme. O nosso boneco tem dois tipos.
Uma mola amortecida na cabeça
A cabeça não fica rígida em cima do pescoço. Ela é puxada para a posição ideal por uma mola (rigidez 260, amortecimento 26), então, quando o corpo dá um tranco, a cabeça se atrasa e se acomoda com um leve passar do ponto. O amortecimento é baixo o bastante para deixar um pouquinho de balanço de desenho animado.
Cabelo com Verlet
O tufo de cabelo é uma cadeia de quatro pontos espaçados 9 unidades entre si. A integração de Verlet guarda a posição atual e a anterior de cada ponto; a velocidade é simplesmente a diferença entre elas. Isso deixa a atualização bem curta:
const s=scarf[i], vx=(s.x-s.px)*0.86, vy=(s.y-s.py)*0.86;
s.px=s.x; s.py=s.y;
s.x+=vx+windX*dt*dt*60; s.y+=vy+g*dt*dt*60;
O fator 0,86 é o arrasto do ar. windX é um empurrão para trás que cresce com a velocidade da corrida (e ganha um reforço extra nos pulos), e g é a gravidade. Depois de mover os pontos, seis iterações de uma restrição de distância puxam cada par de volta para exatamente 9 unidades, com o primeiro ponto preso à parte de trás da cabeça. Fixar o ponto e aplicar as restrições é tudo de que se precisa: o cabelo esvoaça para trás na corrida, cai na caminhada e chicoteia nos pulos — e nada disso foi animado à mão.
Desenhando membros macios
Um boneco de palito desenhado com linhas retas parece duro. O nosso passa os três pontos de cada membro (por exemplo quadril, joelho e pé) por uma spline Catmull-Rom, adensa o resultado numa curva suave e preenche uma fita afunilada em volta dela — grossa no quadril, mais fina no pé — com pontas arredondadas. O resultado parece um membro de desenho animado estilo "mangueira de borracha", que curva em vez de articular. Os membros do lado de longe são desenhados primeiro, mais finos, mais escuros e com 42% de opacidade, o que dá sensação de profundidade sem nada de 3D.
Por fim, o loop é educado. O dt de cada quadro é limitado a 33 ms, para que um engasgo não jogue o cabelo do outro lado da tela. A animação pausa quando o palco sai da área visível ou a aba fica oculta e, se o sistema do visitante pedir menos movimento, ela começa pausada num único quadro parado.
O que vale a pena copiar
- IK de dois ossos cabe em dez linhas. Limite a distância e o cosseno e ele nunca vai produzir
NaN. - Use IK onde há um alvo e FK onde há um ritmo.
- Velocidade do chão = passada × cadência ÷ fator de apoio. Acerte isso e os pés grudam no chão em qualquer marcha.
- Trilhas de quadros-chave com easing por segmento bastam para movimentos únicos expressivos. Duplique uma chave para ter uma pausa.
- Faça o squash em torno do ponto de contato, não do centro, e derive a largura da altura para preservar o volume.
- Uma mola e uma cadeia Verlet de quatro pontos dão quase toda a sensação de "vivo" com quase nenhum código.
Dá para ver tudo isso em movimento na página do boneco de palito, que virou o Stickman Spear Run — corra, pule e dê estocadas com a lança —, com o laboratório de movimento como segundo modo. Se você curte bonecos de palito, o nosso próximo jogo de quebra-cabeça de física, Ink Left, também é estrelado por um — desenhado num caderno inacabado.