Skip to content

MSulSal/bifourcation

Repository files navigation

Bifourcation

Draw a path. Watch Fourier terms rebuild it through rotors in the oriented drawing plane e₁e₂. Listen to the drawing play itself.

Bifourcation showing a Fourier reconstruction through oriented-plane rotor views

Bifourcation is an interactive Fourier drawing app built around geometric algebra. It treats the drawing surface as a real oriented plane generated by e₁ and e₂, with e₁e₂ as the bivector that generates rotation.

The central idea is:

Fourier terms advanced by rotors in the e₁e₂ plane.

What it does

  • Draw freehand strokes on a canvas.
  • Add geometric shape strokes with placement, scaling, and rotation.
  • Import images and trace their visible edges into drawable strokes.
  • Undo and redo drawing actions.
  • Choose stroke color and pen width.
  • Convert every stroke into an evenly sampled path.
  • Compute Fourier terms for each stroke.
  • Animate strokes as rotor-driven Fourier reconstructions.
  • Choose between Sequential and Together animation modes.
  • Use path-length-based animation timing so equal traced distance takes equal time.
  • Preserve completed stroke reconstructions while later strokes animate.
  • Visualize Fourier components as rotor-driven disks, oriented blades, or companion vectors.
  • Show tiny rotor labels so the animation keeps pointing back to the math.
  • Generate optional path-driven sound from the drawing.
  • Sync sound timing with the selected animation mode.
  • Persist lightweight user preferences across refreshes.
  • Export or share snapshots of the current reconstruction.

Why this exists

Most Fourier drawing demos use complex numbers. That is mathematically valid, but it can make rotation feel like it happens in a mysterious “imaginary” place.

Bifourcation uses geometric algebra instead.

The drawing plane has basis vectors:

e₁, e₂

Their geometric product is the unit bivector:

B = e₁e₂

This bivector is the oriented drawing plane.

Because:

B² = (e₁e₂)(e₁e₂) = -1

the bivector B can play the role that i plays in complex Fourier analysis. But unlike i, it has direct geometric meaning:

B = e₁e₂ = the oriented plane of rotation

So instead of introducing an unexplained imaginary axis, Bifourcation uses the plane that was already there.

Plane-phase rotation

In geometric algebra, the standard general-purpose rotor formula for rotating a vector is the sandwich product:

v' = RvR⁻¹

with a half-angle rotor:

R = e^(-Bθ/2)

That is the general rotor action used across dimensions.

Bifourcation works in a specific 2D drawing plane. In this plane, the unit bivector:

B = e₁e₂

acts like the plane’s intrinsic 90° turn. One-sided multiplication by:

e^(Bθ)

is a valid plane-phase rotation in this 2D setting.

So the project uses the 2D phase action:

v' = v e^(Bθ)

or equivalently, after identifying the drawing signal with the scalar-plus-bivector plane:

z' = z e^(Bθ)

This is why the Fourier phase uses the full angle θ, not the sandwich rotor half-angle.

The important distinction is:

General GA vector rotation:
  v' = RvR⁻¹
  R = e^(-Bθ/2)

2D plane-phase Fourier rotation:
  v' = v e^(Bθ)
  or
  z' = z e^(Bθ)

Both are legitimate geometric algebra operations. Bifourcation uses the second one because the app is a 2D Fourier drawing system.

The rotor exponential

The cleanest rotor statement is:

R(θ) = e^(Bθ)

where:

B = e₁e₂

is the oriented plane of rotation.

The exponential packages:

plane + angle

into a finite rotation operator.

Because:

B² = -1

the exponential expands into:

e^(Bθ) = cos(θ) + B sin(θ)

But the exponential is deeper than “cos plus sine.” It says that rotation is the accumulated effect of infinitesimal turning in a plane.

A non-trig way to read it is:

e^(Bθ) = limₙ→∞ (1 + Bθ/n)ⁿ

Each tiny factor:

1 + Bθ/n

means:

keep almost all of the current direction
+ add a tiny turn generated by the plane B

Doing that infinitely many times gives a smooth finite rotor.

So:

B
  = oriented plane / infinitesimal 90° turn

Bθ
  = plane-turn generator scaled by angle

e^(Bθ)
  = finite rotor produced by accumulating those tiny turns

For Bifourcation:

Rₖ(t) = e^(e₁e₂ · 2πkt)

which expands to:

Rₖ(t) = cos(2πkt) + e₁e₂ sin(2πkt)

Rotor-first Fourier model

Each Fourier term is a coefficient advanced by a rotor in the e₁e₂ plane.

The rotor for frequency k is:

Rₖ(t) = e^(e₁e₂ · 2πkt)

Each term is:

termₖ(t) = cₖRₖ(t)

and the reconstruction is:

f(t) = Σ cₖRₖ(t)

or, written as the exponential directly:

