liquiddom
Core Concepts

Core Concepts

The architectural decisions behind liquiddom — what makes it small, fast, and accessible by construction.

The Rule of Two

Rust is DOM-blind and color-blind. TypeScript owns the DOM and rendering. Communication is one-way through a pre-allocated flat buffer.

┌─────────────────────────────────────┐
│  TypeScript (DOM-aware)             │
│  - PhantomObserver: reads rects     │
│  - RAF loop                         │
│  - Pointer/scroll/visibility events │
│  - Renderer (Canvas2D or WebGPU)    │
└─────────┬───────────────────────────┘
          │ flat Float32Array (9 floats/entity)
          │ NO JSON, NO setters
┌─────────▼───────────────────────────┐
│  Rust → WASM (DOM-blind)            │
│  - Physics: mass-spring-damper      │
│  - Particle constraints             │
│  - Area preservation                │
└─────────────────────────────────────┘

The FFI buffer (9 floats per entity)

A single Float32Array in WASM linear memory. No JSON crosses the boundary. Slot indices are stable:

Index Field Writer
0x (DOM left)TypeScript
1y (DOM top)TypeScript
2wTypeScript
3hTypeScript
4interaction_stateTypeScript
5liquid_typeTypeScript
6impulse_vxTypeScript
7impulse_vyTypeScript
8border_radius_pxTypeScript

w == 0 means "slot inactive" — Rust skips that entity in tick(). The constants FLOATS_PER_ENTITY and PARTICLES_PER_BODY are duplicated in Rust and TypeScript and MUST stay in sync.

Per-frame loop ordering

Order matters. Sync first, tick second, render third.

  1. Cache getBoundingClientRect once; set coord offset for container mode
  2. Set canvas DPR via setTransform (never cumulative)
  3. ctx.clearRect
  4. observer.sync() — DOM rect → buffer
  5. core.tick(dt, ...) — physics step (paused under scroll or reduced-motion)
  6. observer.render(...) — particles → canvas splines

liquid_type dispatch

Slot 5 of the buffer selects the per-entity behavior:

Value Name Behavior
0DefaultSoft-body following DOM rect
3DraggedDrag the element; physics follows
4ShakeVibrates after impulse()
5TweenExternal position target
6FreeDropDOM-less droplet particles

NaN and unknown values fall back to Default.

The rendering backend

renderer: 'auto' (default) probes WebGPU and falls back to Canvas2D on WebGPUUnavailableError. Toggle it globally via the segmented control in the page header — the choice persists in localStorage.

  • Canvas2D path: universal browser support, Bezier-spline blobs, no fusion or refraction.
  • WebGPU path: Chrome 113+, Edge 113+. SDF blobs, metaball fusion, background refraction.