From 571cf35151a9a189551d5732d447a514f9fdabd4 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 24 Apr 2026 13:18:10 +0200 Subject: [PATCH] docs: gravity engine fix design spec --- .../specs/2026-04-24-gravity-fix-design.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-24-gravity-fix-design.md diff --git a/docs/superpowers/specs/2026-04-24-gravity-fix-design.md b/docs/superpowers/specs/2026-04-24-gravity-fix-design.md new file mode 100644 index 0000000..b802dc9 --- /dev/null +++ b/docs/superpowers/specs/2026-04-24-gravity-fix-design.md @@ -0,0 +1,46 @@ +# 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