This document explains how geometric algebra was implemented in Bifourcation from the first useful version to the current version.
The goal was not to make a full geometric algebra library. The goal was narrower:
build just enough 2D geometric algebra
to make Fourier drawing feel like rotation in an oriented plane
instead of rotation in an unexplained imaginary axisThe final implementation uses the 2D Euclidean algebra generated by:
1
e₁
e₂
e₁e₂The central object is the bivector:
B = e₁e₂This bivector represents the oriented drawing plane.
Because:
B² = -1it can do the job that i usually does in complex Fourier analysis, while still having a concrete geometric meaning.
The first version was essentially a minimal multivector representation.
A point on the canvas was represented as a vector:
v = x e₁ + y e₂Internally, a multivector needed four scalar components:
type Multivector = {
scalar: number;
e1: number;
e2: number;
e12: number;
};This was enough to represent:
scalar values
vectors
bivectors
mixed multivectorsThe very first requirement was to support the geometric product. Everything else could be derived from that.
The multiplication rules were:
e₁² = 1
e₂² = 1
e₁e₂ = -e₂e₁
(e₁e₂)² = -1That gave the app the key behavior it needed:
multiplication by e₁e₂ turns a vector by 90° in the drawing planeThe first geometric-algebra layer was therefore deliberately small:
ZERO
addMultivectors
scaleMultivector
geometricProduct
vectorThat was enough to make the Fourier code work without importing a symbolic algebra package.
A general-purpose geometric algebra library would have been more complete, but it would also have hidden the educational point.
The project needed the implementation to be readable. I wanted the code itself to show the basis blades and the product rules.
The trade-off was:
general library:
more complete
less transparent
more dependency weight
small local implementation:
less general
easier to inspect
clearer educational valueThe local implementation won because this app only needs the 2D drawing plane.
Higher-dimensional geometric algebra was intentionally left out. There was no need for e₃, trivectors, conformal models, or general blade operations.
The next step was introducing the even subalgebra.
A 2D vector point starts as:
v = x e₁ + y e₂Multiplying by e₁ maps it into the scalar-plus-bivector plane:
e₁v = e₁(xe₁ + ye₂)
= x e₁² + y e₁e₂
= x + y e₁e₂So the usual complex representation:
x + iybecomes:
x + y e₁e₂This is the key interpretation:
the imaginary unit is replaced by the oriented drawing planeThe even subalgebra used by the project is:
span{1, e₁e₂}This space behaves like the complex numbers, but the second basis element is geometric:
e₁e₂ = the oriented drawing planeThe implementation therefore added helpers for converting between canvas vectors and even multivectors:
vectorToEvenMultivector(v)
evenMultivectorToVector(z)The first helper maps:
x e₁ + y e₂to:
x + y e₁e₂The second maps the scalar-plus-bivector signal back to canvas vector coordinates.
The rotor helper was then added in the even subalgebra layer.
The rotor is:
R(θ) = e^(Bθ)where:
B = e₁e₂Since:
B² = -1the exponential expands to:
e^(Bθ) = cos(θ) + B sin(θ)So in code, the rotor is simple:
rotor(angle) = {
scalar: Math.cos(angle),
e1: 0,
e2: 0,
e12: Math.sin(angle),
}That may look like a complex exponential, but the interpretation is different:
cos θ + i sin θbecomes:
cos θ + e₁e₂ sin θThe plane of rotation is no longer implicit. It is the bivector e₁e₂.
One subtle issue came up later: in geometric algebra, vector rotation is usually written as the sandwich product:
v' = RvR⁻¹with:
R = e^(-Bθ/2)That is correct for the general rotor action on raw vectors.
Bifourcation uses a related but more specialized 2D plane-phase action. The Fourier signal is treated in the scalar-plus-bivector phase plane, and phase advancement is one-sided:
z' = z e^(Bθ)This is why the Fourier phase uses the full angle:
θ = 2πktrather than the half-angle sandwich formula.
The final interpretation is:
general GA vector rotation:
v' = RvR⁻¹
R = e^(-Bθ/2)
Bifourcation's 2D Fourier phase action:
z' = z e^(Bθ)Both are legitimate. The project uses the second because it is a 2D Fourier drawing system.
The next major update was visual. The project originally showed circular Fourier components, but the goal became more explicitly geometric-algebraic.
The question was how to make the bivector visible without misleading users.
The final decision was to support three visual modes:
Disk
Blade
CompanionEach mode explains a different part of the same mathematical structure.
Disk view emphasizes rotational symmetry.
A Fourier term has constant magnitude while its phase changes. Its endpoint traces circular motion. Disk mode therefore represents the current rotor-driven term with an equal-area disk glyph.
If the current component vector has length:
|r|then the associated oriented area magnitude is:
|r|²A disk with radius R has area:
πR²So the radius is chosen as:
R = |r| / √πThe disk is not the rotor itself.
The rotor is the action:
Rₖ(t) = e^(e₁e₂ · 2πkt)The disk is a glyph for the current rotor-driven term.
The arrows on the disk mark phase/orientation. They do not mean the plane e₁e₂ itself is rotating.
Blade view is the most direct oriented-area view.
Given a component vector:
rthe app draws its e₁e₂-rotated companion:
e₁e₂rBecause e₁e₂ is a unit plane element, the companion has the same length and is perpendicular to r.
So the blade spanned by:
r
e₁e₂rappears as a square.
That does not mean bivectors are squares. A bivector is oriented area. The square is a representative of the oriented area generated by that vector and its companion.
This distinction matters. The final wording became:
Blade mode represents each component's oriented area using the square spanned by the component vector and its e₁e₂-rotated companion.Companion view shows the action directly:
r → e₁e₂rThis view avoids filled areas and instead shows the primary vector and its perpendicular companion vector.
It is useful because it makes the role of e₁e₂ explicit:
e₁e₂ acts as a 90° turn in the drawing planePositive and negative frequencies are drawn in the stroke color because they belong to the same stroke.
Opposite orientation is shown with visual texture:
negative blades:
diagonal hatching
negative disks:
diagonal hatching
negative companion vectors:
dashed companion lineArrows show direction of phase/orientation.
This was chosen over using totally different colors because color already carries stroke identity.
Small rotor labels were added to make the algebra visible inside the animation.
The compact label is:
cₖRₖ(t)The expanded meaning is:
cₖe^(e₁e₂ · 2πkt)The labels are intentionally small. They should remind the viewer what is happening without dominating the drawing.
The final interpretation is:
each visible component is a Fourier coefficient advanced by a rotor in the e₁e₂ planeThe final implementation keeps these decisions:
Use a minimal local 2D GA implementation.
Represent canvas points as vectors.
Map vectors into the scalar-plus-bivector even subalgebra for Fourier phase.
Use e₁e₂ as the oriented drawing plane.
Use e^(e₁e₂θ) as the rotor / phase action.
Visualize components with disk, blade, and companion glyphs.
Keep the glyphs honest: they are not separate rotating planes.The final mental model is:
There is one drawing plane: e₁e₂.
There are many Fourier terms advanced in that plane.
The disks, blades, and companions are visual glyphs for the current terms.
The rotor is the action that advances each term through phase.These statements are avoided because they can mislead:
the disk is the rotor
the bivector plane rotates
each term has its own separate plane
bivectors are squaresBetter statements are:
the rotor acts in the e₁e₂ plane
the disk is an equal-area glyph for the current term
the blade is an oriented-area representative
the arrows mark term phase/orientationThat is the final geometric-algebra framing used by Bifourcation.