Article
Robotics & Kinematics · ⏱ ~15 min read

IK FABRIK: From Scratch in JavaScript

FABRIK — Forward And Backward Reaching Inverse Kinematics — solves the problem of positioning the joints of a robotic arm so that its tip reaches a target. Unlike Jacobian-based methods, FABRIK is purely geometric: it alternates between pulling the chain toward the goal (forward pass) and anchoring the root (backward pass), converging in just a few iterations. We implement the full algorithm including angle constraints and interactive canvas simulation.

1. The Inverse Kinematics Problem

A robotic arm is a chain of rigid links connected by revolute joints. Forward kinematics (FK) is easy: given joint angles θ₁…θₙ, compute the end-effector position. Inverse kinematics (IK) is the inverse: given a desired end-effector position, find joint angles that produce it.

Underdetermined

For a planar 3-joint arm reaching a 2D target, infinitely many angle configurations are valid. The solution is a family, not a unique answer.

Overdetermined

If the target is unreachable (beyond the fully extended arm length), there is no exact solution — the best we can do is minimise the residual error.

Non-linear

Joint positions involve sums of cosines and sines — the system is trigonometric and generally has multiple solution branches.

FABRIK advantage

No matrix inversion, no singularities, fast convergence in ~5 iterations for typical poses. Handles n-link chains and branching structures.

2. Forward Kinematics Recap

For a 2D planar chain with n links of lengths ℓ₁, ℓ₂, …, ℓₙ and cumulative angles α₁, α₂, …, αₙ (each measured from world x-axis):

Joint positions (starting at root p₀): p₀ = (x₀, y₀) (anchor / root) pᵢ = pᵢ₋₁ + ℓᵢ · (cos αᵢ, sin αᵢ) End-effector: p_n = Σᵢ ℓᵢ · (cos αᵢ, sin αᵢ) + p₀

The total reach of the arm is R = Σᵢ ℓᵢ. If the target T satisfies |T − p₀| > R the target is out of reach and the arm should stretch maximally toward it.

3. The FABRIK Algorithm

FABRIK was proposed by Aristidou and Lasenby in 2011. Each iteration consists of two passes:

Forward pass — pull tip to target:

1. Set p_n = T (place tip at target) 2. For i = n-1 down to 0: dir = (pᵢ − pᵢ₊₁) / |pᵢ − pᵢ₊₁| pᵢ = pᵢ₊₁ + dir · ℓᵢ₊₁

Backward pass — re-anchor root:

3. Set p₀ = root_anchor 4. For i = 1 to n: dir = (pᵢ − pᵢ₋₁) / |pᵢ − pᵢ₋₁| pᵢ = pᵢ₋₁ + dir · ℓᵢ

Repeat until |p_n − T| < tolerance (typically 0.01 px) or max iterations reached (~10). The algorithm is guaranteed to converge for reachable targets.

Complexity: Each iteration is O(n) — linear in the number of joints. Compare with Jacobian pseudo-inverse which requires O(n³) per step for the matrix inversion.

4. Joint Angle Constraints

Real joints have limited range of motion. Constraints are applied during each pass by clamping the direction vector to an allowed angular range around the previous bone's direction:

For joint i with constraint [θ_min, θ_max]: 1. Compute current angle of bone i relative to bone i−1 2. Clamp: θ = clamp(θ_current, θ_min, θ_max) 3. Recompute direction using the clamped angle 4. Set pᵢ = pᵢ₋₁ + clampedDir · ℓᵢ

Applying constraints during the backward pass is most stable. Constraints make the problem more like a constrained optimisation — the algorithm may not reach the exact target but settles into the nearest feasible configuration.

5. Branching Chains

A branching arm (e.g. a hand with multiple fingers) has a tree structure. FABRIK extends naturally:

  1. Perform a forward pass on each sub-chain to its target.
  2. Average the sub-chain roots to find a shared root position.
  3. Perform a backward pass on the trunk from the shared root down to the actual anchor.
  4. Repeat until convergence.

The averaging step distributes the tension equally across branches by default, or can be weighted by sub-chain priority (e.g., dominant arm receives more weight).

6. Jacobian Comparison

The classical IK approach uses the Jacobian J of the FK function, mapping joint velocity to end-effector velocity. The update rule is:

Δθ = Jᵀ (J Jᵀ + λ²I)⁻¹ Δe (Damped Least Squares / Levenberg-Marquardt) e = T − p_n (end-effector error) λ = damping factor (avoids singularities)

Jacobian methods converge in fewer iterations for complex goals, handle redundancy better, and can minimise secondary objectives (e.g. avoid obstacles) via null-space projection. But they require O(n³) linear algebra per step and can diverge near singularities. FABRIK is preferred for real-time interactive applications where robustness matters more than optimality.

