Devlog · Animación procedural

Cómo animar un monigote con IK de dos huesos en canvas HTML

Cómo nuestro monigote camina, corre, salta y hace girar una lanza sin hoja de sprites: un solucionador de IK de diez líneas, una fórmula que evita que los pies resbalen y unos cuantos trucos baratos para darle vida.

Actualizado en Por ShengyuStudio10 min de lectura

El monigote de nuestra página del monigote no tiene hoja de sprites ni archivos de animación. En cada fotograma, unos cientos de líneas de JavaScript calculan dónde deben estar las caderas, las rodillas, las manos y la cabeza, y dibujan el resultado en un <canvas> HTML. En su laboratorio de movimiento camina, corre, hace un lanzamiento sin armas, salta y ejecuta una forma de lanza de 17 poses, y puedes pasar de una acción a otra en cualquier momento sin un salto visible.

Esa página es ahora un pequeño juego, Stickman Spear Run: un runner de stickman en el que el personaje lleva una lanza larga y ataca con una estocada frontal a dos manos mientras corre (Espacio o ↑ para saltar, F o → para atacar). El laboratorio de movimiento original sigue ahí como segundo modo, así que todavía puedes repasar los movimientos —incluido el lanzamiento sin armas, que solo existe allí— y ver trabajar al esqueleto. Este artículo explica las técnicas que hay detrás, con fragmentos reales del código: cinemática inversa de dos huesos con la ley de los cosenos, pistas de fotogramas clave con easing, anclaje de los pies, squash and stretch y un poco de física verlet para el movimiento secundario.

El monigote procedural en plena zancada sobre un escenario violeta oscuro
El personaje se dibuja a partir de una docena de posiciones de articulaciones calculadas en cada fotograma.

Un esqueleto hecho de números

Todo el personaje se reduce a un puñado de longitudes de hueso en unidades del 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

Fíjate en que HIP_H (66) es menor que THIGH + SHIN (74). El personaje nunca está de pie con las rodillas bloqueadas, y eso es lo que le hace parecer vivo incluso en reposo. Como estos tamaños son fijos, el script escala el canvas en su lugar: elige una escala para que el espacio de dibujo mida siempre unas 440 unidades de alto, sea cual sea el tamaño del elemento en pantalla. La composición y los espacios se mantienen idénticos en un móvil y en un monitor 4K.

En cada fotograma, una función llamada buildPose(dt) devuelve un conjunto de puntos en coordenadas de pantalla —pelvis, pecho, cabeza, hombro, cadera, las dos rodillas, los dos pies, los dos codos y las dos manos— y un paso de dibujo independiente los renderiza. Separar el «dónde están las articulaciones» del «cómo las dibujamos» es lo que nos permite cambiar el estilo gráfico sin tocar el movimiento.

IK de dos huesos con la ley de los cosenos

La cinemática inversa responde a la pregunta: si la cadera está aquí y quiero el pie allí, ¿dónde va la rodilla? Para una cadena de solo dos huesos existe una respuesta exacta y cerrada, y ocupa apenas unas líneas:

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);
}

El solucionador de dos huesos completo de stickman.js. Devuelve la articulación intermedia (rodilla o codo).

root (cadera) joint (rodilla) target (pie) L1 L2 d a
Cadera, rodilla y pie forman un triángulo cuyos tres lados ya conocemos.

El razonamiento es este. El muslo (L1), la espinilla (L2) y la recta que va de la cadera al pie (d) forman un triángulo. Conocemos los tres lados, así que la ley de los cosenos nos da cualquier ángulo:

Ley de los cosenos

L2² = L1² + d² − 2·L1·d·cos(a) ⇒ cos(a) = (L1² + d² − L2²) / (2·L1·d)

a es el ángulo en la cadera entre la línea cadera-pie y el muslo. La línea cadera-pie apunta en la dirección base = atan2(dy, dx), así que el muslo apunta a base ± a. Avanza L1 en esa dirección desde la cadera y estarás en la rodilla. El argumento sign elige cuál de las dos soluciones simétricas obtienes: las rodillas se doblan hacia delante y, cuando las dos manos sujetan la lanza, el codo cercano y el lejano usan signos opuestos para doblarse en sentidos contrarios.

Dos clamps hacen robusto al solucionador:

  • d se limita a justo dentro de [|L1 − L2|, L1 + L2]. Si el objetivo queda fuera de alcance, no existe triángulo y acos devolvería NaN. Con el límite, el resultado pasa a ser «estírate todo lo que puedas hacia él».
  • El coseno se limita a [−1, 1]. El error de coma flotante puede empujarlo a 1,0000001, que también es territorio NaN.

El código de las piernas añade una red de seguridad más antes de llamar a ik(): si el objetivo de un pie está más lejos de la cadera que THIGH + SHIN − 1.5, se recoge a lo largo de la misma línea. La pierna puede casi estirarse del todo pero nunca se bloquea por completo, lo que evita el «chasquido» visible que se produce cuando una cadena de dos huesos llega a la extensión máxima.

