Ink & Vapor

The Equations

Every curve, every ripple, every dissolution — governed by mathematics that fits on a single page.

scroll to explore
01

The Boundary Wave

The boundary line that divides ink from vapor is not a simple sine wave. It is a composite of three sine waves at different frequencies, amplitudes, and speeds — creating an organic, ever-shifting curve that feels alive.

y(x, t) = y₀ + A₁ · sin(k₁x + ω₁t + φ₁) + A₂ · sin(k₂x + ω₂t + φ₂) + A₃ · sin(k₃x + ω₃t + φ₃)
Three superimposed waves create the organic boundary shape
y₀
Base Y position (controlled by drag)
A₁ = 12, k₁ = 0.008, ω₁ = 0.6, φ₁ = 0
Primary wave — large, slow undulation
A₂ = 6, k₂ = 0.018, ω₂ = 1.2, φ₂ = π/2
Secondary wave — medium ripple
A₃ = 3, k₃ = 0.035, ω₃ = 2.1, φ₃ = π/4
Tertiary wave — fine detail

Each wave component contributes a different scale of motion. The primary wave creates the broad hills and valleys. The secondary adds medium ripples. The tertiary provides fine texture — like wind on water. Together they produce a curve that never repeats exactly, mimicking natural fluid motion.

Interactive — adjust parameters
02

Click Ripples

When you click on the boundary, a ripple propagates outward from the click point. This is modeled as a Gaussian wavefront — a pulse that travels while decaying in both space and time.

R(x, t) = A · exp(((|x − x₀| − vt)² / 2σ²)) · exp(γt) · sin(β(|x − x₀| − vt))
Gaussian wavefront with temporal and spatial decay
A = 25
Initial amplitude (pixels of displacement)
v = 400
Propagation speed (px/s)
σ = 80
Spatial width of the wave packet
γ = 1.8
Temporal damping rate (per second)
β = 0.05
Oscillation frequency within the packet

The three factors multiply together: the Gaussian envelope shapes the packet in space (peaked at the wavefront, falling off on both sides), the exponential decay reduces amplitude over time, and the sine oscillation creates the alternating crest-and-trough pattern within the packet. Multiple ripples can coexist and superimpose linearly.

Interactive — click to spawn ripples
03

Circular Wave

The cursor ring uses the same superposition principle as the boundary, but wrapped around a circle. Instead of displacement along Y, the wave displaces the radius at each angle.

r(θ, t) = r₀ + a₁ · sin(n₁θ + ω₁t) + a₂ · cos(n₂θ − ω₂t)
Angular wave displacement around a circular base
r₀
Base radius (responsive: 14–22px)
a₁ = 2.5, n₁ = 7, ω₁ = 2.0
Primary angular wave — 7 peaks rotating clockwise
a₂ = 1.25, n₂ = 10.5, ω₂ = 1.4
Secondary wave — counter-rotating interference

On click, an additional pulse term is added: a burst of 8px amplitude with 4 angular peaks, decaying as e^(−t·6) over ~0.6 seconds. An expanding ripple ring simultaneously radiates outward from the ring, creating a visible shockwave.

Interactive — move mouse, click for pulse
04

Noise Flow Field

The vapor particles are driven by a multi-octave fractal Brownian motion — layered value noise at decreasing scales, creating organic swirling patterns.

F(x, y, t) = gⁱ · N(x·lⁱ/s, y·lⁱ/s + z(t))
Fractal Brownian motion with time-evolving z-offset
g = 0.5
Gain — amplitude multiplier per octave
l = 2.2
Lacunarity — frequency multiplier per octave
s = 400
Base spatial scale (pixels)
z(t) = 0.15t
Time evolution (scroll can boost to 0.75t)
N(x, y)
2D value noise (hash + bilinear interpolation, 3 octaves)

The flow field produces two components: vₓ from noise at (x/s, y/s + 5.2 + z) and vᵧ from noise at (x/s + 5.2 + z, y/s + 0.5z). The offset decorrelates the two components, creating rotational flow rather than simple translation. Particles integrate these velocities with damping, creating the mesmerizing drift patterns.

05

Ink ↔ Vapor Transition

Each character has an inkness value ι ∈ [0, 1] that determines whether it's rendered as solid ink or dissolved vapor. The transition is governed by the character's position relative to the boundary.

/dt = −k_d   when   y_char < y_boundary(x)
/dt = +k_c   when   y_chary_boundary(x)
First-order rate equation — dissolve or crystallize
k_d = 2.2
Dissolve rate (full dissolution in ~0.5s)
k_c = 2.8
Crystallize rate (full crystallization in ~0.4s)
y_boundary(x)
The 3-component composite wave from Section 01, including all active ripples

When a character dissolves (ι → 0), it drifts upward with velocity and spawns vapor particles at its position. When it crystallizes (ι → 1), it springs back to its home position with damped spring physics: F = −k·Δx, damping 0.88 per frame.

06

Hold Vortex

Holding the mouse button creates a gravitational well — particles experience both inward pull and tangential spin, spiraling toward the cursor.

F⃗ = −P · (1 − d/R) · + S · (1 − d/R) · θ̂
Radial pull + tangential spin, both falling off linearly with distance
P = 800
Pull strength (inward force)
S = 1200
Spin strength (tangential force)
R = 250
Effect radius (pixels)
d = |p⃗ − c⃗|
Distance from particle to cursor
r̂, θ̂
Radial and tangential unit vectors from cursor

The linear falloff (1 − d/R) ensures a smooth transition at the edge — particles at the boundary barely feel the pull, while those near the center are drawn in sharply. The tangential component θ̂ is perpendicular to the radial direction, creating the spiral trajectory that makes the vortex visually compelling.

Interactive — hold mouse button
07

Character Palette

Each vapor particle renders a typographic character selected by matching the local smoke density to the character's measured brightness.

c* = argminc [ α · |b(c) − ρ| + β · |w(c) − w₀| / w₀ ]
Brightness-weighted character selection with width matching
c*
Selected character from palette (~180 entries)
b(c) ∈ [0, 1]
Measured brightness of character c (alpha scan of rendered glyph)
w(c)
Measured width of character c (via pretext canvas measurement)
ρ
Local smoke density (0 = clear, 1 = dense)
α = 2.5, β = 1.0
Brightness weighted 2.5× more than width

The palette spans 70+ characters across 3 weights (300, 500, 800) and 2 styles (normal, italic) in Georgia — ~180 entries total. Binary search on brightness finds the nearest match, then a ±15 neighborhood search optimizes for width fit. Dense smoke selects dark, heavy characters (@, #, %). Wispy smoke selects light marks (., ,, :).

Live — animated density map