7. JavaScript — Full FABRIK Implementation

// FABRIK Inverse Kinematics — 2D Canvas demo

class FABRIKChain {
  /**
   * @param {number[]} lengths  - link lengths [l0, l1, ..., l_{n-1}]
   * @param {{x,y}}   anchor   - fixed root position
   */
  constructor(lengths, anchor) {
    this.lengths = lengths;
    this.n = lengths.length;
    // initialise joints vertically
    this.joints = [];
    let y = anchor.y;
    for (let i = 0; i <= this.n; i++) {
      this.joints.push({x: anchor.x, y});
      if (i < this.n) y += lengths[i];
    }
    this.anchor = {x: anchor.x, y: anchor.y};
    this.totalLength = lengths.reduce((a, b) => a + b, 0);
    this.constraints = null; // optional [{min, max}, ...]
  }

  solve(target, maxIter = 10, tol = 0.5) {
    const J = this.joints;
    const L = this.lengths;

    // Check reachability
    const dist = Math.hypot(target.x - this.anchor.x, target.y - this.anchor.y);
    if (dist > this.totalLength) {
      // Stretch fully toward target
      for (let i = 0; i < this.n; i++) {
        const dx = target.x - J[i].x, dy = target.y - J[i].y;
        const r = Math.hypot(dx, dy);
        J[i + 1].x = J[i].x + L[i] * dx / r;
        J[i + 1].y = J[i].y + L[i] * dy / r;
      }
      return;
    }

    for (let iter = 0; iter < maxIter; iter++) {
      // --- Forward pass ---
      J[this.n].x = target.x; J[this.n].y = target.y;
      for (let i = this.n - 1; i >= 0; i--) {
        const dx = J[i].x - J[i + 1].x;
        const dy = J[i].y - J[i + 1].y;
        const r = Math.hypot(dx, dy);
        J[i].x = J[i + 1].x + L[i] * dx / r;
        J[i].y = J[i + 1].y + L[i] * dy / r;
      }

      // --- Backward pass ---
      J[0].x = this.anchor.x; J[0].y = this.anchor.y;
      for (let i = 0; i < this.n; i++) {
        let dx = J[i + 1].x - J[i].x;
        let dy = J[i + 1].y - J[i].y;
        // apply angular constraints if defined
        if (this.constraints && this.constraints[i] && i > 0) {
          const prevDx = J[i].x - J[i - 1].x;
          const prevDy = J[i].y - J[i - 1].y;
          const baseAngle = Math.atan2(prevDy, prevDx);
          let relAngle = Math.atan2(dy, dx) - baseAngle;
          const {min, max} = this.constraints[i];
          relAngle = Math.max(min, Math.min(max, relAngle));
          const absAngle = baseAngle + relAngle;
          dx = Math.cos(absAngle); dy = Math.sin(absAngle);
        }
        const r = Math.hypot(dx, dy);
        J[i + 1].x = J[i].x + L[i] * dx / r;
        J[i + 1].y = J[i].y + L[i] * dy / r;
      }

      const err = Math.hypot(J[this.n].x - target.x, J[this.n].y - target.y);
      if (err < tol) break;
    }
  }

  draw(ctx, accent = '#fb923c') {
    const J = this.joints;
    // Links
    ctx.beginPath();
    ctx.strokeStyle = accent;
    ctx.lineWidth = 4;
    ctx.lineCap = 'round';
    ctx.moveTo(J[0].x, J[0].y);
    for (let i = 1; i < J.length; i++) ctx.lineTo(J[i].x, J[i].y);
    ctx.stroke();
    // Joints
    for (let i = 0; i < J.length; i++) {
      ctx.beginPath();
      ctx.fillStyle = i === 0 ? '#64748b' : i === J.length - 1 ? '#34d399' : accent;
      ctx.arc(J[i].x, J[i].y, i === 0 ? 7 : 5, 0, Math.PI * 2);
      ctx.fill();
    }
  }
}

// Usage: mouse-following arm
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const arm = new FABRIKChain([60, 50, 40, 30], {x: 200, y: 100});
let mouse = {x: 200, y: 300};
canvas.addEventListener('mousemove', e => {
  const r = canvas.getBoundingClientRect();
  mouse = {x: e.clientX - r.left, y: e.clientY - r.top};
});
function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  arm.solve(mouse);
  arm.draw(ctx);
  // draw target
  ctx.beginPath();
  ctx.fillStyle = '#34d399';
  ctx.arc(mouse.x, mouse.y, 6, 0, Math.PI * 2);
  ctx.fill();
  requestAnimationFrame(loop);
}
loop();

8. Applications

Live demo: The Mechanisms simulation uses a similar iterative IK approach to animate multi-link mechanical linkages in real time.
⚙️ Open Mechanisms →