Las piernas usan IK; los brazos, casi siempre FK

Es tentador usar IK para todo. Nosotros no lo hacemos. Los pies tienen que estar en posiciones exactas —en el suelo—, así que las piernas se resuelven con IK. Los brazos que se balancean al caminar no tienen un objetivo: tienen un ritmo. Para eso, la cinemática directa (eliges ángulos y calculas posiciones) es más sencilla y queda más 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};
}

La convención de ángulos aquí es «0 apunta recto hacia abajo y los valores positivos van hacia delante», lo que hace muy fácil pensar en las poses de marcha. Al caminar, cada brazo oscila en sentido contrario a la pierna de su lado, y el codo se dobla más en el balanceo hacia delante y se estira en el de vuelta: un pequeño detalle que elimina el aspecto robótico de péndulo. Los brazos solo pasan a IK cuando las manos tienen que estar en un sitio concreto: agarrando el asta de la lanza.

Un ciclo de marcha en una sola variable de fase

Caminar y correr dependen de un único número, phase, que va de 0 a 1 en bucle. El pie cercano usa phase y el lejano phase + 0.5, así que siempre van medio ciclo desfasados. Un segundo número, gait, se mezcla de 0 (caminar) a 1 (correr), y todos los parámetros de la marcha se interpolan con él:

ParámetroCaminar (gait = 0)Correr (gait = 1)
Cadencia cyc (ciclos por segundo)1,52,55
Longitud de zancada4460
Altura del paso1526
Factor de apoyo (parte del ciclo en que un pie está en el suelo)0,620,40
Amplitud del balanceo de cadera512

El factor de apoyo es el importante. Por encima de 0,5, los dos pies están en el suelo durante parte de cada ciclo: esa es la definición de caminar. Por debajo de 0,5 hay momentos sin ningún pie apoyado, una fase de vuelo, y eso es lo que hace que correr parezca correr. Como gait se mezcla de forma suave (se acerca a su objetivo poco a poco en cada fotograma), el paso de caminar a correr nunca da saltos.

La trayectoria de cada pie se divide en dos. Durante la parte de apoyo (la primera fracción duty de su ciclo) se desliza linealmente desde media zancada por delante de la cadera hasta media zancada por detrás, plano sobre el suelo. Durante la parte de balanceo vuelve hacia delante siguiendo una curva smoothstep, levantado por media onda sinusoidal:

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); }

La cadera sube y baja dos veces por ciclo (una por paso) con -|sin(phase·2π)|; al correr, esa curva se desfasa para que el punto más alto coincida con la fase de vuelo. Además, los pies se adelantan un poco en proporción a la inclinación del cuerpo para que sigan bajo el centro de masas.

Anclar los pies: adiós al moonwalk

El personaje se queda en un punto fijo de la pantalla mientras el suelo se desplaza por debajo. Si la velocidad de desplazamiento es incorrecta, el pie apoyado resbala por el suelo: el clásico bug del moonwalk. La solución es una línea:

const scroll = stride*cyc/duty;   // ground speed = stride × cadence ÷ duty factor

¿Por qué funciona? Durante el apoyo, un pie recorre una stride completa respecto a la cadera. El apoyo dura una fracción duty del ciclo, y un ciclo dura 1/cyc segundos, así que el apoyo dura duty/cyc segundos. Por tanto, el pie se mueve hacia atrás respecto al cuerpo a

stride ÷ (duty ÷ cyc) = stride × cyc ÷ duty

Si el suelo se mueve hacia atrás exactamente a esa velocidad, el pie y el suelo se mueven juntos y el pie queda anclado. Con los números de la tabla, al caminar el suelo avanza a 44 × 1,5 ÷ 0,62 ≈ 106 unidades por segundo y al correr a 60 × 2,55 ÷ 0,40 ≈ 383. Fíjate en que correr es más de tres veces más rápido aunque la zancada solo sea un tercio más larga: el factor de apoyo más corto hace casi todo el trabajo. Un error habitual es usar stride × cadence, que ignora el factor de apoyo y hace que el pie de apoyo se deslice.

El suelo de la escena es una fila de puntos suaves que se desplaza scroll × dt en cada fotograma. Cuando el personaje deja de desplazarse para lanzar, saltar o ejecutar la forma de lanza, el desplazamiento cae a cero y los puntos también se detienen.

Pistas de fotogramas clave con easing

Lanzar y saltar no son ciclos: son movimientos únicos con anticipación, acción y recuperación. Para ellos usamos pistas de fotogramas clave diminutas: arrays de pares [tiempo, valor] muestreados en un tiempo 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 propiedad animada tiene su propia pista. Para el salto, el desplazamiento vertical del cuerpo es:

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));

Léelo como una historia: agacharse (+10; recuerda que la y del canvas apunta hacia abajo), impulsarse, subir hasta 160 unidades en el punto más alto, caer, amortiguar el aterrizaje un poco por debajo del nivel del suelo y asentarse. El easing cambia a mitad de camino: easeOutCubic en la subida (despegue rápido que se frena hacia el punto más alto) y easeInCubic en la bajada (inicio lento que se acelera). Es una imitación barata pero convincente de la parábola de la gravedad.

