Files

1 line
24 KiB
Plaintext
Raw Permalink Normal View History

{"version":3,"file":"LayoutAnimationBuilder.mjs","sources":["../../../src/layout/LayoutAnimationBuilder.ts"],"sourcesContent":["import { clamp } from \"motion-utils\"\nimport { GroupAnimation } from \"../animation/GroupAnimation\"\nimport type { AnimationOptions, Transition } from \"../animation/types\"\nimport { frameData, frameSteps } from \"../frameloop\"\nimport { microtask } from \"../frameloop/microtask\"\nimport { time } from \"../frameloop/sync-time\"\nimport { HTMLProjectionNode } from \"../projection/node/HTMLProjectionNode\"\nimport type { IProjectionNode } from \"../projection/node/types\"\nimport { HTMLVisualElement } from \"../render/html/HTMLVisualElement\"\nimport { visualElementStore } from \"../render/store\"\nimport { hasTransform } from \"../projection/utils/has-transform\"\nimport {\n resolveElements,\n type ElementOrSelector,\n} from \"../utils/resolve-elements\"\n\ntype LayoutAnimationScope = Element | Document\ntype LayoutBuilderResolve = (animation: GroupAnimation) => void\ntype LayoutBuilderReject = (error: unknown) => void\n\ninterface RestorePoint {\n parent: Element\n next: ChildNode | null\n}\n\nconst layoutSelector = \"[data-layout],[data-layout-id]\"\n\n/**\n * All imperatively-created projection nodes live in one persistent tree,\n * shared across animateLayout() calls (and with any React-created nodes,\n * via the singleton document root). Keyed by element for reuse.\n */\nconst layoutNodes = new WeakMap<Element, IProjectionNode>()\n\n/**\n * Builders created within the same synchronous tick are flushed together\n * as a single \"commit\": every node is snapshotted before any updateDom\n * runs, mirroring React batching renders from different parts of the tree.\n */\nlet pendingBuilders: LayoutAnimationBuilder[] | undefined\n\nfunction collectLayoutElements(scope: LayoutAnimationScope): HTMLElement[] {\n const elements: HTMLElement[] = []\n\n if (scope instanceof HTMLElement && scope.matches(layoutSelector)) {\n elements.push(scope)\n }\n\n scope.querySelectorAll(layoutSelector).forEach((element) => {\n if (element instanceof HTMLElement) elements.push(element)\n })\n\n return elements\n}\n\n/**\n * Process any work scheduled on the frameloop now. A previous animation\n * may have been seeked while paused (controls.time = x) without a frame\n * having rendered it - we must materialise that state into the DOM\n * before taking snapshots.\n */\nfunction flushPendingFrame() {\n if (frameData.isProcessing) return\n\n const now = time.now()\n frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp)\n frameData.timestamp = now\n frameData.isProcessing = true\n frameSteps.update.process(frameData)\n frameSteps.preRender.process(frameData)\n frameSteps.render.process(frameData)\n frameData.isProcessing = false\n}\n\nfunction getProjectionParent(element: Element): IProjectionNode | undefined {\n let ancestor = element.parentElement\n while (ancestor) {\n const node = layoutNodes.get(ancestor)\n if (node && node.instance) return node\n ancestor = ancestor.parentElement\n }\n return undefined\n}\n\nfunction createVisualElement() {\n return new HTMLVisualElement(\n {\n props: {},\n presenceContext: null,\n visualState: {\n latestValues: {},\n renderState: {\n transform: {},\n transformOrigin: {},\n style: {},\n vars: {},\n },\n },\n },\n { allowProjection: true }\n )\n}\n\nfunction readNodeOptions(element: HTMLElement, transition?: Transition) {\n const layoutAttr = element.getAttribute(\"data-layout\")\n const layoutId = element.getAttribute(\"data-layout-id\") ?? undefined\n\n return {\n layoutId,\n layout: layoutAttr !== null ? true : undefined,\n animationType: (!layoutAttr || layoutAttr === \"true\"\n ? \"both\"\n : layoutAttr) as \"both\",\n