Bringing Creatures to Life with Procedural Animation

By Prateek2025-02-16



Procedural animation has always felt like magic to me. There’s something captivating about how a series of mathematical equations and algorithms can breathe life into virtual creatures. In this post, I’ll walk you through my journey into procedural animation—from the basic building blocks of chains and constraints to the creation of realistic, organic creatures like snakes, fish, and even lizards. Grab a cup of coffee and settle in for an immersive 10‐minute read as we explore how to transform code into life-like motion




The Magic Behind the Motion

At the core of procedural animation lies the idea that every movement can be computed, rather than manually keyframed. Instead of painstakingly animating each frame, we let mathematics and physics do the heavy lifting. This is especially powerful when creating biological simulations or visualizing virtual animals. One of the simplest yet most elegant techniques involves using chains—a series of connected points or “joints” that follow one another according to defined rules.

Distance Constraints and Anchors

Imagine you have a fixed point in space, called an anchor. Now, suppose you want another point to always remain a fixed distance away from this anchor. You can achieve this by computing the vector from the anchor to the point, then scaling that vector to your desired length. This simple rule is known as a distance constraint. For example, consider the following pseudocode:

  // Compute the vector from the anchor to a moving target.
  let dx = target.x - anchor.x;
  let dy = target.y - anchor.y;
  let distance = Math.sqrt(dx * dx + dy * dy);
  let desiredDistance = 100; // the constant distance we want to maintain

  // Normalize and scale the vector:
  let newX = anchor.x + (dx / distance) * desiredDistance;
  let newY = anchor.y + (dy / distance) * desiredDistance;
    

By continuously applying this constraint, the point will appear to follow the anchor smoothly. When you extend this concept to a series of points, you create a chain where each point follows the one before it. This is the backbone (or spine) of our animated creatures.




Building a Chain

A chain is simply an ordered set of points (joints) connected by segments of fixed length. To ensure smooth movement, we need to update the position of each joint based on its predecessor. Let’s look at an ES6 version of our Chain class, which forms the foundation for our creature’s body:
class Chain {
  constructor(origin, jointCount, linkSize, angleConstraint = Math.PI * 2) {
    this.linkSize = linkSize;
    this.angleConstraint = angleConstraint;
    this.joints = [];
    this.angles = [];
    // Create the first joint using a simple object copy.
    this.joints.push({ x: origin.x, y: origin.y });
    this.angles.push(0);
    for (let i = 1; i < jointCount; i++) {
      const prev = this.joints[i - 1];
      this.joints.push({ x: prev.x, y: prev.y + linkSize });
      this.angles.push(0);
    }
  }

  resolve(pos) {
    // Compute the vector from the first joint to the target position.
    const diff = vecSub(pos, this.joints[0]);
    this.angles[0] = heading(diff);
    this.joints[0] = { x: pos.x, y: pos.y };

    // Update subsequent joints based on the constrained angle.
    for (let i = 1; i < this.joints.length; i++) {
      const sub = vecSub(this.joints[i - 1], this.joints[i]);
      const curAngle = heading(sub);
      this.angles[i] = constrainAngle(curAngle, this.angles[i - 1], this.angleConstraint);
      const angleVec = fromAngle(this.angles[i]);
      const scaled = setMag(angleVec, this.linkSize);
      this.joints[i] = vecSub(this.joints[i - 1], scaled);
    }
  }

  // Additional methods like fabrikResolve() and display() omitted for brevity.
}

In this code, helper functions like vecSub, heading, fromAngle, and setMag let us perform the necessary vector math. These functions ensure that the chain’s joints remain a fixed distance apart while allowing smooth bending.




Parametric Equations: Drawing with Precision

Once we have our chain (or spine), the next step is to create the creature’s body. We achieve this by computing positions along the chain using parametric equations. For example, the parametric equations of a circle allow us to determine the left and right sides of a body segment:
  • x = center.x + radius * cos(angle)
  • y = center.y + radius * sin(angle)

