From abde9ff0e95d127915e78d086b7e4a8e0605a891 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 24 Apr 2026 13:19:50 +0200 Subject: [PATCH] docs: gravity engine fix implementation plan --- .../plans/2026-04-24-gravity-fix.md | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-24-gravity-fix.md diff --git a/docs/superpowers/plans/2026-04-24-gravity-fix.md b/docs/superpowers/plans/2026-04-24-gravity-fix.md new file mode 100644 index 0000000..a209f94 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-gravity-fix.md @@ -0,0 +1,339 @@ +# Gravity Engine Fix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix lag and container-blowout bugs in `GravityEngine`, and add a spring-curve glide-back animation when gravity is toggled off. + +**Architecture:** Cache element dimensions at `start()` to eliminate per-frame layout reflow. Track every element whose inline styles are modified in a `dirtyElements` array so `stop()` can fully restore the DOM. On deactivation, animate leaves back to their origin positions via a CSS transition before clearing all styles. + +**Tech Stack:** TypeScript, browser DOM APIs, CSS transitions (`cubic-bezier`) + +--- + +## Files + +- **Modify:** `lib/gravity-engine.ts` — only file changed + +--- + +### Task 1: Extend `PhysicsBody` and add `dirtyElements` to `GravityEngine` + +**Files:** +- Modify: `lib/gravity-engine.ts:1-17` + +- [ ] **Step 1: Replace the `PhysicsBody` interface and class fields** + +Open `lib/gravity-engine.ts`. Replace lines 1–17 with: + +```typescript +interface PhysicsBody { + el: HTMLElement; + x: number; + y: number; + vx: number; + vy: number; + width: number; + height: number; + originX: number; + originY: number; + isDragging: boolean; + startX: number; + startY: number; +} + +export class GravityEngine { + private bodies: PhysicsBody[] = []; + private dirtyElements: HTMLElement[] = []; + private animationFrameId: number | null = null; + private isRunning = false; + + INITIAL_FORCE = 10; +``` + +- [ ] **Step 2: Verify TypeScript compiles** + +```bash +npx tsc --noEmit +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add lib/gravity-engine.ts +git commit -m "refactor: extend PhysicsBody with cached dims and add dirtyElements" +``` + +--- + +### Task 2: Refactor `start()` into three ordered phases + +**Files:** +- Modify: `lib/gravity-engine.ts` — the `start()` method + +- [ ] **Step 1: Replace the entire `start()` method** + +Find and replace the `start()` method (currently lines 19–96) with: + +```typescript + start() { + if (this.isRunning) return; + this.isRunning = true; + this.dirtyElements = []; + + const containers = document.querySelectorAll(".gravity-body"); + const leafElements: HTMLElement[] = []; + + // Phase 1 — Size-lock: freeze intermediate container dimensions + // so they don't collapse when children become position:fixed. + const extractLeaves = (el: HTMLElement) => { + const isSolid = ["SVG", "IMG", "BUTTON", "IFRAME", "A"].includes( + el.tagName.toUpperCase(), + ); + + if (isSolid || el.children.length === 0) { + if (el.offsetWidth > 0 && el.offsetHeight > 0) { + leafElements.push(el); + } + return; + } + + el.style.width = `${el.offsetWidth}px`; + el.style.height = `${el.offsetHeight}px`; + this.dirtyElements.push(el); + + Array.from(el.children).forEach((child) => + extractLeaves(child as HTMLElement), + ); + }; + + containers.forEach((container) => { + const htmlContainer = container as HTMLElement; + htmlContainer.style.width = `${htmlContainer.offsetWidth}px`; + htmlContainer.style.height = `${htmlContainer.offsetHeight}px`; + this.dirtyElements.push(htmlContainer); + + Array.from(htmlContainer.children).forEach((child) => + extractLeaves(child as HTMLElement), + ); + }); + + // Phase 2 — Read: batch all getBoundingClientRect() before any writes. + const snapshots = leafElements.map((el) => ({ + el, + rect: el.getBoundingClientRect(), + })); + + // Phase 3 — Write: apply fixed positioning using snapshotted rects. + this.bodies = snapshots.map(({ el, rect }) => { + el.style.width = `${rect.width}px`; + el.style.height = `${rect.height}px`; + el.style.margin = "0px"; + el.style.position = "fixed"; + el.style.left = "0px"; + el.style.top = "0px"; + el.style.transform = `translate(${rect.left}px, ${rect.top}px)`; + this.dirtyElements.push(el); + + const body: PhysicsBody = { + el, + x: rect.left, + y: rect.top, + vx: (Math.random() - 0.5) * 8, + vy: (Math.random() - 0.5) * 5, + width: rect.width, + height: rect.height, + originX: rect.left, + originY: rect.top, + isDragging: false, + startX: 0, + startY: 0, + }; + + this.attachMouseEvents(body); + return body; + }); + + this.tick(); + } +``` + +- [ ] **Step 2: Verify TypeScript compiles** + +```bash +npx tsc --noEmit +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add lib/gravity-engine.ts +git commit -m "refactor: split start() into size-lock, read, write phases" +``` + +--- + +### Task 3: Fix `tick()` — use cached dimensions, remove redundant style sets + +**Files:** +- Modify: `lib/gravity-engine.ts` — the `tick` arrow function + +- [ ] **Step 1: Replace the `tick` method** + +Find and replace the `private tick = () => { ... };` block with: + +```typescript + private tick = () => { + if (!this.isRunning) return; + + const gravity = 0.5; + const bounce = -0.7; + const floorY = window.innerHeight; + + this.bodies.forEach((body) => { + if (body.isDragging) { + body.el.style.transform = `translate(${body.x}px, ${body.y}px)`; + return; + } + + body.vy += gravity; + body.x += body.vx; + body.y += body.vy; + + body.vx *= 0.99; + body.vy *= 0.99; + + if (body.y + body.height > floorY) { + body.y = floorY - body.height; + body.vy *= bounce; + body.vx *= 0.9; + } + + body.el.style.transform = `translate(${body.x}px, ${body.y}px)`; + }); + + this.animationFrameId = requestAnimationFrame(this.tick); + }; +``` + +- [ ] **Step 2: Verify TypeScript compiles** + +```bash +npx tsc --noEmit +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add lib/gravity-engine.ts +git commit -m "perf: use cached body dimensions in tick, remove redundant style sets" +``` + +--- + +### Task 4: Implement `stop()` with glide-back animation and dirty-set cleanup + +**Files:** +- Modify: `lib/gravity-engine.ts` — the `stop()` method + +- [ ] **Step 1: Replace the `stop()` method** + +Find and replace the entire `stop()` method with: + +```typescript + stop() { + this.isRunning = false; + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + + const DURATION = 600; + + // Glide each body back to its origin position. + this.bodies.forEach((body) => { + body.el.style.transition = `transform ${DURATION}ms cubic-bezier(0.22, 1, 0.36, 1)`; + body.el.style.transform = `translate(${body.originX}px, ${body.originY}px)`; + }); + + // After the transition completes, clear all inline styles from every + // element we touched so the document flow is fully restored. + const elementsToClear = [...this.dirtyElements]; + this.bodies = []; + this.dirtyElements = []; + + setTimeout(() => { + elementsToClear.forEach((el) => { + el.style.transform = ""; + el.style.transition = ""; + el.style.position = ""; + el.style.left = ""; + el.style.top = ""; + el.style.width = ""; + el.style.height = ""; + el.style.margin = ""; + }); + }, DURATION); + } +``` + +- [ ] **Step 2: Verify TypeScript compiles** + +```bash +npx tsc --noEmit +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add lib/gravity-engine.ts +git commit -m "feat: glide-back animation on stop, full dirty-set cleanup" +``` + +--- + +### Task 5: Visual verification + +**Files:** none — browser testing only + +- [ ] **Step 1: Start the dev server** + +```bash +npm run dev +``` + +Open `http://localhost:3000` in a browser. + +- [ ] **Step 2: Test activation** + +Click the gravity toggle button (bottom-right, yellow). Verify: +- Elements fall and bounce with no visible lag +- Page containers do not collapse or change size while gravity is active +- Elements can be dragged and thrown + +- [ ] **Step 3: Test deactivation** + +Click the toggle again. Verify: +- All elements glide back to their original positions with a spring curve (~600ms) +- After the animation, the page looks exactly like it did before gravity was activated (no leftover inline styles, no layout shift) +- Toggling on/off multiple times works correctly each time + +- [ ] **Step 4: Test the about page** + +Navigate to `http://localhost:3000/about`, repeat the activation/deactivation test. The about page also has a `.gravity-body` container with more complex nested layout (flex, prose, grid sections) — verify it also restores cleanly. + +- [ ] **Step 5: Commit if any fixes were needed, otherwise done** + +```bash +git add lib/gravity-engine.ts +git commit -m "fix: address visual issues found during gravity testing" +``` + +(Skip this step if no issues were found.)