# Gravity Engine Fix — Design Spec **Date:** 2026-04-24 **Branch:** gravity **Files in scope:** `lib/gravity-engine.ts` ## Problem Two bugs in the current `GravityEngine`: 1. **Lag** — `tick()` calls `el.getBoundingClientRect()` on every body every frame, forcing a layout reflow at 60fps. 2. **Container blowout** — `extractLeaves()` sets `width`/`height` on intermediate nodes, but `stop()` only clears styles on leaf bodies and `.gravity-body` roots. Intermediate nodes keep their locked dimensions, breaking layout on deactivation. ## Approach — Option A: Cache + dirty-set + CSS transition glide-back ### Data structures `PhysicsBody` gains four new fields: - `width`, `height` — snapshotted from `getBoundingClientRect()` at `start()`, used in `tick()` for floor collision instead of live DOM queries - `originX`, `originY` — `rect.left`/`rect.top` at activation time, used as the glide-back transform target `GravityEngine` gains: - `dirtyElements: HTMLElement[]` — every element whose inline styles are touched (leaves, intermediate containers, `.gravity-body` roots). `stop()` iterates this single list to fully wipe styles. ### `start()` — three strictly ordered phases 1. **Size-lock phase** — walk `.gravity-body` containers and all descendants via `extractLeaves`. For each intermediate node (non-solid, has children), write `width`/`height` from `offsetWidth`/`offsetHeight` and push to `dirtyElements`. Push `.gravity-body` roots too. 2. **Read phase** — batch all `getBoundingClientRect()` across collected leaf elements before touching any positions. One layout flush, no interleaving. 3. **Write phase** — using snapshotted rects, set each leaf to `position: fixed`, zero `left`/`top`, apply `translate(rect.left, rect.top)`. Cache `width`/`height`/`originX`/`originY`. Push leaf to `dirtyElements`. Attach pointer events. ### `tick()` — one change Replace `el.getBoundingClientRect().height` with `body.height`. Remove redundant `position`/`left`/`top` re-sets inside the dragging branch (set once at start, never change). ### `stop()` — glide-back then full cleanup 1. Cancel animation frame, set `isRunning = false`. 2. For each body: apply `transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)` and set `transform: translate(originX, originY)`. 3. After `600ms` (`setTimeout`): iterate `dirtyElements`, clear all inline styles (`transform`, `position`, `left`, `top`, `width`, `height`, `margin`, `transition`). Document flow fully restored. 4. Clear `bodies[]` and `dirtyElements[]`. ## Success criteria - No layout reflow during animation loop - Toggling gravity off glides all elements back to their original positions with a spring curve - After glide completes, all intermediate container styles are cleared and layout is indistinguishable from the initial page load