By offsetting the angle by ±90° (or ±π/2 radians), we can compute the boundaries of a segment. These boundaries, when connected, form a continuous outline of the creature. This technique isn’t just for simple shapes; it allows us to draw complex, organic outlines. In our implementations, we use these equations to shape not only the body but also additional features like fins and eyes.




Procedurally Animating a Snake

Let’s start with a snake—arguably the simplest creature to animate with a chain. Our snake’s body is a chain of segments that follows the head as it moves. Here’s a simplified version of our Snake class:
export class Snake {
  constructor(origin) {
    // Create a spine with 48 joints for a long, wiggly body.
    this.spine = new Chain(origin, 48, 64, Math.PI / 8);
  }

  // Move the snake’s head toward a target (e.g., mouse pointer)
  resolve(targetX, targetY) {
    const headPos = this.spine.joints[0];
    const dx = targetX - headPos.x;
    const dy = targetY - headPos.y;
    const d = Math.sqrt(dx * dx + dy * dy);
    const mag = 8;
    let vx = 0, vy = 0;
    if (d > 0) {
      vx = (dx / d) * mag;
      vy = (dy / d) * mag;
    }
    const targetPos = { x: headPos.x + vx, y: headPos.y + vy };
    this.spine.resolve(targetPos);
  }

  // Draw the snake with its body and eyes using Canvas 2D API.
  display(ctx) {
    ctx.lineWidth = 4;
    ctx.strokeStyle = 'white';
    ctx.fillStyle = 'rgb(172, 57, 49)';

    ctx.beginPath();
    // Right side of the snake's body
    for (let i = 0; i < this.spine.joints.length; i++) {
      const x = this.getPosX(i, Math.PI / 2, 0);
      const y = this.getPosY(i, Math.PI / 2, 0);
      if (i === 0) ctx.moveTo(x, y);
      else ctx.lineTo(x, y);
    }
    // Extra vertex at the tail end
    ctx.lineTo(this.getPosX(47, Math.PI, 0), this.getPosY(47, Math.PI, 0));

    // Left side of the snake's body
    for (let i = this.spine.joints.length - 1; i >= 0; i--) {
      ctx.lineTo(this.getPosX(i, -Math.PI / 2, 0), this.getPosY(i, -Math.PI / 2, 0));
    }

    // Complete the head shape with overlapping curves for smoothness.
    ctx.lineTo(this.getPosX(0, -Math.PI / 6, 0), this.getPosY(0, -Math.PI / 6, 0));
    ctx.lineTo(this.getPosX(0, 0, 0), this.getPosY(0, 0, 0));
    ctx.lineTo(this.getPosX(0, Math.PI / 6, 0), this.getPosY(0, Math.PI / 6, 0));
    ctx.lineTo(this.getPosX(0, Math.PI / 2, 0), this.getPosY(0, Math.PI / 2, 0));
    ctx.lineTo(this.getPosX(1, Math.PI / 2, 0), this.getPosY(1, Math.PI / 2, 0));
    ctx.lineTo(this.getPosX(2, Math.PI / 2, 0), this.getPosY(2, Math.PI / 2, 0));

    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Draw eyes
    ctx.fillStyle = 'white';
    ctx.beginPath();
    ctx.arc(this.getPosX(0, Math.PI / 2, -18), this.getPosY(0, Math.PI / 2, -18), 12, 0, 2 * Math.PI);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(this.getPosX(0, -Math.PI / 2, -18), this.getPosY(0, -Math.PI / 2, -18), 12, 0, 2 * Math.PI);
    ctx.fill();
  }

  bodyWidth(i) {
    if (i === 0) return 76;
    if (i === 1) return 80;
    return 64 - i;
  }

  getPosX(i, angleOffset, lengthOffset) {
    const joint = this.spine.joints[i];
    const angle = this.spine.angles[i];
    return joint.x + Math.cos(angle + angleOffset) * (this.bodyWidth(i) + lengthOffset);
  }