f(t) = Σ cₖe^(e₁e₂ · 2πkt)

where:

cₖ      = Fourier coefficient
Rₖ(t)   = rotor / phase action in the e₁e₂ plane
k       = rotation frequency and orientation
e₁e₂    = oriented drawing plane

In plain language:

Take the drawing.
Break it into Fourier coefficients.
For each coefficient:
  rotate it in the e₁e₂ plane,
  at its own frequency,
  by its own phase.
Add all of those rotating terms together.
The drawing reappears.

The circles you see are the visible traces of rotor-driven phase in the oriented e₁e₂ plane.

Even subalgebra interpretation

A drawing point can be written as a real vector:

v = x e₁ + y e₂

Multiplying by e₁ maps it into the even subalgebra:

e₁v = e₁(xe₁ + ye₂)
    = x e₁² + y e₁e₂
    = x + y e₁e₂

So the familiar complex representation:

x + iy

becomes:

x + y e₁e₂

The even subalgebra:

span{1, e₁e₂}

is isomorphic to the complex numbers, but the “imaginary” direction is now explained geometrically:

e₁e₂ = the oriented drawing plane

Bifourcation does not use the even subalgebra because vectors cannot be rotated directly. It uses this scalar-plus-bivector phase space because Fourier coefficients naturally want complex-like phase behavior, and geometric algebra lets that phase be interpreted as the drawing plane itself.

Drawing inputs

Every source of geometry becomes the same internal object:

type Stroke = {
	color: string;
	width: number;
	points: Point[];
};

That decision keeps the app coherent.

freehand drawing
shape placement
image tracing
→ Stroke[]
→ resampled path
→ Fourier terms
→ rotor animation

The Fourier system does not need to know where a stroke came from.

Freehand strokes

Freehand drawing records ordered pointer positions directly on the canvas. Each stroke keeps its own color and width, and those values are used again during animation.

Shape tools

The gear drawer includes shape tools.

The current shape tools are:

line
circle
rectangle
triangle
star
spiral
sine wave

A shape tool is not inserted at a fixed default location. Instead:

select a shape
press on the canvas to anchor it
drag to scale and rotate it
release to finalize it

Once finalized, the shape becomes a normal stroke. It enters the same Fourier pipeline as hand-drawn strokes and image-traced contours.

Image tracing

Bifourcation can import an image and turn its visible edges into strokes.

This is not AI image recognition. It does not try to understand the image semantically. It uses a classic computer-vision pipeline entirely in the browser.

The high-contrast image pipeline is:

uploaded image
→ hidden canvas
→ grayscale pixels
→ light blur
→ threshold / foreground mask
→ boundary extraction
→ contour tracing
→ simplified paths
→ Stroke[]
→ Fourier / rotor reconstruction

Sobel-style edge detection remains useful as a fallback, but for high-contrast logos, icons, and silhouettes, threshold-based foreground boundary extraction usually gives cleaner contours.

After import, hand-drawn strokes, shape strokes, and image-traced strokes go through the same pipeline.

Best image types

Image tracing works best with:

  • icons
  • logos
  • sketches
  • line art
  • high-contrast images
  • simple photos with clear silhouettes

It works less well with:

  • low-contrast photos
  • noisy images
  • dense textures
  • images where the subject blends into the background

The goal is not perfect photo vectorization. The goal is to turn an image into a drawable set of paths that can be decomposed through the same Fourier/geometric-algebra system.

History

Bifourcation includes undo and redo for committed drawing actions.

History applies to:

freehand strokes
shape strokes
image imports
clear canvas

Keyboard shortcuts:

Cmd/Ctrl+Z
  undo

Cmd/Ctrl+Shift+Z
  redo

Cmd/Ctrl+Y
  redo

Undo and redo reset animation-side state so stale Fourier traces do not remain visible after the underlying drawing has changed.

Animation modes

Bifourcation has two animation modes.

Sequential

Sequential mode traces strokes in drawing order.

stroke 1 animates
stroke 1 remains as a Fourier trace
stroke 2 animates
stroke 1 and stroke 2 remain as Fourier traces
stroke 3 animates
...

Completed strokes are not replaced by raw input strokes. They remain as the Fourier-drawn traces that were actually generated during animation.

Together

Together mode starts every stroke at the same time.

all strokes begin together
short strokes finish first
long strokes continue
the loop finishes when the longest stroke finishes

This mode is useful for seeing the entire drawing emerge as one synchronized Fourier system instead of as a stroke-by-stroke reconstruction.

Length-based animation timing

Animation timing is based on path length.

The rule is:

same traced distance → same time

So the app does not force every stroke to take the same amount of time.

Instead:

short stroke → shorter duration
long stroke  → longer duration

In Sequential mode, each stroke gets a duration proportional to its own path length.

