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 objects with placement, scaling, and rotation.
  • Flood-fill bounded regions on the canvas.
  • Import images and trace their visible edges into drawable strokes.
  • Undo and redo committed actions.
  • Edit selected objects with copy, paste, duplicate, flip, and delete actions.
  • Choose stroke color and stroke width.
  • Convert drawable geometry into evenly sampled paths.
  • Compute Fourier terms for each drawable stroke.
  • Animate reconstructions in Sequential or Together mode.
  • Use path-length timing so equal traced distance takes equal time.
  • Preserve completed Fourier traces while later strokes animate.
  • Visualize components as rotor-driven disks, blades, or companion vectors.
  • Generate optional path-driven sound from the same animation state.
  • Control volume from a compact floating volume drawer.
  • Capture PNG snapshots (share or download).
  • Export MP4 from the snapshot menu, with optional audio capture, quality presets, duration presets, cancel support, and storage checks.
  • Persist lightweight user preferences across refreshes.

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 a drawing object:

type DrawingObject =
	| { type: "freehand"; id: string; stroke: Stroke }
	| { type: "shape"; id: string; shape: ShapeObject }
	| { type: "bucket-fill"; id: string; fill: BucketFillObject };

The Fourier pipeline only consumes drawable stroke geometry, so freehand and shape objects are converted to Stroke values before resampling and DFT. Bucket fills remain visual-layer data.

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

This keeps input handling coherent while preserving special behaviors like fill rendering and object transforms.

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

Shape tools live in a dedicated floating shape drawer on the right side.

The current shape tools are:

line
ellipse
rectangle
parallelogram
equilateral triangle
isosceles triangle
scalene 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 visible sound control is volume-first:

speaker button opens vertical volume drawer
volume = 0 shows muted icon and disables playback
volume > 0 shows volume icon and enables playback behavior

An internal enabled flag is still kept for compatibility, but active sound behavior is effectively driven by soundVolume > 0.

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
  • tool mode
  • selected shape
  • rotor/bivector view
  • animation trace mode
  • visible rotor term count
  • sound volume
  • compatibility sound enabled key (true when volume > 0)
  • export aspect preset
  • export quality preset
  • export duration preset

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/
    BucketFillCanvas.tsx
    DrawingCanvas.tsx
    ObjectTransformCanvas.tsx
    RotorCanvas.tsx
    ShapePlacementCanvas.tsx
    VideoExportOverlay.tsx

  image/
    edgeTracing.ts

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

  types/
    geometry.ts

  App.tsx
  main.tsx

docs/
  implementation/
    README.md
    ANIMATION_IMPLEMENTATION.md
    EDGE_DETECTION_IMPLEMENTATION.md
    EXPORT_IMPLEMENTATION.md
    FOURIER_IMPLEMENTATION.md
    GEOMETRIC_ALGEBRA_IMPLEMENTATION.md
    SOUND_IMPLEMENTATION.md
    UI_SURFACES_IMPLEMENTATION.md

public/
  bifourcation_logo.png
  bifourcation-snapshot.png

Implementation docs

Detailed design and implementation notes live in docs/implementation:

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
Edit
Fill
Animate / Pause
Clear canvas

Floating canvas controls:

rotor drawer
shape drawer
color drawer
stroke-width drawer
animation style toggle
volume drawer
image import button
snapshot menu
animation view selector

Floating drawers are anchored to the right and open leftward so they do not cover their trigger buttons.

Tap-away behavior:

tap/click outside any open floating drawer closes it
escape key also closes open drawers

The selected-object action bar appears in edit mode and is mobile-friendly:

Duplicate
Copy
Paste
Flip H
Flip V
Delete

Undo/redo are always available as floating buttons above the bottom bar.

Animation style is controlled by a single right-side toggle:

ArrowDown01 icon = Sequential
ArrowDownFromLine icon = Together (parallel)

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:

Export MP4
Share snapshot
Download PNG

Export MP4 opens a floating export panel (not a full-screen modal) with:

format preset
quality preset
duration preset
output summary
progress
cancel export
share last export

MP4 export uses dynamic loading for the encoder package and can include captured app audio when volume is above zero.

Both snapshot sharing and MP4 sharing use the browser share sheet when file sharing is supported, and fall back to download otherwise.

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.