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):
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:
Backward pass — re-anchor root:
Repeat until |p_n − T| < tolerance (typically 0.01 px) or max iterations reached (~10). The algorithm is guaranteed to converge for reachable targets.
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:
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:
- Perform a forward pass on each sub-chain to its target.
- Average the sub-chain roots to find a shared root position.
- Perform a backward pass on the trunk from the shared root down to the actual anchor.
- 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:
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
- Game character animation: IK is used in Unreal Engine and Unity for foot placement on uneven terrain, hand-on-object interactions, and procedural body adjustment.
- Industrial robotics: 6-DoF robot arms (KUKA, ABB, Universal Robots) use analytical IK solutions or numerical methods for precise end-effector positioning.
- Motion capture retargeting: IK maps marker positions onto a character skeleton, ensuring feet stay on the floor and hands reach animated props.
- Surgical robots: IK with strict joint limits guides the Da Vinci surgical system's instruments through narrow trocar ports to precise intra-body targets.
- Prosthetics: IK control strategies drive powered prosthetic arms, interpreting EMG or BCI signals into target positions.