From 2ba0b90fcedf8bf2bf67208960ef111359c63bf5 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 24 Apr 2026 13:22:45 +0200 Subject: [PATCH] refactor: split start() into size-lock, read, write phases --- lib/gravity-engine.ts | 50 +++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/lib/gravity-engine.ts b/lib/gravity-engine.ts index 93d3262..6fe2425 100644 --- a/lib/gravity-engine.ts +++ b/lib/gravity-engine.ts @@ -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,