docs: gravity bug fixes plan
This commit is contained in:
247
docs/superpowers/plans/2026-04-24-gravity-bug-fixes.md
Normal file
247
docs/superpowers/plans/2026-04-24-gravity-bug-fixes.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user