docs: gravity engine fix implementation plan

This commit is contained in:
2026-04-24 13:19:50 +02:00
parent 571cf35151
commit abde9ff0e9

View 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 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.)