In Together mode, each stroke still uses its own path-length-based duration, but all strokes start at the same time. The full Together loop lasts as long as the longest stroke.

This makes the animation feel more like drawing speed than like a fixed slideshow of strokes.

Visual modes

During animation, Bifourcation can show each rotor-driven Fourier component in three geometrically consistent ways.

Disk mode

Disk mode is the most natural Fourier-symmetric view.

A Fourier term has constant magnitude while its phase rotates. Its endpoint traces circular motion. Disk mode leans into that rotational symmetry by representing the current rotor-driven term as an equal-area oriented disk glyph.

If the component vector has length:

|r|

then the associated oriented area represented by r and its e₁e₂ companion has magnitude:

|r|²

Disk mode chooses radius R so that:

πR² = |r|²

therefore:

R = |r| / √π

This keeps the circular view honest: the disk is an equal-area glyph for the current term.

The disk is not the rotor itself. The rotor is:

Rₖ(t) = e^(e₁e₂ · 2πkt)

The disk is a visual glyph for the current rotor-driven term. The arrows mark term phase/orientation; they do not mean the plane e₁e₂ itself is rotating.

Blade mode

Blade mode is the most direct geometric-algebra view.

Given the current rotor-driven component vector:

r

the app draws its e₁e₂-rotated companion:

e₁e₂r

Because e₁e₂ is a unit plane element, the companion has the same length:

|e₁e₂r| = |r|

and it is perpendicular to r.

So the blade spanned by r and e₁e₂r appears as a square:

area = |r|²

This does not mean bivectors are inherently squares. A bivector is oriented area. The square is a convenient representative of the oriented area generated by a component vector and its e₁e₂ companion.

Companion mode

Companion mode shows the e₁e₂ action directly:

r → e₁e₂r

It keeps the primary component vector visible and draws its equal-length, 90°-rotated companion vector.

This mode is useful when you want to see e₁e₂ acting as a plane rotation instead of looking at filled area.

Translation component

The frequency-0 term is treated separately as translation / offset. It is not drawn as a rotating bivector blade because it does not represent a rotating phase component.

Orientation cues

Positive and negative Fourier frequencies are drawn in the stroke’s color, because they belong to the same stroke. Their opposite orientations are distinguished by direction arrows and secondary texture cues.

  • Negative blades use subtle diagonal hatching.
  • Negative disks use subtle diagonal hatching.
  • Negative companion vectors use dashed companion lines.
  • Tiny labels identify early rotor terms as cₖRₖ(t).

The three visual modes emphasize different truths:

Disk:
  shows the rotational symmetry of the Fourier phase action.

Blade:
  shows the oriented area associated with the current term.

Companion:
  shows e₁e₂ acting as a 90° rotation.

What the glyphs are and are not

There is only one bivector plane in this 2D app:

e₁e₂

All disk, blade, and companion glyphs represent quantities associated with that same oriented plane.

The glyphs are visual bookkeeping for the Fourier reconstruction chain. They are not separate little physical planes floating in space.

Correct interpretation:

There is one plane: e₁e₂.
There are many Fourier terms advanced in that plane.
The disks/blades/companions are glyphs for the current rotor-driven terms.

Avoid this interpretation:

Each disk is a separate rotating plane.
The disk is the rotor.
The bivector plane itself rotates.

Better wording:

The rotor acts in the e₁e₂ plane.
The disk/blade is a glyph for the current rotor-driven term.
The arrows mark term phase/orientation.

Rotor labels

During animation, Bifourcation labels the first few visible components with compact rotor notation:

cₖRₖ(t)

These labels are intentionally small. They are not meant to dominate the drawing. They are there to keep the viewer anchored to the idea that each visible component is:

a Fourier coefficient being advanced by a rotor

An equivalent expanded label would be:

cₖe^(e₁e₂ · 2πkt)

Sound

Bifourcation includes an optional Web Audio sound engine.

The sound is not intended to be a literal raw playback of Fourier coefficients. Raw sonification tends to become harsh, buzzy, or disconnected from the drawing. Instead, Bifourcation uses the drawing path and Fourier term data as musical control material.

The sound model is:

path position → pitch contour / stereo position
path tangent → melodic motion
path curvature → density / sparkle
Fourier amplitude → loudness
Fourier frequency sign → subtle stereo/orientation cue
current view → instrument character
animation mode → timing behavior

Sound only plays while animation is active. Pausing animation stops the sound. Clearing the canvas stops and resets sound.

The sound system follows the same animation timing idea as the visual reconstruction:

Sequential:
  samples the active stroke according to its path-length-based duration

Together:
  samples multiple active strokes from the shared Together-mode clock
  caps the number of simultaneous voices so the result stays musical

The current sound system is intentionally lightweight and browser-native. It uses the Web Audio API and does not depend on external audio files or libraries.