La forma de lanza lleva la misma idea más lejos: 18 puntos de paso (las 17 poses más una clave de cierre igual a la de apertura, para que la forma se repita sin cortes), cada uno describiendo el cuerpo entero —ángulo de la lanza, posición del agarre, inclinación, desplazamiento del peso, flexión, anchura de la postura, elevación y squash—, con un easing opcional por tramo. Dos trucos hacen que se lea bien. Dos puntos de paso idénticos seguidos crean una pausa, el momento congelado de «pose» de una forma de artes marciales. Y las estocadas usan easeOutCubic, de modo que la lanza sale disparada y se frena al llegar a la pose, lo que se percibe como un golpe limpio y potente.

Squash and stretch

El salto también anima una escala vertical, sy: aplastada a 0,84 al agacharse, estirada a 1,14 al despegar y aplastada de nuevo a 0,82 al aterrizar. La escala horizontal se deriva de ella para que el personaje parezca conservar su volumen:

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);

Todas las articulaciones pasan por S() al final de buildPose. Escalar alrededor del punto del suelo que hay bajo el personaje (y no alrededor de su centro) mantiene los pies en el suelo mientras el cuerpo se aplasta, justo lo que dibujaría un animador a mano. El radio de la cabeza se escala con la media de sx y sy.

Movimiento secundario: un muelle y una cadena verlet

El movimiento primario es lo que hace el personaje. El secundario es lo que le sigue: el pelo, la ropa, una cabeza que va un instante por detrás del cuerpo. Es barato de añadir y marca una diferencia enorme. Nuestro monigote tiene dos tipos.

Un muelle amortiguado en la cabeza

La cabeza no va rígida sobre el cuello. Un muelle (rigidez 260, amortiguación 26) tira de ella hacia su posición ideal, así que cuando el cuerpo da un bandazo la cabeza se retrasa y se asienta con un leve rebote. La amortiguación es lo bastante baja como para dejar un pequeño temblor de dibujo animado.

Pelo verlet

El mechón de pelo es una cadena de cuatro puntos separados 9 unidades. La integración de Verlet guarda la posición actual y la anterior de cada punto; la velocidad es simplemente la diferencia. Eso deja una actualización muy corta:

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;

El factor 0,86 es la resistencia del aire. windX es un empuje hacia atrás que crece con la velocidad de carrera (y recibe un impulso extra durante los saltos), y g es la gravedad. Tras mover los puntos, seis iteraciones de una restricción de distancia devuelven cada pareja a exactamente 9 unidades, con el primer punto fijado a la parte trasera de la cabeza. Fijar un punto y aplicar restricciones es todo lo que hace falta: el pelo ondea hacia atrás al correr, cae al caminar y da latigazos al saltar, y nada de eso se animó a mano.

Dibujar extremidades blandas

Un monigote dibujado con líneas rectas parece rígido. El nuestro pasa los tres puntos de cada extremidad (por ejemplo cadera, rodilla y pie) por una spline Catmull-Rom, la densifica en una curva suave y rellena alrededor una cinta que se estrecha —gruesa en la cadera, más fina en el pie— con extremos redondeados. El resultado parece una extremidad de dibujo animado tipo «manguera de goma», que se curva en lugar de girar sobre una bisagra. Las extremidades del lado lejano se dibujan primero, más finas, más oscuras y al 42 % de opacidad, lo que da sensación de profundidad sin nada de 3D.

Por último, el bucle es educado. El dt de cada fotograma se limita a 33 ms para que un tirón no lance el pelo de un lado a otro de la pantalla. La animación se pausa cuando el escenario sale de la vista o la pestaña queda oculta, y si el sistema del visitante pide movimiento reducido arranca en pausa con un único fotograma fijo.

Lo que te recomendamos copiar

  • La IK de dos huesos son diez líneas. Limita la distancia y el coseno y nunca te dará NaN.
  • Usa IK donde hay un objetivo y FK donde hay un ritmo.
  • Velocidad del suelo = zancada × cadencia ÷ factor de apoyo. Acierta con esto y los pies se quedarán pegados al suelo con cualquier tipo de marcha.
  • Las pistas de fotogramas clave con easing por tramo bastan para movimientos únicos expresivos. Duplica una clave para conseguir una pausa.
  • Aplasta alrededor del punto de contacto, no del centro, y deriva la anchura de la altura para conservar el volumen.
  • Un muelle y una cadena verlet de cuatro puntos te dan casi toda la sensación de «está vivo» con muy poco código.

Puedes ver todo esto en movimiento en la página del monigote, que ahora es Stickman Spear Run —corre, salta y ataca con la lanza—, con el laboratorio de movimiento como segundo modo. Si te gustan los monigotes, nuestro próximo juego de puzles de física, Ink Left, también está protagonizado por uno, dibujado en un cuaderno de bocetos sin terminar.