From 7cec30170a32723b79d6d25b81211f7430645032 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 24 Apr 2026 13:42:19 +0200 Subject: [PATCH] docs: gravity bug fixes plan --- .../plans/2026-04-24-gravity-bug-fixes.md | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-24-gravity-bug-fixes.md diff --git a/docs/superpowers/plans/2026-04-24-gravity-bug-fixes.md b/docs/superpowers/plans/2026-04-24-gravity-bug-fixes.md new file mode 100644 index 0000000..2d5305f --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-gravity-bug-fixes.md @@ -0,0 +1,247 @@ +# 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.