340 lines
8.7 KiB
Markdown
340 lines
8.7 KiB
Markdown
# 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.)
|