# 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.)