docs: gravity engine fix implementation plan
This commit is contained in:
339
docs/superpowers/plans/2026-04-24-gravity-fix.md
Normal file
339
docs/superpowers/plans/2026-04-24-gravity-fix.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# 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.)
|
||||
Reference in New Issue
Block a user