devol.dev

Inverse kinematics, two ways

Forward kinematics is arithmetic. Given the joint angles, the end effector is wherever the trigonometry says it is, and there is exactly one answer.

Inverse kinematics runs the other direction, and it is not arithmetic. Given a point in space, what joint angles put the hand there? For a two-link planar arm the answer is: usually two sets, sometimes one, often none.

Planar manipulator / 2-DOF

Inverse kinematics, two ways

Drag the target. The solver finds both joint configurations that put the end effector there. Change the link lengths and watch the reachable workspace open a hole in the middle.

Theta 1
Theta 2
Target
Reach

The two branches

Drag the target anywhere inside the shaded region and the solver returns both configurations. The solid arm is elbow-down, the ghost is elbow-up. Both put the end effector on exactly the same point. Both are correct.

The math is the law of cosines applied to the triangle formed by the shoulder, the elbow, and the hand. The elbow angle comes out of

cos(θ₂) = (r² − L₁² − L₂²) / (2·L₁·L₂)

and acos returns a magnitude, not a sign. That missing sign is the whole story: ±θ₂ are both valid, and each one implies a different shoulder angle. Two solutions, one target.

Why it matters that there are two

If you are writing the controller, you have to pick one, and the choice is not cosmetic.

The elbow-up configuration sweeps a different volume than elbow-down. Put a shelf above the arm and one branch collides while the other does not. Joint limits may rule out a branch entirely. And you cannot switch branches mid-path without passing through a singularity, which means a real trajectory has to commit to a branch and stay there, or plan an explicit reconfiguration.

Turn on the trace and drag in a slow circle. Watch how far the joints travel for a small movement of the hand near the workspace edge. That ratio is the thing that bites you in practice.

The hole in the middle

Set the two links to different lengths and a dead zone opens around the shoulder. The arm cannot fold tighter than |L₁ − L₂|, so there is a disc of space, right next to the base, that it can never reach.

Now drag the sliders until both links are equal. The hole closes completely. An arm with equal links can touch its own shoulder; an arm with unequal links has a permanent blind spot at close range. That is a real design constraint, and it is visible in about four seconds of dragging.

Singularities

Push the target all the way out until the arm is straight. The ghost and the solid arm converge — the two solutions have merged into one. That is a singularity. The elbow angle is zero, the arm has lost a degree of freedom, and the manipulability measure

w = |L₁ · L₂ · sin(θ₂)|

has gone to zero along with it. Near that boundary, moving the hand outward by a millimetre demands an enormous joint rotation. Controllers that naively invert the Jacobian here produce commanded velocities that no motor can deliver.

The same thing happens at the inner boundary, where the arm is folded back on itself.

The code

The solver is about sixty lines and lives in a module with no rendering dependencies at all — no canvas, no DOM, no three.js. It takes numbers and returns numbers, which means it runs under Node and has a test suite that verifies the important property directly: run IK on a grid of targets, feed every solution back through FK, and check the hand lands where it was asked to. Worst error across roughly a thousand solutions is about 1e-13.

That separation is deliberate. When a 3D version of this arrives, it imports the identical solver and swaps only the renderer.