Files
gabrielkaszewski-next/docs/superpowers/plans/2026-04-24-gravity-fix.md

340 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 117 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 1996) 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.)