Persistence

Bifourcation saves lightweight user preferences locally in the browser.

Currently persisted:

  • pen color
  • pen width
  • rotor/bivector view
  • animation trace mode
  • sound on/off preference

Drawing data itself is not persisted. Refreshing the page keeps the user’s preferred tools, but it does not restore the canvas drawing.

Sound preference is remembered, but browser autoplay rules still apply. The app may need a user gesture before audio can actually start.

Tech stack

  • Vite
  • React
  • TypeScript
  • Tailwind CSS
  • HTML Canvas
  • Web Audio API
  • Browser localStorage
  • pnpm

No backend is required.

Getting started

Install dependencies:

pnpm install

Start the dev server:

pnpm dev

Build for production:

pnpm build

Preview the production build:

pnpm preview

Package manager note

This project uses pnpm.

Keep:

pnpm-lock.yaml

Do not commit:

package-lock.json
yarn.lock

A mixed lockfile setup can confuse deployment platforms and cause the wrong package manager to be inferred.

Project structure

src/
  audio/
    soundEngine.ts

  components/
    DrawingCanvas.tsx
    RotorCanvas.tsx
    ShapePlacementCanvas.tsx

  image/
    edgeTracing.ts

  math/
    clifford.ts
    evenSubalgebra.ts
    fourier.ts
    path.ts
    shapes.ts

  types/
    geometry.ts

  App.tsx
  main.tsx

public/
  bifourcation_logo.png
  bifourcation-snapshot.png

Math pipeline

The app’s core data flow is:

raw pointer stroke
or placed shape stroke
or traced image contour
→ ordered path points
→ evenly resampled path
→ vector / even-subalgebra signal
→ Fourier terms
→ rotor-driven reconstruction

1. Raw stroke data

Each stroke stores:

type Stroke = {
	color: string;
	width: number;
	points: Point[];
};

The visual stroke and the mathematical stroke are the same object. This lets the reconstruction inherit the original stroke color and width.

2. Resampling

Pointer events, shape generators, and traced contours can have uneven spacing.

src/math/path.ts converts raw points into evenly spaced points along the path’s arc length.

This matters because Fourier reconstruction expects a clean ordered signal.

3. Geometric algebra conversion

A point is first interpreted as a vector:

x e₁ + y e₂

Then it is mapped into the even subalgebra:

x + y e₁e₂

This is the geometric-algebra version of the usual complex signal.

4. Fourier transform

The app computes a direct DFT over the even-subalgebra signal.

Each term stores:

type FourierTerm = {
	frequency: number;
	coefficient: Multivector;
	amplitude: number;
	phase: number;
};

The terms are sorted by amplitude so the largest broad components appear first in the rotor chain.

5. Rotor evaluation

At animation progress t, each term is evaluated as:

cₖRₖ(t)

where:

Rₖ(t) = e^(e₁e₂ · 2πkt)

The terms are added head-to-tail. The endpoint of the chain is the reconstructed drawing point.

6. Open-stroke seam handling

A normal DFT is periodic, so it assumes the last sample connects back to the first sample.

That can create an artificial seam for open strokes.

Bifourcation keeps the classic periodic DFT because that preserves the rotor-chain identity of the app. But visually, it avoids the final artificial return interval.

The visible animation treats the stroke interval as:

0 → (N - 1) / N

rather than:

0 → 1

This does not turn the DFT into a non-periodic open-curve transform. It simply avoids intentionally drawing the final implied return-to-start segment.

UI overview

Primary bottom controls:

Draw
Animate / Pause
Clear canvas

Floating canvas controls:

gear drawer
sound toggle
snapshot menu
animation view selector

The gear drawer contains:

history controls
animation mode selector
shape tools
color palette
pen width
image import
rotor-plane explanation

The gear icon is used because the drawer is now a general settings and tool surface, not just a color drawer.

Snapshot behavior

The snapshot button captures the current canvas layers into a PNG.

If animation is playing, the app captures the current frame and then pauses.

Snapshot options:

Share snapshot
Download PNG

The share option uses the browser share sheet when available and falls back to PNG download when sharing files is not supported.

Accuracy notes

Bifourcation is intentionally small and local.

It does not claim to be:

a full geometric algebra library
a full vectorization tool
a mathematically pure open-curve Fourier system
a literal audio rendering of Fourier coefficients

It is a focused interactive model:

2D geometric algebra
direct DFT
rotor-first Fourier visualization
browser-native drawing, tracing, sound, and snapshots

Final mental model

There is one drawing plane: e₁e₂.
A stroke becomes an ordered signal in that plane.
The DFT decomposes that signal into coefficients.
Each coefficient is advanced by a rotor.
The rotating terms are added head-to-tail.
The endpoint is the drawing.
Completed endpoints are cached as Fourier traces.
The final picture appears by superposition.