A physics-driven text layout experiment. Grab any element on the page and throw it through the text — paragraphs reflow around moving obstacles in real time using Pretext's line-by-line layout engine and Matter.js physics.
Fetch any public webpage by URL to turn it into a live physics playground, or explore the built-in preset scenes.
npm install
npm run devOpen http://localhost:5173.
npm run dev # Dev server on http://localhost:5173
npm run build # TypeScript check + Vite build → dist/
npm run lint # Biome check (lint + format)
npm run lint:fix # Biome check with auto-fix
npm run format # Biome format only
npm run test # Vitest run (all tests)
npm run test:watch # Vitest watch mode
npm run preview # Preview production build- Engine — A minimal physics sandbox for experimenting with throwable elements and text reflow
- Landing — A dark-themed product landing page with feature cards and CTAs
- Alice — Alice in Wonderland Chapter I with themed illustrations and interactive elements
Paste any public URL into the Pages panel to fetch and render it as a live physics scene. The page is rendered in a sandboxed iframe with external CSS inlined. Images, badges, and visual elements are auto-selected as throwable physics bodies. Built-in site rules clean up known sites (NYTimes, Wikipedia, Notion, Craigslist).
Enter throwable picker mode via the toolbar to click any element and toggle it as a physics body. Alternatively, use save mode to stash elements for reuse across scenes.
Save elements from any scene to a persistent stash. Drag saved items onto any page to drop them as new throwable bodies. Stash persists via localStorage.
| Button | Action |
|---|---|
| Pages | Switch presets, fetch URLs, import HTML |
| Pause / Resume | Freeze or resume physics |
| Explode | Apply random velocity to all throwable bodies |
| Reset | Return all bodies to original positions |
| Picker | Toggle throwable / save picker modes |
| Stash | Open saved elements, drag onto scene |
| Settings | Physics, reflow, gravity, debug bounds, reset all |
- React 19 + TypeScript (strict mode) — UI and application logic
- Vite 8 — Build tooling with a dev-only fetch proxy for CORS-free page fetching
- Matter.js — 2D physics engine (rigid bodies, mouse constraint, fixed timestep)
- @chenglou/pretext — Text measurement and line-by-line layout without DOM reflows
- Biome — Linting and formatting
- Vitest — Unit tests with jsdom + Testing Library
URL → fetchPageHtml() → prepareHtmlForViewer() → SnapshotCustomPage (localStorage)
↓
iframe srcDoc
↓
snapshotWalker() → candidates + text blocks
↓
auto-select throwables → ImportedPhysicsClone overlays
↓
Matter.js bodies ← RAF loop → SnapshotTextLayer (Pretext)
- Fetch —
fetchPageHtml(url)routes through the Vite dev middleware at/api/fetch-pageto bypass CORS, spoofing a desktop User-Agent. Private/internal network hostnames are blocked. - Prepare (
snapshot/prepare.ts) — Inlines external CSS, rewrites relative URLs to absolute (resolving CSSurl()against the CSS file's own origin), strips<script>tags, fixes JS-dependent visibility classes, and injects a<base>tag. - Render & Walk (
snapshot/walker.ts) — Renders the prepared HTML in a hidden offscreen iframe, then walks the DOM (up to depth 40) reading computed styles and bounding rects to build a list of selectable candidates. - Auto-select — Filters candidates by size, skips inline/float-anchored/gallery elements, picks the best throwable elements (images, badges) up to a cap. Per-site rules (
siteRules.ts) can force or exclude specific selectors. - Physics overlay — Selected elements are hidden in the iframe and replaced with
ImportedPhysicsClonecomponents that clone live DOM nodes with inline styles. Matter.js bodies drive their positions via RAF. - Text reflow — When elements move from their original positions they become obstacles.
SnapshotTextLayerre-runscomputeTextFlow()and re-renders Pretext'd text blocks around them.
The core algorithm uses Pretext's layoutNextLine:
- Prepare —
prepareWithSegments(text, font)does one-time canvas-based text analysis. Results are cached bytext+fontkey. - Row iteration — Walk downward in
lineHeightincrements from the container top. - Obstacle projection —
getBlockedIntervalsForRow()finds overlapping obstacle AABBs and projects their horizontal extents for the current row. - Available segments —
getAvailableSegments()subtracts merged blocked intervals from the row width, adding configurable padding around obstacles. - Best segment —
pickBestSegment()chooses the widest available span (leftmost on ties). - Line layout —
layoutNextLine(prepared, cursor, width)fits as much text as possible into the chosen segment, advancing the cursor. - Positioning — Each line is placed absolutely at
(segment.left, rowY).
For snapshot pages, only elements that have moved from their original position act as text obstacles — preventing false displacement of text around static content.
Matter.js world management:
- Fixed-timestep runner with configurable gravity
- Rectangular, circular, and polygon body creation
- Boundary walls (floor, ceiling, left/right) to contain bodies
MouseConstraintfor drag interaction with tuned stiffness/damping- Mouse offset re-synced on scroll and resize
- Body clamping to prevent wall escape on resize
explode(),reset(),pin(),unpin(),pause(),resume(), and gravity adjustment
| Component | Role |
|---|---|
DominoScene |
Orchestrator for preset scenes: physics init, RAF sync, obstacle building |
SnapshotPageView |
Orchestrator for fetched/imported pages: iframe, scanning, physics overlay |
SceneTextLayer |
Pretext text overlay for preset scenes |
SnapshotTextLayer |
Pretext text overlay for snapshot pages |
SnapshotPhysicsOverlay |
Physics clone management for snapshot pages |
TextFlowRegion |
Renders a single reflowed text block as absolutely-positioned line <div>s |
PhysicsDomItem |
Throwable DOM element driven by Matter.js body position (preset scenes) |
ImportedPhysicsClone |
Cloned live DOM node from iframe, positioned by physics body |
ElementRenderer |
Type-switch renderer for preset SceneElement visuals |
Toolbar |
Compact pill toolbar with flyout panels |
Three React contexts (React 19 use() / <Context value={}> API) reduce prop drilling:
NavigationContext— Active page, preset key, pages list, import/fetch stateSavedElementsContext— Stash of saved elements (save, unsave, remove, drop)SettingsContext—SceneSettings(physics params, debug flags) + stats for the Settings panel
Key hooks:
useScenePhysics— Composes picker mode, physics loop, and animated alpha for scene componentsusePickerPause—PickerModediscriminated union (null | "throwable" | "save") with physics pause/resumeusePhysicsLoop— RAF-driven Matter.js position syncuseSnapshotScanner— Iframe DOM scanning and selection stateuseTextFlow/useImportedTextLayouts— Text flow computation with Pretext
Per-site configuration applied when fetching known URLs:
removeSelectors— DOM elements to strip entirely (ads, nav overlays, banners)css— Additional CSS injected into the iframe for layout correctionscssLinks— CSS files to fetch and inline for sites that load all CSS via JavaScript (e.g. Craigslist)snapshotPretext— Which tags/selectors participate in Pretext text flowsnapshotSolid— Elements that act as static walls rather than throwablessnapshotAutoSelect— Force or exclude elements from automatic throwable selection
Built-in rules: Wikipedia, NYTimes, Notion, Craigslist.
prepareWithSegments()runs once per uniquetext+fontpair; results are cached in aMaplayoutNextLine()is pure arithmetic — sub-millisecond per paragraph- Physics position state is compared as string snapshots; React state only updates on meaningful change
- Text reflow is gated on a
generationcounter that increments when obstacle positions change - Snapshot page physics uses only dynamic bodies — no static obstacle bodies in the Matter world
src/
App.tsx # Lean orchestrator — contexts + routing
main.tsx # Entry point with URL param handling
contexts/ # React contexts (React 19 use() API)
hooks/ # Custom React hooks
components/ # React UI components
picker/ # Element picker overlays + badges
toolbar/ # Toolbar flyout panels (Pages, Settings, Stash)
physics/
engine.ts # Matter.js world setup, bodies, walls, mouse
textflow/
useTextFlow.ts # Core text-flow algorithm with Pretext
obstacles.ts # Obstacle projection and interval helpers
glyphBodies.ts # Glyph body measurement from DOM nodes
scene/ # Scene data, types, and snapshot pipeline
types.ts # Core types (SceneElement, CustomPage, etc.)
domSnapshot.ts # Thin re-export facade for snapshot pipeline
snapshot/ # Snapshot pipeline modules
prepare.ts # HTML preparation: CSS inlining, URL rewriting
walker.ts # Iframe render + DOM walker
parser.ts # Regex-based fallback HTML parser
fetchPage.ts # URL fetching + auto-select throwables
snapshotHelpers.ts # Snapshot DOM utilities
snapshotViewUtils.ts # Stage rect conversion helpers
snapshotWalker.ts # Pure iframe DOM scanning function
siteStyles.ts # Per-site CSS overrides + selectors
siteRules.ts # Site rule definitions
presets/ # Preset scene definitions (Engine, Landing, Alice)
utils/
persistence.ts # localStorage save/load/clear
fonts.ts # Font string building + parsing
gifFrames.ts # GIF parsing via gifuct-js
imageAlpha.ts # Alpha row extraction from images
stashImageFromFile.ts # Create SavedElement from dropped image files
physics.ts # Physics math helpers
styles.ts # Background style helpers
url.ts # URL normalization