  getPosY(i, angleOffset, lengthOffset) {
    const joint = this.spine.joints[i];
    const angle = this.spine.angles[i];
    return joint.y + Math.sin(angle + angleOffset) * (this.bodyWidth(i) + lengthOffset);
  }
}

Notice how we update the spine by moving the head toward the target, then have the rest of the body follow. The getPosX and getPosY functions use a body width array to give the snake a tapered, natural appearance.




Diving Deeper: Creating a Fish with Fins

A fish is a more complex creature because it not only has a body but also several fins that respond to the motion of its spine. The fish’s body is defined by a chain, and we use parametric equations to add fins and shape the body.
The Fish Class Explained
Below is our ES6 version of the Fish class:
export class Fish {
  constructor(origin) {
    // Create a spine with 12 segments (10 for body, 2 for tail)
    this.spine = new Chain(origin, 12, 64, Math.PI / 8);
    this.bodyColor = "rgb(58,124,165)";
    this.finColor = "rgb(129,195,215)";
    this.bodyWidth = [68, 81, 84, 83, 77, 64, 51, 38, 32, 19];
  }

  resolve(targetX, targetY) {
    const headPos = this.spine.joints[0];
    const dx = targetX - headPos.x;
    const dy = targetY - headPos.y;
    const d = Math.sqrt(dx * dx + dy * dy);
    const mag = 16;
    let vx = 0, vy = 0;
    if (d > 0) {
      vx = (dx / d) * mag;
      vy = (dy / d) * mag;
    }
    const targetPos = { x: headPos.x + vx, y: headPos.y + vy };
    this.spine.resolve(targetPos);
  }

  display(ctx) {
    // Aliases for brevity
    const j = this.spine.joints;
    const a = this.spine.angles;

    // Helper for relative angle differences.
    const relativeAngleDiff = (a1, a2) => {
      let diff = a1 - a2;
      diff = ((diff + Math.PI) % (2 * Math.PI)) - Math.PI;
      return diff;
    };

    // Compute relative angles for fin positioning.
    const headToMid1 = relativeAngleDiff(a[0], a[6]);
    const headToMid2 = relativeAngleDiff(a[0], a[7]);
    const headToTail = headToMid1 + relativeAngleDiff(a[6], a[11]);

    // Draw pectoral fins using rotated ellipses.
    ctx.lineWidth = 4;
    ctx.strokeStyle = "white";
    ctx.fillStyle = this.finColor;

    // Right pectoral fin
    ctx.save();
    ctx.translate(this.getPosX(3, Math.PI / 3, 0), this.getPosY(3, Math.PI / 3, 0));
    ctx.rotate(a[2] - Math.PI / 4);
    ctx.beginPath();
    ctx.ellipse(0, 0, 80, 32, 0, 0, 2 * Math.PI);
    ctx.fill();
    ctx.restore();

    // Left pectoral fin
    ctx.save();
    ctx.translate(this.getPosX(3, -Math.PI / 3, 0), this.getPosY(3, -Math.PI / 3, 0));
    ctx.rotate(a[2] + Math.PI / 4);
    ctx.beginPath();
    ctx.ellipse(0, 0, 80, 32, 0, 0, 2 * Math.PI);
    ctx.fill();
    ctx.restore();

    // Ventral fins follow a similar approach.
    // [Ventral fin code omitted for brevity; similar to pectoral fins with adjusted offsets.]

    // Tail fins (caudal) are drawn using a combination of points computed along the tail segments.
    let tailPoints = [];
    for (let i = 8; i < 12; i++) {
      const tailWidth = 1.5 * headToTail * Math.pow(i - 8, 2);
      tailPoints.push({
        x: j[i].x + Math.cos(a[i] - Math.PI / 2) * tailWidth,
        y: j[i].y + Math.sin(a[i] - Math.PI / 2) * tailWidth
      });
    }
    const tailWidthConst = Math.max(-13, Math.min(13, headToTail * 6));
    for (let i = 11; i >= 8; i--) {
      tailPoints.push({
        x: j[i].x + Math.cos(a[i] + Math.PI / 2) * tailWidthConst,
        y: j[i].y + Math.sin(a[i] + Math.PI / 2) * tailWidthConst
      });
    }
    ctx.beginPath();
    if (tailPoints.length) {
      ctx.moveTo(tailPoints[0].x, tailPoints[0].y);
      tailPoints.forEach(pt => ctx.lineTo(pt.x, pt.y));
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
    }

    // Draw the fish's body by connecting computed boundary points.
    let bodyPoints = [];
    for (let i = 0; i < 10; i++) {
      bodyPoints.push({ x: this.getPosX(i, Math.PI / 2, 0), y: this.getPosY(i, Math.PI / 2, 0) });
    }
    bodyPoints.push({ x: this.getPosX(9, Math.PI, 0), y: this.getPosY(9, Math.PI, 0) });
    for (let i = 9; i >= 0; i--) {
      bodyPoints.push({ x: this.getPosX(i, -Math.PI / 2, 0), y: this.getPosY(i, -Math.PI / 2, 0) });
    }
    bodyPoints.push({ x: this.getPosX(0, -Math.PI / 6, 0), y: this.getPosY(0, -Math.PI / 6, 0) });
    bodyPoints.push({ x: this.getPosX(0, 0, 4), y: this.getPosY(0, 0, 4) });
    bodyPoints.push({ x: this.getPosX(0, Math.PI / 6, 0), y: this.getPosY(0, Math.PI / 6, 0) });
    // Extra smoothing vertices...
    ctx.beginPath();
    if (bodyPoints.length) {
      ctx.moveTo(bodyPoints[0].x, bodyPoints[0].y);
      bodyPoints.forEach(pt => ctx.lineTo(pt.x, pt.y));
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
    }

    // Finally, draw the dorsal fin and eyes.
    // [Dorsal fin and eye drawing code omitted for brevity—follows similar techniques as above.]
  }

