// Single owner for every resource the tree allocates, so one dispose() call // releases all of it. // // Why this exists: the PixiJS layer creates ~480 infinite GSAP tweens, a ticker // callback, and a dozen DOM listeners. None of them were ever released, and 112 // of the tweens are created inside an `onComplete` — so they don't exist yet at // the moment a naive `killTweensOf` sweep would run. // // `gsap.context()` solves exactly that: GSAP records each animation's context on // creation, and restores that context while invoking the animation's callbacks // (see `_callback` in gsap-core). So a tween created inside a tracked tween's // onComplete is tracked by the same context, and ctx.kill() gets all of them. import { gsap } from 'gsap'; export function createLifecycle() { const disposers: Array<() => void> = []; const gsapCtx = gsap.context(() => {}); let isDisposed = false; return { /** True once dispose() has run — guards async work that resolves late. */ get disposed() { return isDisposed; }, /** * Run `fn` with GSAP's active context set, so every tween it creates is * tracked for teardown. Returns whatever `fn` returns. */ animate(fn: () => T): T { return gsapCtx.add(fn) as T; }, /** Register an arbitrary teardown step. */ add(fn: () => void) { disposers.push(fn); }, /** Add a DOM listener and register its removal in one call. */ listen( target: T, type: string, handler: (event: never) => void, options?: AddEventListenerOptions ) { target.addEventListener(type, handler as EventListener, options); disposers.push(() => target.removeEventListener(type, handler as EventListener, options)); }, /** * Release everything, newest first. `kill()` rather than `revert()`: we do * not want GSAP writing properties back onto Pixi objects that are about * to be destroyed. */ dispose() { if (isDisposed) return; isDisposed = true; gsapCtx.kill(); for (let i = disposers.length - 1; i >= 0; i--) { try { disposers[i](); } catch (err) { // One failing teardown step must not strand the rest. console.warn('Teardown step failed:', err); } } disposers.length = 0; } }; } export type Lifecycle = ReturnType;