refactor: split start() into size-lock, read, write phases

This commit is contained in:
2026-04-24 13:22:45 +02:00
parent 415dcb8459
commit 2ba0b90fce

View File

@@ -24,70 +24,68 @@ export class GravityEngine {
start() {
if (this.isRunning) return;
this.isRunning = true;
this.dirtyElements = [];
const containers = document.querySelectorAll(".gravity-body");
const targetElements: HTMLElement[] = [];
const leafElements: HTMLElement[] = [];
// Helper to recursively drill down to leaf nodes
// Phase 1 — Size-lock: freeze intermediate container dimensions
// so they don't collapse when children become position:fixed.
const extractLeaves = (el: HTMLElement) => {
// 1. Define "solid" elements that should fall as one single piece
const isSolid = ["SVG", "IMG", "BUTTON", "IFRAME", "A"].includes(
el.tagName.toUpperCase(),
);
// 2. Base case: If it's solid, or has no children, it's a target
if (isSolid || el.children.length === 0) {
// Only extract elements that actually take up visual space
if (el.offsetWidth > 0 && el.offsetHeight > 0) {
targetElements.push(el);
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);
});
Array.from(el.children).forEach((child) =>
extractLeaves(child as HTMLElement),
);
};
containers.forEach((container) => {
const htmlContainer = container as HTMLElement;
// Lock the main container's dimensions
htmlContainer.style.width = `${htmlContainer.offsetWidth}px`;
htmlContainer.style.height = `${htmlContainer.offsetHeight}px`;
this.dirtyElements.push(htmlContainer);
// Start the recursive extraction on its direct children
Array.from(htmlContainer.children).forEach((child) => {
extractLeaves(child as HTMLElement);
});
Array.from(htmlContainer.children).forEach((child) =>
extractLeaves(child as HTMLElement),
);
});
const initialStates = targetElements.map((el) => {
const rect = el.getBoundingClientRect();
return { el, rect };
});
// Phase 2 — Read: batch all getBoundingClientRect() before any writes.
const snapshots = leafElements.map((el) => ({
el,
rect: el.getBoundingClientRect(),
}));
this.bodies = initialStates.map(({ el, rect }) => {
// Lock dimensions so it doesn't warp when pulled out of flex/grid
// 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";
// Snap to fixed positioning at its exact current visual location
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: el,
el,
x: rect.left,
y: rect.top,
vx: (Math.random() - 0.5) * 8, // slight explosion outward
vy: (Math.random() - 0.5) * 5, // slight pop upward
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 5,
width: rect.width,
height: rect.height,
originX: rect.left,