  getPosX(i, angleOffset, lengthOffset) {
    const joint = this.spine.joints[i];
    const angle = this.spine.angles[i];
    const bw = i < this.bodyWidth.length ? this.bodyWidth[i] : 0;
    return joint.x + Math.cos(angle + angleOffset) * (bw + lengthOffset);
  }

  getPosY(i, angleOffset, lengthOffset) {
    const joint = this.spine.joints[i];
    const angle = this.spine.angles[i];
    const bw = i < this.bodyWidth.length ? this.bodyWidth[i] : 0;
    return joint.y + Math.sin(angle + angleOffset) * (bw + lengthOffset);
  }
}


In this fish example, we see how every fin, curve, and detail is computed in real time. The use of parametric equations to calculate offsets and the relative angle differences ensures that the fins react naturally to the curvature of the fish’s body.




Wrapping It All Up

In this blog, we’ve taken a deep dive into procedural animation techniques, starting with the simple idea of distance constraints and extending it to full creature animation. We’ve looked at:


  • Distance Constraints and Anchors: How to ensure a point follows a moving anchor.
  • Chains and Spines: Building a chain of joints that form the backbone of our creature.
  • Parametric Equations: Using math to define the outlines of bodies, fins, and other features.
  • Procedural Animation in Action: How these concepts come together in classes like Snake and Fish.
  • Inverse Kinematics: (In our next posts, we’ll explore leg animations using FABRIK to bring creatures like lizards to life.)


By interleaving code with detailed explanations, I hope you now have a clear picture of how procedural animation works. The techniques discussed here provide a powerful toolbox for creating lifelike, dynamic characters purely through code. Whether you’re an animator, a developer, or just someone who appreciates the blend of art and mathematics, procedural animation offers endless creative possibilities. Happy coding, and may your virtual creatures dance with life!




Feel free to explore the interactive simulation below and experiment with spawning snakes, fish, and lizards. Every time you adjust parameters, you’re witnessing math in motion—true procedural magic.