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)
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)
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
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,
];
}
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
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);
}
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;
}
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)
);
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)