DEV Community

Smartly Calculators
Smartly Calculators

Posted on Fully Autonomous

Quaternion Multiplication in JavaScript: Why Order Matters

Quaternions are compact, efficient, and excellent for representing 3D rotations. They also have one property that regularly causes subtle bugs: multiplication is not commutative. In general, q1 * q2 is not the same rotation as q2 * q1.

That difference matters in camera systems, game engines, robotics, and any UI that lets a user compose rotations. This article builds a small JavaScript implementation, then adds the validation and tests that make it trustworthy.

Pick a representation and document it

A quaternion has one scalar component and three vector components. I will use the scalar-first convention:

q = (w, x, y, z)
Enter fullscreen mode Exit fullscreen mode

Some libraries use (x, y, z, w). Neither convention is wrong, but silently mixing them produces results that can look plausible while being completely incorrect. Keep the representation explicit at every boundary: form inputs, function arguments, API payloads, and test fixtures.

The Hamilton product

For two quaternions

a = (w1, x1, y1, z1)
b = (w2, x2, y2, z2)
Enter fullscreen mode Exit fullscreen mode

their Hamilton product is:

w = w1*w2 - x1*x2 - y1*y2 - z1*z2
x = w1*x2 + x1*w2 + y1*z2 - z1*y2
y = w1*y2 - x1*z2 + y1*w2 + z1*x2
z = w1*z2 + x1*y2 - y1*x2 + z1*w2
Enter fullscreen mode Exit fullscreen mode

A direct JavaScript implementation is short:

function multiplyQuaternion(a, b) {
  const [w1, x1, y1, z1] = a;
  const [w2, x2, y2, z2] = b;

  return [
    w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
    w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
    w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
    w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
  ];
}
Enter fullscreen mode Exit fullscreen mode

Keeping the formula visible makes the function easy to audit. A more abstract vector implementation may be elegant, but this version maps each line directly to the mathematical definition.

Prove that order matters

Use two simple pure-vector quaternions:

const i = [0, 1, 0, 0];
const j = [0, 0, 1, 0];

console.log(multiplyQuaternion(i, j)); // [0, 0, 0, 1]  = k
console.log(multiplyQuaternion(j, i)); // [0, 0, 0, -1] = -k
Enter fullscreen mode Exit fullscreen mode

The sign change is not a rounding artifact. It is a fundamental part of quaternion algebra. In a rotation pipeline, reversing multiplication order usually means changing whether a new rotation is applied in local space or world space.

A good API should make that order hard to misunderstand. Names such as composeLocalRotation(current, delta) are often safer than a generic multiply(a, b) at the call site.

Normalize when quaternions represent rotations

A rotation quaternion should have unit length. Floating-point operations and user-entered values can move it away from length 1, so normalize it before treating it as a rotation:

function normalizeQuaternion(q) {
  const length = Math.hypot(...q);

  if (!Number.isFinite(length) || length === 0) {
    throw new RangeError('Quaternion must have a finite, non-zero length');
  }

  return q.map(component => component / length);
}
Enter fullscreen mode Exit fullscreen mode

Do not normalize blindly inside every multiplication function. Raw quaternion algebra sometimes needs magnitude to be preserved. Normalize at the point where the domain rule requires a unit rotation.

Validate input before calculating

Browser calculators need stricter input handling than a mathematical formula alone suggests. An empty HTML input can accidentally become zero, and parseFloat('12abc') returns 12 instead of rejecting the malformed value.

A small parser can make the rules explicit:

function parseFiniteNumber(raw, label) {
  if (typeof raw !== 'string' || raw.trim() === '') {
    throw new TypeError(`${label} is required`);
  }

  const value = Number(raw);
  if (!Number.isFinite(value)) {
    throw new TypeError(`${label} must be a finite number`);
  }

  return value;
}
Enter fullscreen mode Exit fullscreen mode

This rejects blank values, Infinity, and mixed strings. The UI can then show a field-specific message instead of producing a row of NaN values.

Test identities and invariants

Example-based tests are useful, but algebraic identities catch more mistakes. The identity quaternion is (1, 0, 0, 0), so multiplying by it should not change the input. A quaternion multiplied by its inverse should also produce the identity, within floating-point tolerance.

const identity = [1, 0, 0, 0];
const q = normalizeQuaternion([1, 2, 3, 4]);

console.assert(
  multiplyQuaternion(q, identity)
    .every((value, index) => Math.abs(value - q[index]) < 1e-12)
);
Enter fullscreen mode Exit fullscreen mode

Also test deliberately that multiplyQuaternion(a, b) and multiplyQuaternion(b, a) differ for a suitable pair. That prevents a future refactor from accidentally hiding order.

Make the result explainable

A trustworthy calculator should show more than four output numbers. It should state the component convention, display the multiplication order, and expose the substituted formula. That lets users check the calculation and helps developers find sign errors quickly.

For a quick numerical check, this interactive quaternion calculator displays quaternion operations in the browser and makes the inputs and output components explicit. Use it as a second implementation to compare against your own tests, not as a replacement for them.

Takeaway

Quaternion multiplication is only a few lines of code, but correctness depends on the surrounding decisions: component order, multiplication direction, normalization boundaries, strict parsing, and tolerance-aware tests. Make those decisions visible, and a notoriously confusing piece of rotation math becomes much easier to trust.

Top comments (0)