# Gravity Engine Bug Fixes 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 three root-cause bugs in `GravityEngine`: too many physics bodies (lag), Phase 1 layout thrashing (startup freeze), and inline style destruction on restore (image breakage). **Architecture:** All three fixes are in `lib/gravity-engine.ts`. The `dirtyElements` field changes from `HTMLElement[]` to `Array<{el, originalCssText}>` so `stop()` restores exact pre-gravity inline styles (fixing image breakage). `start()` is rewritten with a clean read-then-write two-pass approach and a `MAX_DEPTH = 3` depth cap so sections/cards become physics units instead of hundreds of atomic text nodes (fixing lag and startup freeze). No other files change. **Tech Stack:** TypeScript, browser DOM APIs --- ## Files - **Modify:** `lib/gravity-engine.ts` — only file changed --- ### Task 1: Rewrite `start()` and `stop()` with all three fixes **Files:** - Modify: `lib/gravity-engine.ts` - [ ] **Step 1: Read the current file** ```bash cat /mnt/drive/dev/gabrielkaszewski-next/lib/gravity-engine.ts ``` Confirm it starts with the `PhysicsBody` interface and `GravityEngine` class with `bodies`, `dirtyElements`, `animationFrameId`, `isRunning`, and `INITIAL_FORCE` fields. - [ ] **Step 2: Replace the `dirtyElements` field** Find this line in `GravityEngine`: ```typescript private dirtyElements: HTMLElement[] = []; ``` Replace with: ```typescript private dirtyElements: Array<{ el: HTMLElement; originalCssText: string }> = []; ``` - [ ] **Step 3: Replace the entire `start()` method** Find and replace the full `start()` method with: ```typescript start() { if (this.isRunning) return; this.isRunning = true; this.dirtyElements = []; const MAX_DEPTH = 3; const containers = document.querySelectorAll(".gravity-body"); const leafElements: HTMLElement[] = []; const intermediates: { el: HTMLElement; w: number; h: number }[] = []; // READ-ONLY traversal: classify all elements and snapshot sizes. // No DOM writes here — one layout flush for the entire traversal. const collectElements = (el: HTMLElement, depth: number) => { // Skip purely decorative elements (e.g. background images). if (el.classList.contains("pointer-events-none")) return; const isSolid = ["SVG", "IMG", "BUTTON", "IFRAME", "A"].includes( el.tagName.toUpperCase(), ); // Treat as a leaf if solid, childless, or at the depth cap. if (isSolid || el.children.length === 0 || depth >= MAX_DEPTH) { if (el.offsetWidth > 0 && el.offsetHeight > 0) { leafElements.push(el); } return; } intermediates.push({ el, w: el.offsetWidth, h: el.offsetHeight }); Array.from(el.children).forEach((child) => collectElements(child as HTMLElement, depth + 1), ); }; const rootSizes: { el: HTMLElement; w: number; h: number }[] = []; containers.forEach((container) => { const htmlContainer = container as HTMLElement; rootSizes.push({ el: htmlContainer, w: htmlContainer.offsetWidth, h: htmlContainer.offsetHeight, }); Array.from(htmlContainer.children).forEach((child) => collectElements(child as HTMLElement, 0), ); }); // WRITE PASS 1: size-lock roots and intermediates so they don't collapse // when their children become position:fixed. [...rootSizes, ...intermediates].forEach(({ el, w, h }) => { this.dirtyElements.push({ el, originalCssText: el.style.cssText }); el.style.width = `${w}px`; el.style.height = `${h}px`; }); // READ PASS 2: batch all getBoundingClientRect calls before any leaf writes. const snapshots = leafElements.map((el) => ({ el, rect: el.getBoundingClientRect(), })); // WRITE PASS 2: apply fixed positioning to leaves using snapshotted rects. this.bodies = snapshots.map(({ el, rect }) => { this.dirtyElements.push({ el, originalCssText: el.style.cssText }); 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)`; 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 4: Replace the entire `stop()` method** Find and replace the full `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, restore every element's original inline // styles so document flow is fully recovered — including elements that had // pre-existing inline styles we must not erase (e.g. Next.js fill images). const elementsToClear = [...this.dirtyElements]; this.bodies = []; this.dirtyElements = []; setTimeout(() => { elementsToClear.forEach(({ el, originalCssText }) => { el.style.cssText = originalCssText; }); }, DURATION); } ``` - [ ] **Step 5: Verify TypeScript compiles with no errors** ```bash cd /mnt/drive/dev/gabrielkaszewski-next && npx tsc --noEmit ``` Expected: no errors. - [ ] **Step 6: Commit** ```bash git -C /mnt/drive/dev/gabrielkaszewski-next add lib/gravity-engine.ts git -C /mnt/drive/dev/gabrielkaszewski-next commit -m "fix: two-pass start, depth cap, cssText restore on stop" ``` --- ### Task 2: Visual verification **Files:** none — browser testing only - [ ] **Step 1: Start the dev server** ```bash cd /mnt/drive/dev/gabrielkaszewski-next && npm run dev ``` Open `http://localhost:3000`. - [ ] **Step 2: Test home page — activation** Click the gravity toggle (bottom-right, yellow). Verify: - No freeze or stutter at the moment of activation - Elements fall and bounce without visible lag - Hero section text/links fall; background image does NOT move (excluded via `pointer-events-none` class) - [ ] **Step 3: Test home page — deactivation** Click toggle again. Verify: - Elements glide back with spring curve (~600ms) - Page returns to its original layout — no broken images, no collapsed sections, no leftover inline styles - Hero background image is still correctly filling the section - [ ] **Step 4: Test projects page** Navigate to `http://localhost:3000/projects`. Toggle gravity on/off. Verify: - Activation is instant (no freeze) - Animation runs smoothly — project entries fall as chunked units (left panel + image panel), not as hundreds of individual text nodes - Deactivation restores layout cleanly - [ ] **Step 5: Test about page** Navigate to `http://localhost:3000/about`. Toggle gravity on/off. Verify: - Profile photo glides back and renders correctly after restore - All sections (hobbies, toolkit, FAQ) restore to original layout - [ ] **Step 6: Toggle multiple times** On each page, toggle gravity on/off 3 times in succession. Verify no accumulated style corruption across cycles. - [ ] **Step 7: Commit any fixes found, or mark done** If visual issues were found and fixed during testing: ```bash git -C /mnt/drive/dev/gabrielkaszewski-next add lib/gravity-engine.ts git -C /mnt/drive/dev/gabrielkaszewski-next commit -m "fix: address visual issues found during gravity verification" ``` If no issues found: skip this step.