@replayablejs/pixi 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/integrations/register-pixi-devtools.ts","../src/lifecycle/cleanup-pixi-resources.ts","../src/integrations/setup-pixi-integrations.ts","../src/loader/configure-pixi-assets.ts","../src/loader/load-pixi-atlas.ts","../src/loader/load-pixi-sprite.ts","../src/renderer/destroy-pixi-renderer.ts","../src/renderer/create-pixi-renderer.ts","../src/renderer/start-pixi-rendering.ts","../src/renderer/synchronize-pixi-screen.ts","../src/create-pixi.ts","../src/factories/create-button.ts","../src/factories/apply-display-object-options.ts","../src/factories/create-animated-sprite.ts","../src/factories/create-nine-slice-sprite.ts","../src/factories/create-sprite.ts","../src/factories/create-split-text.ts","../src/factories/create-text.ts","../src/layout/debug/resolve-content-bounds.ts","../src/layout/debug/resolve-layout-debug-options.ts","../src/layout/debug/create-debug-layout.ts","../src/layout/debug/create-layout-debug-renderer.ts","../src/layout/layout-content.ts","../src/layout/layout-values.ts","../src/layout/resolve-layout.ts","../src/layout/create-layout.ts","../src/text/fit-text.ts"],"sourcesContent":["/* eslint-disable no-underscore-dangle -- Pixi DevTools requires these exact global names. */\n\nimport type { Container, WebGLRenderer } from 'pixi.js';\n\n/** Exposes a successfully initialized stage and renderer to Pixi DevTools in development. */\nexport function registerPixiDevtools(stage: Container, renderer: WebGLRenderer): () => void {\n if (!import.meta.env.DEV) {\n return noop;\n }\n\n // Replayable owns these separately, not through a PIXI.Application instance.\n globalThis.__PIXI_STAGE__ = stage;\n globalThis.__PIXI_RENDERER__ = renderer;\n\n return unregister;\n\n /** Releases only our references; an older instance must not clear a newer registration. */\n function unregister(): void {\n if (globalThis.__PIXI_STAGE__ === stage) {\n Reflect.deleteProperty(globalThis, '__PIXI_STAGE__');\n }\n if (globalThis.__PIXI_RENDERER__ === renderer) {\n Reflect.deleteProperty(globalThis, '__PIXI_RENDERER__');\n }\n }\n}\n\n/** Keeps lifecycle cleanup unconditional when production strips the DevTools branch. */\nfunction noop(): void {}\n","/** Releases acquired resources in reverse order, even when individual cleanup fails. */\nexport function cleanupPixiResources(cleanups: (() => void)[]): void {\n const errors = collectCleanupErrors(cleanups);\n\n if (errors.length === 1) {\n throw errors[0];\n }\n if (errors.length > 1) {\n throw new AggregateError(errors, 'Pixi resource cleanup failed.');\n }\n}\n\n/** Preserves the original setup failure alongside any rollback failures. */\nexport function failPixiSetup(cause: unknown, cleanups: (() => void)[]): never {\n const errors = collectCleanupErrors(cleanups);\n\n if (errors.length === 0) {\n throw cause;\n }\n throw new AggregateError([cause, ...errors], 'Pixi setup and cleanup failed.', { cause });\n}\n\n/** Pops before calling so repeated cleanup never retries already released resources. */\nfunction collectCleanupErrors(cleanups: (() => void)[]): unknown[] {\n const errors: unknown[] = [];\n let dispose = cleanups.pop();\n\n while (dispose !== undefined) {\n try {\n dispose();\n } catch (error) {\n errors.push(error);\n }\n dispose = cleanups.pop();\n }\n return errors;\n}\n","import type { PixiIntegration } from '#types/pixi.js';\n\n/** Installs integrations in declaration order and cleans them up in reverse order. */\nexport function setupPixiIntegrations(integrations: readonly PixiIntegration[] = []): () => void {\n const cleanups: (() => void)[] = [];\n\n try {\n for (const integration of integrations) {\n cleanups.push(integration.setup());\n }\n } catch (error) {\n failPixiSetup(error, cleanups);\n }\n\n return () => cleanupPixiResources(cleanups);\n}\nimport { cleanupPixiResources, failPixiSetup } from '#lifecycle/cleanup-pixi-resources.js';\n","import { Assets } from 'pixi.js';\n\n/** Applies deterministic browser-loading behavior for playable assets. */\nexport function configurePixiAssets(): void {\n Assets.setPreferences({\n // Playable exports must not depend on separately hosted worker scripts.\n preferWorkers: false,\n // Image elements behave more consistently across supported ad webviews.\n preferCreateImageBitmap: false,\n });\n}\n","import type { AssetLoadContext } from '@replayablejs/runtime';\nimport type { SpritesheetData, Texture } from 'pixi.js';\nimport { Assets, Spritesheet } from 'pixi.js';\n\n/** Loads one generated Replayable atlas and registers its parsed Pixi spritesheet. */\nexport async function loadPixiAtlas(context: AssetLoadContext<'atlases'>): Promise<Spritesheet> {\n const { id, source } = context;\n const [texture, data] = await Promise.all([\n Assets.load<Texture>(source.image),\n loadSpritesheetData(id, source.json),\n ]);\n const spritesheet = new Spritesheet({ data, texture });\n\n await spritesheet.parse();\n Assets.cache.set(id, spritesheet);\n\n return spritesheet;\n}\n\n/** Resolves either generated inline JSON or an emitted atlas JSON resource. */\nasync function loadSpritesheetData(\n id: string,\n source: AssetLoadContext<'atlases'>['source']['json'],\n): Promise<SpritesheetData> {\n const data: unknown =\n typeof source === 'string' ? await fetchSpritesheetData(id, source) : source;\n\n if (!isSpritesheetData(data)) {\n throw new Error(`Replayable atlas \"${id}\" contains invalid spritesheet data.`);\n }\n\n return data;\n}\n\n/** Fetches JSON only when the active asset mode emitted it as a resource URL. */\nasync function fetchSpritesheetData(id: string, url: string): Promise<unknown> {\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`Failed to load Replayable atlas \"${id}\" from ${url}.`);\n }\n\n return response.json();\n}\n\n/** Checks the required top-level shape before passing generated JSON to Pixi. */\nfunction isSpritesheetData(value: unknown): value is SpritesheetData {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n return 'frames' in value && 'meta' in value;\n}\n","import type { AssetLoadContext } from '@replayablejs/runtime';\nimport type { Texture } from 'pixi.js';\nimport { Assets } from 'pixi.js';\n\n/** Loads one generated Replayable sprite into Pixi's texture cache. */\nexport async function loadPixiSprite(context: AssetLoadContext<'sprites'>): Promise<Texture> {\n const { id, source } = context;\n const texture = await Assets.load<Texture>({\n alias: id,\n src: source.src,\n });\n\n // Asset processing may resize the physical image. Pixi resolution restores\n // its authored logical dimensions without changing the generated bitmap.\n texture.source.resolution = source.scale;\n texture.update();\n\n return texture;\n}\n","import type { WebGLRenderer } from 'pixi.js';\n\n/**\n * Releases Pixi resources without forcibly losing a borrowed WebGL context.\n *\n * Pixi 8.20's GlContextSystem.destroy() calls its cached loseContext extension;\n * removeView:false only preserves the canvas element. There is no preserve-context\n * destroy option. Temporarily remove that entry from this renderer's own cache,\n * not from the shared WebGL context or the extension object another renderer uses.\n * Recheck this compatibility boundary when upgrading Pixi.\n */\nexport function destroyPixiRenderer(renderer: WebGLRenderer, ownsContext: boolean): void {\n // Context systems may not exist yet if asynchronous initialization failed early.\n const extensions = renderer.context?.extensions;\n const loseContext = extensions?.loseContext;\n\n if (!ownsContext && extensions !== undefined) {\n delete extensions.loseContext;\n }\n\n try {\n renderer.destroy({ removeView: false });\n } finally {\n if (!ownsContext && extensions !== undefined && loseContext !== undefined) {\n extensions.loseContext = loseContext;\n }\n }\n}\n","import { getCanvasHost, type SharedRenderingContext } from '@replayablejs/canvas';\nimport { playable } from '@replayablejs/runtime';\nimport { WebGLRenderer } from 'pixi.js';\n\nimport { cleanupPixiResources, failPixiSetup } from '#lifecycle/cleanup-pixi-resources.js';\nimport type { CreatePixiOptions, PixiRendererResult } from '#types/pixi.js';\n\nimport { destroyPixiRenderer } from './destroy-pixi-renderer.js';\n\n/** Creates Pixi's renderer on Replayable's canvas and shared WebGL context. */\nexport async function createPixiRenderer(options: CreatePixiOptions): Promise<PixiRendererResult> {\n const canvasHost = getCanvasHost();\n const sharedContext = resolveSharedContext(canvasHost.getSharedContext());\n const ownsContext = sharedContext === null;\n const cleanups: (() => void)[] = [];\n\n if (ownsContext) {\n // Capture this host, never resolve the singleton again during destruction.\n cleanups.push(() => canvasHost.destroy());\n }\n\n try {\n const renderer = new WebGLRenderer();\n cleanups.push(() => destroyPixiRenderer(renderer, ownsContext));\n\n await renderer.init({\n antialias: options.antialias ?? false,\n autoDensity: false,\n backgroundAlpha: ownsContext ? 1 : 0,\n backgroundColor: playable.config.backgroundColor,\n canvas: canvasHost.getCanvas(),\n clearBeforeRender: ownsContext,\n context: sharedContext,\n hello: false,\n powerPreference: options.powerPreference ?? 'high-performance',\n preferWebGLVersion: 2,\n useBackBuffer: options.useBackBuffer ?? false,\n });\n\n if (ownsContext) {\n canvasHost.setSharedContext(renderer.gl);\n }\n\n return { renderer, destroy: () => cleanupPixiResources(cleanups) };\n } catch (error) {\n return failPixiSetup(error, cleanups);\n }\n}\n\n/** Pixi 8 accepts only WebGL 2 when reusing an externally created context. */\nfunction resolveSharedContext(\n context: SharedRenderingContext | null,\n): WebGL2RenderingContext | null {\n if (\n context === null ||\n (typeof WebGL2RenderingContext !== 'undefined' && context instanceof WebGL2RenderingContext)\n ) {\n return context;\n }\n\n throw new Error('Pixi requires the shared Replayable canvas context to use WebGL 2.');\n}\n","import { playable, type UpdateContext } from '@replayablejs/runtime';\nimport { Ticker, type Container, type WebGLRenderer } from 'pixi.js';\n\nconst MILLISECONDS_PER_SECOND = 1000;\n\n/** Connects Pixi's ticker and rendering to Replayable's lifecycle-aware frame loop. */\nexport function startPixiRendering(renderer: WebGLRenderer, stage: Container): () => void {\n const ticker = Ticker.shared;\n let elapsedMilliseconds = 0;\n\n // Pixi systems may register work on the shared ticker, but Replayable alone\n // owns browser-frame scheduling so Pixi must never request another RAF loop.\n ticker.autoStart = false;\n ticker.stop();\n ticker.lastTime = 0;\n\n return playable.update.add(renderFrame);\n\n /** Advances Pixi systems and renders the stage once for this Replayable frame. */\n function renderFrame({ deltaSeconds }: UpdateContext): void {\n elapsedMilliseconds += deltaSeconds * MILLISECONDS_PER_SECOND;\n\n renderer.resetState();\n ticker.update(elapsedMilliseconds);\n renderer.render(stage);\n }\n}\n","import { playable } from '@replayablejs/runtime';\nimport type { WebGLRenderer } from 'pixi.js';\n\n/** Keeps Pixi's drawing buffer aligned with Replayable's screen. */\nexport function synchronizePixiScreen(renderer: WebGLRenderer): () => void {\n const unsubscribe = playable.on('resize', applyScreen);\n\n try {\n // Renderer initialization is asynchronous. The initial resize may have\n // happened while it was awaiting Pixi; catch up without starting readiness.\n if (playable.screen.frame !== undefined) {\n applyScreen();\n }\n } catch (error) {\n unsubscribe();\n throw error;\n }\n\n return unsubscribe;\n\n function applyScreen(): void {\n const { frame, resolution } = playable.screen;\n\n renderer.resolution = resolution;\n renderer.resize(frame.width, frame.height);\n }\n}\n","import { playable } from '@replayablejs/runtime';\nimport { Container } from 'pixi.js';\n\nimport { registerPixiDevtools } from '#integrations/register-pixi-devtools.js';\nimport { setupPixiIntegrations } from '#integrations/setup-pixi-integrations.js';\nimport { cleanupPixiResources, failPixiSetup } from '#lifecycle/cleanup-pixi-resources.js';\nimport { configurePixiAssets } from '#loader/configure-pixi-assets.js';\nimport { loadPixiAtlas } from '#loader/load-pixi-atlas.js';\nimport { loadPixiSprite } from '#loader/load-pixi-sprite.js';\nimport { createPixiRenderer } from '#renderer/create-pixi-renderer.js';\nimport { startPixiRendering } from '#renderer/start-pixi-rendering.js';\nimport { synchronizePixiScreen } from '#renderer/synchronize-pixi-screen.js';\nimport type { CreatePixiOptions, ReplayablePixi } from '#types/pixi.js';\n\n/** Initializes Pixi around Replayable's assets, lifecycle, and shared canvas. */\nexport async function createPixi(options: CreatePixiOptions = {}): Promise<ReplayablePixi> {\n configurePixiAssets();\n\n const cleanups: (() => void)[] = [];\n\n try {\n // Register before the first await, so readiness can load primary assets.\n // Record each release immediately: any later acquisition may fail.\n cleanups.push(playable.loader.register('atlases', loadPixiAtlas));\n cleanups.push(playable.loader.register('sprites', loadPixiSprite));\n cleanups.push(setupPixiIntegrations(options.integrations));\n\n const { renderer, destroy } = await createPixiRenderer(options);\n cleanups.push(destroy);\n\n // Acquire the stage after the renderer so reverse cleanup destroys scene\n // objects while their renderer is still alive.\n const stage = new Container();\n cleanups.push(() => stage.destroy({ children: true }));\n\n const stopScreenSynchronization = synchronizePixiScreen(renderer);\n cleanups.push(stopScreenSynchronization);\n\n const stopRendering = startPixiRendering(renderer, stage);\n cleanups.push(stopRendering);\n\n const unregisterDevtools = registerPixiDevtools(stage, renderer);\n cleanups.push(unregisterDevtools);\n\n return {\n renderer,\n stage,\n\n destroy(): void {\n // Draining the stack makes destruction idempotent, even after an error.\n cleanupPixiResources(cleanups);\n },\n };\n } catch (error) {\n return failPixiSetup(error, cleanups);\n }\n}\n","import { Container, Rectangle, type FederatedPointerEvent } from 'pixi.js';\n\nimport type { CreateButtonOptions, ReplayableButton } from '#types/button.js';\n\n/**\n * Wraps artwork in a stable hit target without choosing its visuals or action.\n * Children are non-interactive: the button owns completed taps for the whole artwork.\n * Bounds are captured once, including the content's initial transform. Later artwork\n * animations do not shrink the hit target or change the bounds consumed by layout.\n *\n * @example\n * const button = createButton({ content: artwork, onActivate: handleAction });\n * stage.addChild(button.container);\n * button.setEnabled(false);\n * // Disposing the button also destroys artwork, but not its shared textures.\n * button.destroy();\n */\nexport function createButton(options: CreateButtonOptions): ReplayableButton {\n const container = new Container({ label: 'button' });\n const { content, onActivate } = options;\n content.eventMode = 'none';\n container.addChild(content);\n\n const bounds = container.getLocalBounds();\n const buttonBounds = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);\n container.boundsArea = buttonBounds;\n container.hitArea = buttonBounds;\n\n let enabled = options.enabled ?? true;\n setEnabled(enabled);\n container.on('pointertap', handleTap);\n\n return { container, setEnabled, destroy };\n\n /** Keep the action in the trusted input call stack; never await or defer it. */\n function handleTap(event: FederatedPointerEvent): void {\n event.stopPropagation();\n if (enabled) {\n onActivate();\n }\n }\n\n /** Change input policy only; the consumer owns visibility and disabled styling. */\n function setEnabled(value: boolean): void {\n if (container.destroyed) {\n return;\n }\n enabled = value;\n container.eventMode = enabled ? 'static' : 'none';\n container.cursor = enabled ? 'pointer' : 'default';\n }\n\n /** Retire input before destroying owned display objects, never shared textures. */\n function destroy(): void {\n if (container.destroyed) {\n return;\n }\n setEnabled(false);\n container.off('pointertap', handleTap);\n container.destroy({ children: true });\n }\n}\n","import type { AnchorableDisplayObject, AnchorableDisplayObjectOptions } from '#types/factories.js';\n\nimport { applyContainerOptions } from './apply-container-options.js';\n\nconst CENTER_ANCHOR = { x: 0.5, y: 0.5 };\n\n/** Applies the defaults shared by every Replayable Pixi factory. */\nexport function applyDisplayObjectOptions(\n displayObject: AnchorableDisplayObject,\n options: AnchorableDisplayObjectOptions,\n): void {\n applyContainerOptions(displayObject, options);\n displayObject.anchor.copyFrom(options.anchor ?? CENTER_ANCHOR);\n}\n","import { AnimatedSprite, Texture } from 'pixi.js';\n\nimport type { CreateAnimatedSpriteOptions } from '#types/factories.js';\n\nimport { applyDisplayObjectOptions } from './apply-display-object-options.js';\n\n/** Creates an unattached animated sprite driven by Pixi's Replayable-managed ticker. */\nexport function createAnimatedSprite(options: CreateAnimatedSpriteOptions): AnimatedSprite {\n const { animationSpeed = 1, autoPlay = false, frames, loop = false } = options;\n\n if (frames.length === 0) {\n throw new Error('Cannot create an animated sprite without texture frames.');\n }\n\n const textures = frames.map((frame) => (typeof frame === 'string' ? Texture.from(frame) : frame));\n const sprite = new AnimatedSprite(textures);\n\n applyDisplayObjectOptions(sprite, options);\n sprite.animationSpeed = animationSpeed;\n sprite.loop = loop;\n\n if (autoPlay) {\n sprite.play();\n }\n\n return sprite;\n}\n","import { NineSliceSprite, Texture } from 'pixi.js';\n\nimport type { CreateNineSliceSpriteOptions } from '#types/factories.js';\n\nimport { applyDisplayObjectOptions } from './apply-display-object-options.js';\n\n/** Creates an unattached nine-slice sprite with explicitly named borders. */\nexport function createNineSliceSprite(options: CreateNineSliceSpriteOptions): NineSliceSprite {\n const { bottomHeight, height, leftWidth, rightWidth, texture, topHeight, width } = options;\n const sprite = new NineSliceSprite({\n bottomHeight,\n height,\n leftWidth,\n rightWidth,\n texture: typeof texture === 'string' ? Texture.from(texture) : texture,\n topHeight,\n width,\n });\n\n applyDisplayObjectOptions(sprite, options);\n\n return sprite;\n}\n","import { Sprite, Texture } from 'pixi.js';\n\nimport type { CreateSpriteOptions } from '#types/factories.js';\n\nimport { applyDisplayObjectOptions } from './apply-display-object-options.js';\n\n/** Creates an unattached Pixi sprite with Replayable's conventional defaults. */\nexport function createSprite(options: CreateSpriteOptions = {}): Sprite {\n const sprite = Sprite.from(options.texture ?? Texture.EMPTY);\n\n applyDisplayObjectOptions(sprite, options);\n\n if (options.tint !== undefined) {\n sprite.tint = options.tint;\n }\n\n if (options.eventMode !== undefined) {\n sprite.eventMode = options.eventMode;\n }\n\n return sprite;\n}\n","import { SplitText } from 'pixi.js';\n\nimport type { CreateSplitTextOptions } from '#types/factories.js';\n\nimport { applyContainerOptions } from './apply-container-options.js';\n\n/**\n * Creates unattached split text with Pixi's native splitting options.\n * Unlike Text, SplitText is a container, so it has no shared anchor default.\n * With autoSplit: false, call split() explicitly before accessing the characters.\n * Fitting, localization, and character animation remain application concerns.\n */\nexport function createSplitText(options: CreateSplitTextOptions): SplitText {\n const textObject = new SplitText(options);\n\n applyContainerOptions(textObject, options);\n\n return textObject;\n}\n","import { Text } from 'pixi.js';\n\nimport type { CreateTextOptions } from '#types/factories.js';\n\nimport { applyDisplayObjectOptions } from './apply-display-object-options.js';\n\n/** Creates unattached Pixi canvas text while leaving content and layout to the application. */\nexport function createText(options: CreateTextOptions): Text {\n const textObject = new Text({ style: options.style, text: options.text });\n\n applyDisplayObjectOptions(textObject, options);\n\n return textObject;\n}\n","import type { Container } from 'pixi.js';\n\nimport type { LayoutBounds } from '#types/layout.js';\n\n/**\n * Measures the axis-aligned box produced by Replayable's applied placement.\n * Rotation and skew remain excluded because core layout deliberately fits the\n * attached object's untransformed local bounds.\n *\n * The object has already been laid out when this function runs. Adding its\n * position to both pivot-adjusted, scaled edges therefore reconstructs the\n * exact box used by core placement without adding debug output to core logic.\n * Measuring both edges preserves flipped objects whose scale is negative.\n */\nexport function resolveContentBounds(content: Container): LayoutBounds {\n const bounds = content.getLocalBounds();\n const firstX = content.x + (bounds.x - content.pivot.x) * content.scale.x;\n const secondX = content.x + (bounds.x + bounds.width - content.pivot.x) * content.scale.x;\n const firstY = content.y + (bounds.y - content.pivot.y) * content.scale.y;\n const secondY = content.y + (bounds.y + bounds.height - content.pivot.y) * content.scale.y;\n\n return {\n x: Math.min(firstX, secondX),\n y: Math.min(firstY, secondY),\n width: Math.abs(secondX - firstX),\n height: Math.abs(secondY - firstY),\n };\n}\n","import type { ResolvedLayoutDebugLabels, ResolvedLayoutDebugOptions } from '#types/layout-debug.js';\nimport type { LayoutConfig, LayoutDebugOptions } from '#types/layout.js';\n\nconst allLabels: ResolvedLayoutDebugLabels = {\n areas: true,\n content: true,\n layout: true,\n};\n\n/** Canonical defaults shared by boolean shorthand and selective option objects. */\nconst allDiagnostics: ResolvedLayoutDebugOptions = {\n areaBounds: true,\n contentBounds: true,\n labels: allLabels,\n layoutBounds: true,\n};\n\n/**\n * Resolves the public debug shorthand into the complete internal configuration.\n *\n * `true` enables every diagnostic. An options object starts from the same set\n * and can selectively disable individual diagnostics. `false` and omission\n * both disable the overlay.\n */\nexport function resolveLayoutDebugOptions(\n debug: LayoutConfig['debug'],\n): ResolvedLayoutDebugOptions | undefined {\n if (debug === undefined || debug === false) {\n return undefined;\n }\n\n if (debug === true) {\n return allDiagnostics;\n }\n\n return {\n ...allDiagnostics,\n ...debug,\n labels: resolveLabels(debug.labels),\n };\n}\n\n/** Resolves the label shorthand independently from the other debug switches. */\nfunction resolveLabels(labels: LayoutDebugOptions['labels']): ResolvedLayoutDebugLabels {\n if (labels === false) {\n return { areas: false, content: false, layout: false };\n }\n\n if (labels === undefined || labels === true) {\n return allLabels;\n }\n\n return { ...allLabels, ...labels };\n}\n","import type { Container } from 'pixi.js';\n\nimport type { DebugLayoutArea, LayoutDebugRenderer } from '#types/layout-debug.js';\nimport type {\n LayoutAttachment,\n LayoutConfig,\n ReplayableLayout,\n ResolvedLayoutArea,\n} from '#types/layout.js';\n\nimport { resolveContentBounds } from './resolve-content-bounds.js';\nimport { resolveLayoutDebugOptions } from './resolve-layout-debug-options.js';\n\n/**\n * Wraps a layout with the development boundary used by diagnostics.\n *\n * Delegation remains intentionally transparent: successful operations keep\n * their normal behavior and failures escape unchanged. Diagnostic state\n * therefore changes only after a delegated mutation has completed.\n */\nexport function createDebugLayout(\n layout: ReplayableLayout,\n initialConfig: LayoutConfig,\n renderer: LayoutDebugRenderer,\n): ReplayableLayout {\n const attachments = new Map<Container, LayoutAttachment>();\n let config = initialConfig;\n let destroyed = false;\n\n renderInspection();\n\n return {\n container: layout.container,\n\n /** Delegates reads directly; the decorator never owns resolved area state. */\n getArea(name): ResolvedLayoutArea | undefined {\n return layout.getArea(name);\n },\n\n /**\n * Delegates attachment first, then mirrors ownership for diagnostics.\n *\n * This order is essential: if core validation or measurement throws, the\n * debugger records nothing and emits no misleading redraw. The additional\n * destruction listener updates the overlay when application code destroys\n * attached content without calling `detach()`.\n */\n attach(areaName, content): void {\n layout.attach(areaName, content);\n\n /** Mirrors core cleanup and immediately removes the stale diagnostic box. */\n const handleDestroyed = (): void => {\n attachments.delete(content);\n renderInspection();\n };\n\n content.once('destroyed', handleDestroyed);\n attachments.set(content, { areaName, handleDestroyed });\n renderInspection();\n },\n\n /** Mirrors a new area name only after the core move has succeeded. */\n move(content, areaName): void {\n layout.move(content, areaName);\n const attachment = requireAttachment(content);\n\n attachments.set(content, { ...attachment, areaName });\n renderInspection();\n },\n\n /**\n * Stops diagnostic tracking after core ownership is successfully released.\n * The exact registered callback is removed to avoid retaining the decorator.\n */\n detach(content): void {\n layout.detach(content);\n const attachment = requireAttachment(content);\n\n content.off('destroyed', attachment.handleDestroyed);\n attachments.delete(content);\n renderInspection();\n },\n\n /**\n * Adopts new debug configuration only after the atomic core update succeeds.\n * This keeps the drawn inspection synchronized with actual live transforms.\n */\n update(nextConfig): void {\n layout.update(nextConfig);\n config = nextConfig;\n renderInspection();\n },\n\n /**\n * Idempotently releases decorator listeners and renderer resources before\n * delegating destruction to the core layout and its owned container.\n */\n destroy(): void {\n if (destroyed) {\n return;\n }\n\n destroyed = true;\n\n for (const [content, attachment] of attachments) {\n content.off('destroyed', attachment.handleDestroyed);\n }\n\n attachments.clear();\n renderer.destroy();\n layout.destroy();\n },\n } satisfies ReplayableLayout;\n\n /**\n * Resolves public debug shorthand and sends one complete renderer snapshot.\n * `undefined` is an explicit request to remove the overlay. Content geometry\n * is not measured when its corresponding diagnostic is disabled.\n */\n function renderInspection(): void {\n const options = resolveLayoutDebugOptions(config.debug);\n\n if (options === undefined) {\n renderer.render(undefined);\n return;\n }\n\n renderer.render({\n areas: Object.keys(config.areas).map((name) =>\n inspectArea(name, options.contentBounds || options.labels.content),\n ),\n bounds: config.bounds,\n options,\n });\n }\n\n /**\n * Combines authoritative core area geometry with decorator-owned occupancy.\n *\n * Attached content is filtered in map insertion order, so multiple diagnostic\n * boxes follow deterministic attachment order without exposing the map itself.\n */\n function inspectArea(name: string, includeContentBounds: boolean): DebugLayoutArea {\n const area = layout.getArea(name);\n\n if (area === undefined) {\n throw new Error(`Debug layout could not inspect area \"${name}\".`);\n }\n\n const areaAttachments = [...attachments].filter(\n ([, attachment]) => attachment.areaName === name,\n );\n\n return {\n ...area,\n contentBounds: includeContentBounds\n ? areaAttachments.map(([content]) => resolveContentBounds(content))\n : [],\n occupied: areaAttachments.length > 0,\n };\n }\n\n /** Guards the invariant that every successful core attachment was mirrored. */\n function requireAttachment(content: Container): LayoutAttachment {\n const attachment = attachments.get(content);\n\n if (attachment === undefined) {\n throw new Error('Debug layout lost track of attached content.');\n }\n\n return attachment;\n }\n}\n","import { Container, Graphics, Text } from 'pixi.js';\n\nimport type { DebugLayoutInspection, LayoutDebugRenderer } from '#types/layout-debug.js';\nimport type { LayoutBounds } from '#types/layout.js';\n\nconst AREA_EMPTY_COLOR = '#f2b84b';\nconst AREA_OCCUPIED_COLOR = '#54d17a';\nconst CONTENT_BOUNDS_COLOR = '#ef5cdb';\nconst LAYOUT_BOUNDS_COLOR = '#45d9ef';\nconst LINE_WIDTH = 2;\n\n/**\n * Creates the visual half of layout diagnostics.\n *\n * This renderer knows nothing about layout mutations or attached content. It\n * simply redraws the latest read-only inspection supplied by the decorator.\n * The overlay is a single non-interactive, non-measurable child of the layout\n * container, so diagnostics share its coordinate space without affecting\n * pointer handling or bounds used by a parent layout.\n */\nexport function createLayoutDebugRenderer(container: Container): LayoutDebugRenderer {\n const overlay = new Container();\n const outlines = new Graphics();\n const labels: Text[] = [];\n\n overlay.eventMode = 'none';\n overlay.label = 'Replayable layout debugger';\n // Diagnostics must never change the bounds used when this layout is itself\n // attached as content to another layout.\n overlay.measurable = false;\n overlay.addChild(outlines);\n\n return { destroy, render };\n\n /**\n * Replaces the complete overlay with one current inspection.\n *\n * Layout mutations are infrequent, so rebuilding labels is simpler and safer\n * than reconciling display objects. Passing `undefined` removes the overlay\n * entirely, leaving disabled diagnostics with no hidden scene-graph child.\n */\n function render(inspection: DebugLayoutInspection | undefined): void {\n clearOverlay();\n\n if (inspection === undefined) {\n if (overlay.parent === container) {\n container.removeChild(overlay);\n }\n\n return;\n }\n\n const { areas, bounds, options } = inspection;\n\n if (options.layoutBounds) {\n drawBounds(bounds, LAYOUT_BOUNDS_COLOR);\n }\n\n if (options.labels.layout) {\n drawLabel(bounds, LAYOUT_BOUNDS_COLOR);\n }\n\n for (const area of areas) {\n if (options.areaBounds) {\n drawBounds(area.bounds, area.occupied ? AREA_OCCUPIED_COLOR : AREA_EMPTY_COLOR);\n }\n\n if (options.contentBounds) {\n for (const contentBounds of area.contentBounds) {\n drawBounds(contentBounds, CONTENT_BOUNDS_COLOR);\n }\n }\n\n if (options.labels.areas) {\n drawLabel(area.bounds, area.occupied ? AREA_OCCUPIED_COLOR : AREA_EMPTY_COLOR);\n }\n\n if (options.labels.content) {\n for (const contentBounds of area.contentBounds) {\n drawLabel(contentBounds, CONTENT_BOUNDS_COLOR, 'bottom-right');\n }\n }\n }\n\n // Attaching content adds it after the overlay. Move diagnostics back to the\n // top after every mutation without enabling sortable children on the app.\n container.addChild(overlay);\n }\n\n /**\n * Releases text, geometry, and container resources owned by this renderer.\n * The decorator guarantees this is called once before core layout destruction.\n */\n function destroy(): void {\n clearOverlay();\n overlay.removeFromParent();\n overlay.destroy({ children: true });\n }\n\n /** Clears retained vector commands and destroys labels from the previous redraw. */\n function clearOverlay(): void {\n outlines.clear();\n\n for (const label of labels) {\n label.destroy();\n }\n\n labels.length = 0;\n }\n\n /**\n * Appends one pixel-aligned rectangle to the shared Graphics command list.\n * `pixelLine` keeps diagnostic edges crisp across renderer resolutions.\n */\n function drawBounds(bounds: LayoutBounds, color: string): void {\n outlines.rect(bounds.x, bounds.y, bounds.width, bounds.height).stroke({\n color,\n pixelLine: true,\n width: LINE_WIDTH,\n });\n }\n\n /**\n * Creates a compact label inside a diagnostic rectangle.\n * The label reuses its rectangle's diagnostic color, while a dark stroke\n * preserves readability over arbitrary playable artwork.\n */\n function drawLabel(\n bounds: LayoutBounds,\n color: string,\n placement: 'top-left' | 'bottom-right' = 'top-left',\n ): void {\n const { height, width, x, y } = bounds;\n const label = new Text({\n text: `${formatDimension(width)} × ${formatDimension(height)}`,\n style: {\n fill: color,\n fontFamily: 'monospace',\n fontSize: 8,\n stroke: { color: '#000000', width: 2 },\n },\n });\n\n label.eventMode = 'none';\n\n if (placement === 'bottom-right') {\n label.anchor.set(1, 1);\n label.position.set(x + width - 4, y + height - 3);\n } else {\n label.position.set(x + 4, y + 3);\n }\n\n labels.push(label);\n overlay.addChild(label);\n }\n}\n\n/** Keeps integer dimensions compact while making fractional pixels explicit. */\nfunction formatDimension(value: number): string {\n return Number.isInteger(value) ? String(value) : value.toFixed(1);\n}\n","import type { Container } from 'pixi.js';\n\nimport type {\n ContentLayout,\n LayoutAlignment,\n LayoutBounds,\n LayoutScaleMode,\n ResolvedLayoutArea,\n} from '#types/layout.js';\n\n/**\n * Resolves one content placement without mutating its live Pixi transform.\n *\n * Pixi local bounds may begin away from `(0, 0)`, and pivot moves the object's\n * transform origin independently from those bounds. This calculation first\n * determines the final scale, transforms both local edges around the pivot,\n * and finally derives the position that puts the resulting visual box at the\n * area's requested alignment and pixel offset.\n *\n * Rotation and skew are intentionally excluded. Applications that need those\n * transforms to participate in fitting should attach an untransformed parent\n * container and rotate or skew its child.\n */\nexport function resolveContentLayout(content: Container, area: ResolvedLayoutArea): ContentLayout {\n const contentBounds = content.getLocalBounds();\n const { scaleX, scaleY } = resolveScale(\n area.scale,\n area.bounds,\n contentBounds,\n content.scale.x,\n content.scale.y,\n );\n\n // Local bounds exclude the object's own pivot and scale. Transform both\n // edges so preserved negative scale still produces the correct visual box.\n const firstX = (contentBounds.x - content.pivot.x) * scaleX;\n const secondX = (contentBounds.x + contentBounds.width - content.pivot.x) * scaleX;\n const firstY = (contentBounds.y - content.pivot.y) * scaleY;\n const secondY = (contentBounds.y + contentBounds.height - content.pivot.y) * scaleY;\n const minimumX = Math.min(firstX, secondX);\n const minimumY = Math.min(firstY, secondY);\n const contentWidth = Math.abs(secondX - firstX);\n const contentHeight = Math.abs(secondY - firstY);\n\n return {\n x: resolveAlignedX(area.align, area.bounds, contentWidth) - minimumX + area.offset.x,\n y: resolveAlignedY(area.align, area.bounds, contentHeight) - minimumY + area.offset.y,\n scaleX,\n scaleY,\n };\n}\n\n/**\n * Commits a previously resolved placement to Pixi.\n *\n * Resolution is separated from mutation so attach, move, and configuration\n * updates can validate every placement before changing live scene state. Scale\n * is applied before position only for conceptual consistency; both values were\n * fully calculated beforehand and neither setter is expected to fail.\n */\nexport function applyContentLayout(content: Container, layout: ContentLayout): void {\n content.scale.set(layout.scaleX, layout.scaleY);\n content.position.set(layout.x, layout.y);\n}\n\n/**\n * Resolves the final scale for one area's scaling policy.\n *\n * `none` preserves the object's authored scale, including negative values used\n * for flipping. Other modes derive positive scales from unscaled local bounds:\n * `fit` only shrinks, `contain` may shrink or grow while remaining entirely\n * visible, `cover` fills and may crop, and `stretch` scales each axis alone.\n */\nfunction resolveScale(\n mode: LayoutScaleMode,\n area: LayoutBounds,\n content: LayoutBounds,\n currentScaleX: number,\n currentScaleY: number,\n): { readonly scaleX: number; readonly scaleY: number } {\n if (mode === 'none') {\n return { scaleX: currentScaleX, scaleY: currentScaleY };\n }\n\n if (content.width === 0 || content.height === 0) {\n throw new Error(`Cannot apply layout scale mode \"${mode}\" to zero-sized content.`);\n }\n\n const widthScale = area.width / content.width;\n const heightScale = area.height / content.height;\n\n switch (mode) {\n case 'fit': {\n const scale = Math.min(1, widthScale, heightScale);\n return { scaleX: scale, scaleY: scale };\n }\n case 'contain': {\n const scale = Math.min(widthScale, heightScale);\n return { scaleX: scale, scaleY: scale };\n }\n case 'cover': {\n const scale = Math.max(widthScale, heightScale);\n return { scaleX: scale, scaleY: scale };\n }\n case 'stretch':\n return { scaleX: widthScale, scaleY: heightScale };\n default:\n throw new Error('Unknown layout scale mode.');\n }\n}\n\n/** Places the scaled visual width against the horizontal component of alignment. */\nfunction resolveAlignedX(\n alignment: LayoutAlignment,\n area: LayoutBounds,\n contentWidth: number,\n): number {\n switch (alignment) {\n case 'top-left':\n case 'center-left':\n case 'bottom-left':\n return area.x;\n case 'top-center':\n case 'center':\n case 'bottom-center':\n return area.x + (area.width - contentWidth) / 2;\n case 'top-right':\n case 'center-right':\n case 'bottom-right':\n return area.x + area.width - contentWidth;\n default:\n throw new Error('Unknown layout alignment.');\n }\n}\n\n/** Places the scaled visual height against the vertical component of alignment. */\nfunction resolveAlignedY(\n alignment: LayoutAlignment,\n area: LayoutBounds,\n contentHeight: number,\n): number {\n switch (alignment) {\n case 'top-left':\n case 'top-center':\n case 'top-right':\n return area.y;\n case 'center-left':\n case 'center':\n case 'center-right':\n return area.y + (area.height - contentHeight) / 2;\n case 'bottom-left':\n case 'bottom-center':\n case 'bottom-right':\n return area.y + area.height - contentHeight;\n default:\n throw new Error('Unknown layout alignment.');\n }\n}\n","import type { LayoutAlignment, LayoutScaleMode } from '#types/layout.js';\n\n/** Supported positions of content inside one resolved layout area. */\nexport const layoutAlignments = [\n 'top-left',\n 'top-center',\n 'top-right',\n 'center-left',\n 'center',\n 'center-right',\n 'bottom-left',\n 'bottom-center',\n 'bottom-right',\n] as const satisfies readonly LayoutAlignment[];\n\n/** Supported ways to scale content relative to one resolved layout area. */\nexport const layoutScaleModes = [\n 'none',\n 'fit',\n 'contain',\n 'cover',\n 'stretch',\n] as const satisfies readonly LayoutScaleMode[];\n\n/** Placement used when an area does not explicitly choose an alignment. */\nexport const DEFAULT_LAYOUT_ALIGNMENT = 'center' satisfies LayoutAlignment;\n\n/** Safe scaling default: shrink oversized content without unexpectedly enlarging it. */\nexport const DEFAULT_LAYOUT_SCALE_MODE = 'fit' satisfies LayoutScaleMode;\n","import type {\n LayoutAreaConfig,\n LayoutBounds,\n LayoutConfig,\n ResolvedLayoutArea,\n} from '#types/layout.js';\n\nimport {\n DEFAULT_LAYOUT_ALIGNMENT,\n DEFAULT_LAYOUT_SCALE_MODE,\n layoutAlignments,\n layoutScaleModes,\n} from './layout-values.js';\n\n/** Shared read-only fallback copied into each resolved area's immutable offset. */\nconst ZERO_OFFSET = { x: 0, y: 0 };\n\n/**\n * Validates and resolves all authored areas into layout-local pixel coordinates.\n *\n * Area bounds are normalized fractions relative to `config.bounds`; the outer\n * layout bounds themselves are already expressed in pixels. Each result is a\n * newly frozen snapshot so consumers cannot mutate controller state through\n * `getArea()`. The returned map is replaced wholesale during layout updates.\n */\nexport function resolveLayout(config: LayoutConfig): ReadonlyMap<string, ResolvedLayoutArea> {\n validateLayoutBounds(config.bounds);\n\n const areas = new Map<string, ResolvedLayoutArea>();\n\n for (const [name, area] of Object.entries(config.areas)) {\n if (name.trim().length === 0) {\n throw new Error('Layout area names must not be empty.');\n }\n\n areas.set(name, resolveLayoutArea(name, area, config.bounds));\n }\n\n return areas;\n}\n\n/**\n * Resolves one normalized area without applying its content offset.\n *\n * `bounds` describes the area's rectangle. `offset` belongs to content\n * placement inside that rectangle, so adding it here would incorrectly move\n * the debug rectangle and change what normalized area definitions mean.\n */\nfunction resolveLayoutArea(\n name: string,\n config: LayoutAreaConfig,\n layoutBounds: LayoutBounds,\n): ResolvedLayoutArea {\n validateAreaBounds(name, config.bounds);\n const offset = config.offset ?? ZERO_OFFSET;\n\n validateFiniteNumber(offset.x, `Layout area \"${name}\" offset.x`);\n validateFiniteNumber(offset.y, `Layout area \"${name}\" offset.y`);\n validatePlacementValues(name, config);\n\n return Object.freeze({\n name,\n bounds: Object.freeze({\n x: layoutBounds.x + config.bounds.x * layoutBounds.width,\n y: layoutBounds.y + config.bounds.y * layoutBounds.height,\n width: config.bounds.width * layoutBounds.width,\n height: config.bounds.height * layoutBounds.height,\n }),\n align: config.align ?? DEFAULT_LAYOUT_ALIGNMENT,\n scale: config.scale ?? DEFAULT_LAYOUT_SCALE_MODE,\n offset: Object.freeze({ x: offset.x, y: offset.y }),\n });\n}\n\n/** Rejects runtime strings outside the unions even when untyped JavaScript supplied them. */\nfunction validatePlacementValues(name: string, config: LayoutAreaConfig): void {\n if (config.align !== undefined && !layoutAlignments.includes(config.align)) {\n throw new Error(`Layout area \"${name}\" has an unknown alignment: ${config.align}.`);\n }\n\n if (config.scale !== undefined && !layoutScaleModes.includes(config.scale)) {\n throw new Error(`Layout area \"${name}\" has an unknown scale mode: ${config.scale}.`);\n }\n}\n\n/** Ensures the coordinate space itself can resolve normalized rectangles. */\nfunction validateLayoutBounds(bounds: LayoutBounds): void {\n validateBounds('Layout bounds', bounds);\n\n if (bounds.width <= 0 || bounds.height <= 0) {\n throw new Error('Layout bounds width and height must be greater than zero.');\n }\n}\n\n/** Allows empty areas but rejects negative sizes that invert their geometry. */\nfunction validateAreaBounds(name: string, bounds: LayoutBounds): void {\n validateBounds(`Layout area \"${name}\" bounds`, bounds);\n\n if (bounds.width < 0 || bounds.height < 0) {\n throw new Error(`Layout area \"${name}\" width and height must not be negative.`);\n }\n}\n\n/** Validates the four numeric components shared by layout and area rectangles. */\nfunction validateBounds(label: string, bounds: LayoutBounds): void {\n validateFiniteNumber(bounds.x, `${label}.x`);\n validateFiniteNumber(bounds.y, `${label}.y`);\n validateFiniteNumber(bounds.width, `${label}.width`);\n validateFiniteNumber(bounds.height, `${label}.height`);\n}\n\n/** Rejects `NaN` and infinities before they can poison Pixi transforms. */\nfunction validateFiniteNumber(value: number, label: string): void {\n if (!Number.isFinite(value)) {\n throw new Error(`${label} must be a finite number.`);\n }\n}\n","import { Container } from 'pixi.js';\n\nimport type {\n LayoutAttachment,\n LayoutConfig,\n ReplayableLayout,\n ResolvedLayoutArea,\n} from '#types/layout.js';\n\nimport { createDebugLayout } from './debug/create-debug-layout.js';\nimport { createLayoutDebugRenderer } from './debug/create-layout-debug-renderer.js';\nimport { applyContentLayout, resolveContentLayout } from './layout-content.js';\nimport { resolveLayout } from './resolve-layout.js';\n\n/**\n * Creates a named-area layout whose container can be mounted on any Pixi stage.\n *\n * @example Fitting rotated content through an explicit layout root\n * ```ts\n * const layoutRoot = new Container();\n * const image = createSprite({ texture: 'character' });\n *\n * image.rotation = Math.PI / 4;\n * layoutRoot.addChild(image);\n * layout.attach('character', layoutRoot);\n * ```\n *\n * The extra container is opt-in: ordinary untransformed content can be\n * attached directly without adding hidden nodes to the Pixi scene graph.\n *\n * Development builds transparently decorate the core controller with layout\n * diagnostics. `import.meta.env.DEV` is replaced at build time, allowing the\n * complete debugger branch to be removed from production playable bundles.\n */\nexport function createLayout(config: LayoutConfig): ReplayableLayout {\n const layout = createCoreLayout(config);\n\n if (!import.meta.env.DEV) {\n return layout;\n }\n\n return createDebugLayout(layout, config, createLayoutDebugRenderer(layout.container));\n}\n\n/**\n * Implements layout ownership and placement without development instrumentation.\n *\n * The controller owns one Pixi container and the transforms of every attached\n * object. It deliberately keeps content as direct children: applications do\n * not pay for an extra wrapper per area. The attachment map is authoritative\n * for ownership; the Pixi parent is checked as a separate invariant so manual\n * reparenting fails clearly instead of silently corrupting future updates.\n */\nexport function createCoreLayout(config: LayoutConfig): ReplayableLayout {\n const container = new Container();\n const attachments = new Map<Container, LayoutAttachment>();\n let areas = resolveLayout(config);\n let destroyed = false;\n\n return {\n container,\n\n /**\n * Returns the currently resolved, immutable area snapshot.\n *\n * Coordinates are layout-local pixels, not normalized authoring values.\n * Callers must request the area again after `update()` because the returned\n * snapshot intentionally does not mutate in place.\n */\n getArea(name): ResolvedLayoutArea | undefined {\n requireActive();\n return areas.get(name);\n },\n\n /**\n * Transfers placement ownership of one Pixi object to a named area.\n *\n * Every operation capable of failing runs before the scene graph or\n * attachment map changes. The resolved transform is therefore committed\n * only after validation and measurement succeed. Destroyed content removes\n * its own bookkeeping entry through Pixi's `destroyed` event.\n */\n attach(areaName, content): void {\n requireActive();\n const area = requireArea(areaName);\n\n if (attachments.has(content)) {\n throw new Error('Content is already attached to this layout. Use move() instead.');\n }\n\n if (content.destroyed) {\n throw new Error('Cannot attach destroyed content to a layout.');\n }\n\n assertNoParentCycle(content);\n const contentLayout = resolveContentLayout(content, area);\n\n container.addChild(content);\n\n /** Forgets ownership when application code destroys content directly. */\n const handleDestroyed = (): void => {\n attachments.delete(content);\n };\n\n content.once('destroyed', handleDestroyed);\n attachments.set(content, { areaName, handleDestroyed });\n applyContentLayout(content, contentLayout);\n },\n\n /**\n * Reassigns managed content to another area without reparenting it.\n *\n * Measurement is completed before either the recorded area name or live\n * transform changes, preserving the old valid placement if resolution\n * throws—for example when scaled content has zero-sized local bounds.\n */\n move(content, areaName): void {\n requireActive();\n const attachment = requireAttachment(content);\n const area = requireArea(areaName);\n\n requireManagedParent(content);\n const contentLayout = resolveContentLayout(content, area);\n\n attachments.set(content, { ...attachment, areaName });\n applyContentLayout(content, contentLayout);\n },\n\n /**\n * Releases placement ownership and removes content from this container.\n *\n * Detach never destroys application content. The parent check makes the\n * final removal tolerant of content already removed by Pixi destruction,\n * while `requireAttachment` still rejects objects this layout never owned.\n */\n detach(content): void {\n requireActive();\n const attachment = requireAttachment(content);\n\n content.off('destroyed', attachment.handleDestroyed);\n attachments.delete(content);\n\n if (content.parent === container) {\n container.removeChild(content);\n }\n },\n\n /**\n * Atomically replaces bounds and areas, then relays out every attachment.\n *\n * Resolution and all content measurements are staged first. No current\n * area or live transform changes unless the complete next configuration is\n * valid for every attachment. Occupied areas cannot disappear because that\n * would leave their content without a deterministic destination.\n */\n update(nextConfig): void {\n requireActive();\n const nextAreas = resolveLayout(nextConfig);\n const nextContentLayouts = new Map<Container, ReturnType<typeof resolveContentLayout>>();\n\n for (const [content, { areaName }] of attachments) {\n requireManagedParent(content);\n const area = nextAreas.get(areaName);\n\n if (area === undefined) {\n throw new Error(`Cannot remove occupied layout area \"${areaName}\".`);\n }\n\n nextContentLayouts.set(content, resolveContentLayout(content, area));\n }\n\n areas = nextAreas;\n\n for (const [content, contentLayout] of nextContentLayouts) {\n applyContentLayout(content, contentLayout);\n }\n },\n\n /**\n * Idempotently releases layout bookkeeping and destroys only its container.\n *\n * Attached application objects are detached, not destroyed. Their listeners\n * are removed explicitly so they no longer retain this controller after its\n * lifecycle ends.\n */\n destroy(): void {\n if (destroyed) {\n return;\n }\n\n destroyed = true;\n\n for (const [content, attachment] of attachments) {\n content.off('destroyed', attachment.handleDestroyed);\n\n if (content.parent === container) {\n container.removeChild(content);\n }\n }\n\n attachments.clear();\n areas = new Map();\n container.destroy();\n },\n };\n\n /** Guards every read and mutation whose state disappears during destruction. */\n function requireActive(): void {\n if (destroyed) {\n throw new Error('Cannot use a destroyed Replayable layout.');\n }\n }\n\n /** Resolves a required area while producing an error that names the authoring key. */\n function requireArea(name: string): ResolvedLayoutArea {\n const area = areas.get(name);\n\n if (area === undefined) {\n throw new Error(`Layout area \"${name}\" does not exist.`);\n }\n\n return area;\n }\n\n /** Returns this layout's ownership record for content or rejects foreign content. */\n function requireAttachment(content: Container): LayoutAttachment {\n const attachment = attachments.get(content);\n\n if (attachment === undefined) {\n throw new Error('Content is not attached to this layout.');\n }\n\n return attachment;\n }\n\n /**\n * Detects manual scene-graph reparenting of managed content.\n *\n * Continuing after reparenting would update an object in another coordinate\n * space, so failing is safer than producing a visually incorrect placement.\n */\n function requireManagedParent(content: Container): void {\n if (content.parent !== container) {\n throw new Error('Attached layout content was reparented outside the layout container.');\n }\n }\n\n /**\n * Prevents Pixi from parenting the layout beneath its own descendant.\n *\n * Walking upward from the owned container catches both the container itself\n * and any ancestor supplied as content, either of which would create a cycle.\n */\n function assertNoParentCycle(content: Container): void {\n let ancestor: Container | null = container;\n\n while (ancestor !== null) {\n if (ancestor === content) {\n throw new Error('Cannot attach the layout container or one of its ancestors.');\n }\n\n ancestor = ancestor.parent;\n }\n }\n}\n","import type { SplitText, Text } from 'pixi.js';\n\nimport type { FitTextOptions } from '#types/text.js';\n\n/**\n * Uniformly fits text inside a box without enlarging beyond its authored size.\n * Replaces the object's scale using local bounds, so repeated fitting never\n * compounds a previous fit. Position, pivot, wrapping, and text remain unchanged.\n * Split manually managed SplitText before fitting, and fit before animating its\n * characters (or supply stable boundsArea). Empty bounds impose no constraint.\n */\nexport function fitText(text: Text | SplitText, options: FitTextOptions): void {\n const { width, height } = options;\n if (\n !Number.isFinite(width) ||\n width <= 0 ||\n (height !== undefined && (!Number.isFinite(height) || height <= 0))\n ) {\n throw new Error('Text fitting dimensions must be positive finite numbers.');\n }\n\n const bounds = text.getLocalBounds();\n const widthScale = bounds.width > 0 ? width / bounds.width : 1;\n const heightScale = height !== undefined && bounds.height > 0 ? height / bounds.height : 1;\n text.scale.set(Math.min(1, widthScale, heightScale));\n}\n"],"mappings":";;;;;;AAKA,SAAgB,qBAAqB,OAAkB,UAAqC;CAC1F,IAAI,CAAC,YAAY,IAAI,KACnB,OAAO;CAIT,WAAW,iBAAiB;CAC5B,WAAW,oBAAoB;CAE/B,OAAO;;CAGP,SAAS,aAAmB;EAC1B,IAAI,WAAW,mBAAmB,OAChC,QAAQ,eAAe,YAAY,gBAAgB;EAErD,IAAI,WAAW,sBAAsB,UACnC,QAAQ,eAAe,YAAY,mBAAmB;CAE1D;AACF;;AAGA,SAAS,OAAa,CAAC;;;;AC3BvB,SAAgB,qBAAqB,UAAgC;CACnE,MAAM,SAAS,qBAAqB,QAAQ;CAE5C,IAAI,OAAO,WAAW,GACpB,MAAM,OAAO;CAEf,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,eAAe,QAAQ,+BAA+B;AAEpE;;AAGA,SAAgB,cAAc,OAAgB,UAAiC;CAC7E,MAAM,SAAS,qBAAqB,QAAQ;CAE5C,IAAI,OAAO,WAAW,GACpB,MAAM;CAER,MAAM,IAAI,eAAe,CAAC,OAAO,GAAG,MAAM,GAAG,kCAAkC,EAAE,MAAM,CAAC;AAC1F;;AAGA,SAAS,qBAAqB,UAAqC;CACjE,MAAM,SAAoB,CAAC;CAC3B,IAAI,UAAU,SAAS,IAAI;CAE3B,OAAO,YAAY,KAAA,GAAW;EAC5B,IAAI;GACF,QAAQ;EACV,SAAS,OAAO;GACd,OAAO,KAAK,KAAK;EACnB;EACA,UAAU,SAAS,IAAI;CACzB;CACA,OAAO;AACT;;;;ACjCA,SAAgB,sBAAsB,eAA2C,CAAC,GAAe;CAC/F,MAAM,WAA2B,CAAC;CAElC,IAAI;EACF,KAAK,MAAM,eAAe,cACxB,SAAS,KAAK,YAAY,MAAM,CAAC;CAErC,SAAS,OAAO;EACd,cAAc,OAAO,QAAQ;CAC/B;CAEA,aAAa,qBAAqB,QAAQ;AAC5C;;;;ACZA,SAAgB,sBAA4B;CAC1C,OAAO,eAAe;EAEpB,eAAe;EAEf,yBAAyB;CAC3B,CAAC;AACH;;;;ACLA,eAAsB,cAAc,SAA4D;CAC9F,MAAM,EAAE,IAAI,WAAW;CACvB,MAAM,CAAC,SAAS,QAAQ,MAAM,QAAQ,IAAI,CACxC,OAAO,KAAc,OAAO,KAAK,GACjC,oBAAoB,IAAI,OAAO,IAAI,CACrC,CAAC;CACD,MAAM,cAAc,IAAI,YAAY;EAAE;EAAM;CAAQ,CAAC;CAErD,MAAM,YAAY,MAAM;CACxB,OAAO,MAAM,IAAI,IAAI,WAAW;CAEhC,OAAO;AACT;;AAGA,eAAe,oBACb,IACA,QAC0B;CAC1B,MAAM,OACJ,OAAO,WAAW,WAAW,MAAM,qBAAqB,IAAI,MAAM,IAAI;CAExE,IAAI,CAAC,kBAAkB,IAAI,GACzB,MAAM,IAAI,MAAM,qBAAqB,GAAG,qCAAqC;CAG/E,OAAO;AACT;;AAGA,eAAe,qBAAqB,IAAY,KAA+B;CAC7E,MAAM,WAAW,MAAM,MAAM,GAAG;CAEhC,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,oCAAoC,GAAG,SAAS,IAAI,EAAE;CAGxE,OAAO,SAAS,KAAK;AACvB;;AAGA,SAAS,kBAAkB,OAA0C;CACnE,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO;CAGT,OAAO,YAAY,SAAS,UAAU;AACxC;;;;AC/CA,eAAsB,eAAe,SAAwD;CAC3F,MAAM,EAAE,IAAI,WAAW;CACvB,MAAM,UAAU,MAAM,OAAO,KAAc;EACzC,OAAO;EACP,KAAK,OAAO;CACd,CAAC;CAID,QAAQ,OAAO,aAAa,OAAO;CACnC,QAAQ,OAAO;CAEf,OAAO;AACT;;;;;;;;;;;;ACPA,SAAgB,oBAAoB,UAAyB,aAA4B;CAEvF,MAAM,aAAa,SAAS,SAAS;CACrC,MAAM,cAAc,YAAY;CAEhC,IAAI,CAAC,eAAe,eAAe,KAAA,GACjC,OAAO,WAAW;CAGpB,IAAI;EACF,SAAS,QAAQ,EAAE,YAAY,MAAM,CAAC;CACxC,UAAU;EACR,IAAI,CAAC,eAAe,eAAe,KAAA,KAAa,gBAAgB,KAAA,GAC9D,WAAW,cAAc;CAE7B;AACF;;;;ACjBA,eAAsB,mBAAmB,SAAyD;CAChG,MAAM,aAAa,cAAc;CACjC,MAAM,gBAAgB,qBAAqB,WAAW,iBAAiB,CAAC;CACxE,MAAM,cAAc,kBAAkB;CACtC,MAAM,WAA2B,CAAC;CAElC,IAAI,aAEF,SAAS,WAAW,WAAW,QAAQ,CAAC;CAG1C,IAAI;EACF,MAAM,WAAW,IAAI,cAAc;EACnC,SAAS,WAAW,oBAAoB,UAAU,WAAW,CAAC;EAE9D,MAAM,SAAS,KAAK;GAClB,WAAW,QAAQ,aAAa;GAChC,aAAa;GACb,iBAAiB,cAAc,IAAI;GACnC,iBAAiB,SAAS,OAAO;GACjC,QAAQ,WAAW,UAAU;GAC7B,mBAAmB;GACnB,SAAS;GACT,OAAO;GACP,iBAAiB,QAAQ,mBAAmB;GAC5C,oBAAoB;GACpB,eAAe,QAAQ,iBAAiB;EAC1C,CAAC;EAED,IAAI,aACF,WAAW,iBAAiB,SAAS,EAAE;EAGzC,OAAO;GAAE;GAAU,eAAe,qBAAqB,QAAQ;EAAE;CACnE,SAAS,OAAO;EACd,OAAO,cAAc,OAAO,QAAQ;CACtC;AACF;;AAGA,SAAS,qBACP,SAC+B;CAC/B,IACE,YAAY,QACX,OAAO,2BAA2B,eAAe,mBAAmB,wBAErE,OAAO;CAGT,MAAM,IAAI,MAAM,oEAAoE;AACtF;;;AC1DA,MAAM,0BAA0B;;AAGhC,SAAgB,mBAAmB,UAAyB,OAA8B;CACxF,MAAM,SAAS,OAAO;CACtB,IAAI,sBAAsB;CAI1B,OAAO,YAAY;CACnB,OAAO,KAAK;CACZ,OAAO,WAAW;CAElB,OAAO,SAAS,OAAO,IAAI,WAAW;;CAGtC,SAAS,YAAY,EAAE,gBAAqC;EAC1D,uBAAuB,eAAe;EAEtC,SAAS,WAAW;EACpB,OAAO,OAAO,mBAAmB;EACjC,SAAS,OAAO,KAAK;CACvB;AACF;;;;ACtBA,SAAgB,sBAAsB,UAAqC;CACzE,MAAM,cAAc,SAAS,GAAG,UAAU,WAAW;CAErD,IAAI;EAGF,IAAI,SAAS,OAAO,UAAU,KAAA,GAC5B,YAAY;CAEhB,SAAS,OAAO;EACd,YAAY;EACZ,MAAM;CACR;CAEA,OAAO;CAEP,SAAS,cAAoB;EAC3B,MAAM,EAAE,OAAO,eAAe,SAAS;EAEvC,SAAS,aAAa;EACtB,SAAS,OAAO,MAAM,OAAO,MAAM,MAAM;CAC3C;AACF;;;;ACXA,eAAsB,WAAW,UAA6B,CAAC,GAA4B;CACzF,oBAAoB;CAEpB,MAAM,WAA2B,CAAC;CAElC,IAAI;EAGF,SAAS,KAAK,SAAS,OAAO,SAAS,WAAW,aAAa,CAAC;EAChE,SAAS,KAAK,SAAS,OAAO,SAAS,WAAW,cAAc,CAAC;EACjE,SAAS,KAAK,sBAAsB,QAAQ,YAAY,CAAC;EAEzD,MAAM,EAAE,UAAU,YAAY,MAAM,mBAAmB,OAAO;EAC9D,SAAS,KAAK,OAAO;EAIrB,MAAM,QAAQ,IAAI,UAAU;EAC5B,SAAS,WAAW,MAAM,QAAQ,EAAE,UAAU,KAAK,CAAC,CAAC;EAErD,MAAM,4BAA4B,sBAAsB,QAAQ;EAChE,SAAS,KAAK,yBAAyB;EAEvC,MAAM,gBAAgB,mBAAmB,UAAU,KAAK;EACxD,SAAS,KAAK,aAAa;EAE3B,MAAM,qBAAqB,qBAAqB,OAAO,QAAQ;EAC/D,SAAS,KAAK,kBAAkB;EAEhC,OAAO;GACL;GACA;GAEA,UAAgB;IAEd,qBAAqB,QAAQ;GAC/B;EACF;CACF,SAAS,OAAO;EACd,OAAO,cAAc,OAAO,QAAQ;CACtC;AACF;;;;;;;;;;;;;;;;ACvCA,SAAgB,aAAa,SAAgD;CAC3E,MAAM,YAAY,IAAI,UAAU,EAAE,OAAO,SAAS,CAAC;CACnD,MAAM,EAAE,SAAS,eAAe;CAChC,QAAQ,YAAY;CACpB,UAAU,SAAS,OAAO;CAE1B,MAAM,SAAS,UAAU,eAAe;CACxC,MAAM,eAAe,IAAI,UAAU,OAAO,GAAG,OAAO,GAAG,OAAO,OAAO,OAAO,MAAM;CAClF,UAAU,aAAa;CACvB,UAAU,UAAU;CAEpB,IAAI,UAAU,QAAQ,WAAW;CACjC,WAAW,OAAO;CAClB,UAAU,GAAG,cAAc,SAAS;CAEpC,OAAO;EAAE;EAAW;EAAY;CAAQ;;CAGxC,SAAS,UAAU,OAAoC;EACrD,MAAM,gBAAgB;EACtB,IAAI,SACF,WAAW;CAEf;;CAGA,SAAS,WAAW,OAAsB;EACxC,IAAI,UAAU,WACZ;EAEF,UAAU;EACV,UAAU,YAAY,UAAU,WAAW;EAC3C,UAAU,SAAS,UAAU,YAAY;CAC3C;;CAGA,SAAS,UAAgB;EACvB,IAAI,UAAU,WACZ;EAEF,WAAW,KAAK;EAChB,UAAU,IAAI,cAAc,SAAS;EACrC,UAAU,QAAQ,EAAE,UAAU,KAAK,CAAC;CACtC;AACF;;;ACzDA,MAAM,gBAAgB;CAAE,GAAG;CAAK,GAAG;AAAI;;AAGvC,SAAgB,0BACd,eACA,SACM;CACN,sBAAsB,eAAe,OAAO;CAC5C,cAAc,OAAO,SAAS,QAAQ,UAAU,aAAa;AAC/D;;;;ACNA,SAAgB,qBAAqB,SAAsD;CACzF,MAAM,EAAE,iBAAiB,GAAG,WAAW,OAAO,QAAQ,OAAO,UAAU;CAEvE,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MAAM,0DAA0D;CAG5E,MAAM,WAAW,OAAO,KAAK,UAAW,OAAO,UAAU,WAAW,QAAQ,KAAK,KAAK,IAAI,KAAM;CAChG,MAAM,SAAS,IAAI,eAAe,QAAQ;CAE1C,0BAA0B,QAAQ,OAAO;CACzC,OAAO,iBAAiB;CACxB,OAAO,OAAO;CAEd,IAAI,UACF,OAAO,KAAK;CAGd,OAAO;AACT;;;;ACnBA,SAAgB,sBAAsB,SAAwD;CAC5F,MAAM,EAAE,cAAc,QAAQ,WAAW,YAAY,SAAS,WAAW,UAAU;CACnF,MAAM,SAAS,IAAI,gBAAgB;EACjC;EACA;EACA;EACA;EACA,SAAS,OAAO,YAAY,WAAW,QAAQ,KAAK,OAAO,IAAI;EAC/D;EACA;CACF,CAAC;CAED,0BAA0B,QAAQ,OAAO;CAEzC,OAAO;AACT;;;;ACfA,SAAgB,aAAa,UAA+B,CAAC,GAAW;CACtE,MAAM,SAAS,OAAO,KAAK,QAAQ,WAAW,QAAQ,KAAK;CAE3D,0BAA0B,QAAQ,OAAO;CAEzC,IAAI,QAAQ,SAAS,KAAA,GACnB,OAAO,OAAO,QAAQ;CAGxB,IAAI,QAAQ,cAAc,KAAA,GACxB,OAAO,YAAY,QAAQ;CAG7B,OAAO;AACT;;;;;;;;;ACTA,SAAgB,gBAAgB,SAA4C;CAC1E,MAAM,aAAa,IAAI,UAAU,OAAO;CAExC,sBAAsB,YAAY,OAAO;CAEzC,OAAO;AACT;;;;ACXA,SAAgB,WAAW,SAAkC;CAC3D,MAAM,aAAa,IAAI,KAAK;EAAE,OAAO,QAAQ;EAAO,MAAM,QAAQ;CAAK,CAAC;CAExE,0BAA0B,YAAY,OAAO;CAE7C,OAAO;AACT;;;;;;;;;;;;;ACCA,SAAgB,qBAAqB,SAAkC;CACrE,MAAM,SAAS,QAAQ,eAAe;CACtC,MAAM,SAAS,QAAQ,KAAK,OAAO,IAAI,QAAQ,MAAM,KAAK,QAAQ,MAAM;CACxE,MAAM,UAAU,QAAQ,KAAK,OAAO,IAAI,OAAO,QAAQ,QAAQ,MAAM,KAAK,QAAQ,MAAM;CACxF,MAAM,SAAS,QAAQ,KAAK,OAAO,IAAI,QAAQ,MAAM,KAAK,QAAQ,MAAM;CACxE,MAAM,UAAU,QAAQ,KAAK,OAAO,IAAI,OAAO,SAAS,QAAQ,MAAM,KAAK,QAAQ,MAAM;CAEzF,OAAO;EACL,GAAG,KAAK,IAAI,QAAQ,OAAO;EAC3B,GAAG,KAAK,IAAI,QAAQ,OAAO;EAC3B,OAAO,KAAK,IAAI,UAAU,MAAM;EAChC,QAAQ,KAAK,IAAI,UAAU,MAAM;CACnC;AACF;;;ACxBA,MAAM,YAAuC;CAC3C,OAAO;CACP,SAAS;CACT,QAAQ;AACV;;AAGA,MAAM,iBAA6C;CACjD,YAAY;CACZ,eAAe;CACf,QAAQ;CACR,cAAc;AAChB;;;;;;;;AASA,SAAgB,0BACd,OACwC;CACxC,IAAI,UAAU,KAAA,KAAa,UAAU,OACnC;CAGF,IAAI,UAAU,MACZ,OAAO;CAGT,OAAO;EACL,GAAG;EACH,GAAG;EACH,QAAQ,cAAc,MAAM,MAAM;CACpC;AACF;;AAGA,SAAS,cAAc,QAAiE;CACtF,IAAI,WAAW,OACb,OAAO;EAAE,OAAO;EAAO,SAAS;EAAO,QAAQ;CAAM;CAGvD,IAAI,WAAW,KAAA,KAAa,WAAW,MACrC,OAAO;CAGT,OAAO;EAAE,GAAG;EAAW,GAAG;CAAO;AACnC;;;;;;;;;;ACjCA,SAAgB,kBACd,QACA,eACA,UACkB;CAClB,MAAM,8BAAc,IAAI,IAAiC;CACzD,IAAI,SAAS;CACb,IAAI,YAAY;CAEhB,iBAAiB;CAEjB,OAAO;EACL,WAAW,OAAO;;EAGlB,QAAQ,MAAsC;GAC5C,OAAO,OAAO,QAAQ,IAAI;EAC5B;;;;;;;;;EAUA,OAAO,UAAU,SAAe;GAC9B,OAAO,OAAO,UAAU,OAAO;;GAG/B,MAAM,wBAA8B;IAClC,YAAY,OAAO,OAAO;IAC1B,iBAAiB;GACnB;GAEA,QAAQ,KAAK,aAAa,eAAe;GACzC,YAAY,IAAI,SAAS;IAAE;IAAU;GAAgB,CAAC;GACtD,iBAAiB;EACnB;;EAGA,KAAK,SAAS,UAAgB;GAC5B,OAAO,KAAK,SAAS,QAAQ;GAC7B,MAAM,aAAa,kBAAkB,OAAO;GAE5C,YAAY,IAAI,SAAS;IAAE,GAAG;IAAY;GAAS,CAAC;GACpD,iBAAiB;EACnB;;;;;EAMA,OAAO,SAAe;GACpB,OAAO,OAAO,OAAO;GACrB,MAAM,aAAa,kBAAkB,OAAO;GAE5C,QAAQ,IAAI,aAAa,WAAW,eAAe;GACnD,YAAY,OAAO,OAAO;GAC1B,iBAAiB;EACnB;;;;;EAMA,OAAO,YAAkB;GACvB,OAAO,OAAO,UAAU;GACxB,SAAS;GACT,iBAAiB;EACnB;;;;;EAMA,UAAgB;GACd,IAAI,WACF;GAGF,YAAY;GAEZ,KAAK,MAAM,CAAC,SAAS,eAAe,aAClC,QAAQ,IAAI,aAAa,WAAW,eAAe;GAGrD,YAAY,MAAM;GAClB,SAAS,QAAQ;GACjB,OAAO,QAAQ;EACjB;CACF;;;;;;CAOA,SAAS,mBAAyB;EAChC,MAAM,UAAU,0BAA0B,OAAO,KAAK;EAEtD,IAAI,YAAY,KAAA,GAAW;GACzB,SAAS,OAAO,KAAA,CAAS;GACzB;EACF;EAEA,SAAS,OAAO;GACd,OAAO,OAAO,KAAK,OAAO,KAAK,CAAC,CAAC,KAAK,SACpC,YAAY,MAAM,QAAQ,iBAAiB,QAAQ,OAAO,OAAO,CACnE;GACA,QAAQ,OAAO;GACf;EACF,CAAC;CACH;;;;;;;CAQA,SAAS,YAAY,MAAc,sBAAgD;EACjF,MAAM,OAAO,OAAO,QAAQ,IAAI;EAEhC,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,wCAAwC,KAAK,GAAG;EAGlE,MAAM,kBAAkB,CAAC,GAAG,WAAW,CAAC,CAAC,QACtC,GAAG,gBAAgB,WAAW,aAAa,IAC9C;EAEA,OAAO;GACL,GAAG;GACH,eAAe,uBACX,gBAAgB,KAAK,CAAC,aAAa,qBAAqB,OAAO,CAAC,IAChE,CAAC;GACL,UAAU,gBAAgB,SAAS;EACrC;CACF;;CAGA,SAAS,kBAAkB,SAAsC;EAC/D,MAAM,aAAa,YAAY,IAAI,OAAO;EAE1C,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,MAAM,8CAA8C;EAGhE,OAAO;CACT;AACF;;;ACvKA,MAAM,mBAAmB;AACzB,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAC5B,MAAM,aAAa;;;;;;;;;;AAWnB,SAAgB,0BAA0B,WAA2C;CACnF,MAAM,UAAU,IAAI,UAAU;CAC9B,MAAM,WAAW,IAAI,SAAS;CAC9B,MAAM,SAAiB,CAAC;CAExB,QAAQ,YAAY;CACpB,QAAQ,QAAQ;CAGhB,QAAQ,aAAa;CACrB,QAAQ,SAAS,QAAQ;CAEzB,OAAO;EAAE;EAAS;CAAO;;;;;;;;CASzB,SAAS,OAAO,YAAqD;EACnE,aAAa;EAEb,IAAI,eAAe,KAAA,GAAW;GAC5B,IAAI,QAAQ,WAAW,WACrB,UAAU,YAAY,OAAO;GAG/B;EACF;EAEA,MAAM,EAAE,OAAO,QAAQ,YAAY;EAEnC,IAAI,QAAQ,cACV,WAAW,QAAQ,mBAAmB;EAGxC,IAAI,QAAQ,OAAO,QACjB,UAAU,QAAQ,mBAAmB;EAGvC,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI,QAAQ,YACV,WAAW,KAAK,QAAQ,KAAK,WAAW,sBAAsB,gBAAgB;GAGhF,IAAI,QAAQ,eACV,KAAK,MAAM,iBAAiB,KAAK,eAC/B,WAAW,eAAe,oBAAoB;GAIlD,IAAI,QAAQ,OAAO,OACjB,UAAU,KAAK,QAAQ,KAAK,WAAW,sBAAsB,gBAAgB;GAG/E,IAAI,QAAQ,OAAO,SACjB,KAAK,MAAM,iBAAiB,KAAK,eAC/B,UAAU,eAAe,sBAAsB,cAAc;EAGnE;EAIA,UAAU,SAAS,OAAO;CAC5B;;;;;CAMA,SAAS,UAAgB;EACvB,aAAa;EACb,QAAQ,iBAAiB;EACzB,QAAQ,QAAQ,EAAE,UAAU,KAAK,CAAC;CACpC;;CAGA,SAAS,eAAqB;EAC5B,SAAS,MAAM;EAEf,KAAK,MAAM,SAAS,QAClB,MAAM,QAAQ;EAGhB,OAAO,SAAS;CAClB;;;;;CAMA,SAAS,WAAW,QAAsB,OAAqB;EAC7D,SAAS,KAAK,OAAO,GAAG,OAAO,GAAG,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,OAAO;GACpE;GACA,WAAW;GACX,OAAO;EACT,CAAC;CACH;;;;;;CAOA,SAAS,UACP,QACA,OACA,YAAyC,YACnC;EACN,MAAM,EAAE,QAAQ,OAAO,GAAG,MAAM;EAChC,MAAM,QAAQ,IAAI,KAAK;GACrB,MAAM,GAAG,gBAAgB,KAAK,EAAE,KAAK,gBAAgB,MAAM;GAC3D,OAAO;IACL,MAAM;IACN,YAAY;IACZ,UAAU;IACV,QAAQ;KAAE,OAAO;KAAW,OAAO;IAAE;GACvC;EACF,CAAC;EAED,MAAM,YAAY;EAElB,IAAI,cAAc,gBAAgB;GAChC,MAAM,OAAO,IAAI,GAAG,CAAC;GACrB,MAAM,SAAS,IAAI,IAAI,QAAQ,GAAG,IAAI,SAAS,CAAC;EAClD,OACE,MAAM,SAAS,IAAI,IAAI,GAAG,IAAI,CAAC;EAGjC,OAAO,KAAK,KAAK;EACjB,QAAQ,SAAS,KAAK;CACxB;AACF;;AAGA,SAAS,gBAAgB,OAAuB;CAC9C,OAAO,OAAO,UAAU,KAAK,IAAI,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;AAClE;;;;;;;;;;;;;;;;ACzIA,SAAgB,qBAAqB,SAAoB,MAAyC;CAChG,MAAM,gBAAgB,QAAQ,eAAe;CAC7C,MAAM,EAAE,QAAQ,WAAW,aACzB,KAAK,OACL,KAAK,QACL,eACA,QAAQ,MAAM,GACd,QAAQ,MAAM,CAChB;CAIA,MAAM,UAAU,cAAc,IAAI,QAAQ,MAAM,KAAK;CACrD,MAAM,WAAW,cAAc,IAAI,cAAc,QAAQ,QAAQ,MAAM,KAAK;CAC5E,MAAM,UAAU,cAAc,IAAI,QAAQ,MAAM,KAAK;CACrD,MAAM,WAAW,cAAc,IAAI,cAAc,SAAS,QAAQ,MAAM,KAAK;CAC7E,MAAM,WAAW,KAAK,IAAI,QAAQ,OAAO;CACzC,MAAM,WAAW,KAAK,IAAI,QAAQ,OAAO;CACzC,MAAM,eAAe,KAAK,IAAI,UAAU,MAAM;CAC9C,MAAM,gBAAgB,KAAK,IAAI,UAAU,MAAM;CAE/C,OAAO;EACL,GAAG,gBAAgB,KAAK,OAAO,KAAK,QAAQ,YAAY,IAAI,WAAW,KAAK,OAAO;EACnF,GAAG,gBAAgB,KAAK,OAAO,KAAK,QAAQ,aAAa,IAAI,WAAW,KAAK,OAAO;EACpF;EACA;CACF;AACF;;;;;;;;;AAUA,SAAgB,mBAAmB,SAAoB,QAA6B;CAClF,QAAQ,MAAM,IAAI,OAAO,QAAQ,OAAO,MAAM;CAC9C,QAAQ,SAAS,IAAI,OAAO,GAAG,OAAO,CAAC;AACzC;;;;;;;;;AAUA,SAAS,aACP,MACA,MACA,SACA,eACA,eACsD;CACtD,IAAI,SAAS,QACX,OAAO;EAAE,QAAQ;EAAe,QAAQ;CAAc;CAGxD,IAAI,QAAQ,UAAU,KAAK,QAAQ,WAAW,GAC5C,MAAM,IAAI,MAAM,mCAAmC,KAAK,yBAAyB;CAGnF,MAAM,aAAa,KAAK,QAAQ,QAAQ;CACxC,MAAM,cAAc,KAAK,SAAS,QAAQ;CAE1C,QAAQ,MAAR;EACE,KAAK,OAAO;GACV,MAAM,QAAQ,KAAK,IAAI,GAAG,YAAY,WAAW;GACjD,OAAO;IAAE,QAAQ;IAAO,QAAQ;GAAM;EACxC;EACA,KAAK,WAAW;GACd,MAAM,QAAQ,KAAK,IAAI,YAAY,WAAW;GAC9C,OAAO;IAAE,QAAQ;IAAO,QAAQ;GAAM;EACxC;EACA,KAAK,SAAS;GACZ,MAAM,QAAQ,KAAK,IAAI,YAAY,WAAW;GAC9C,OAAO;IAAE,QAAQ;IAAO,QAAQ;GAAM;EACxC;EACA,KAAK,WACH,OAAO;GAAE,QAAQ;GAAY,QAAQ;EAAY;EACnD,SACE,MAAM,IAAI,MAAM,4BAA4B;CAChD;AACF;;AAGA,SAAS,gBACP,WACA,MACA,cACQ;CACR,QAAQ,WAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK,eACH,OAAO,KAAK;EACd,KAAK;EACL,KAAK;EACL,KAAK,iBACH,OAAO,KAAK,KAAK,KAAK,QAAQ,gBAAgB;EAChD,KAAK;EACL,KAAK;EACL,KAAK,gBACH,OAAO,KAAK,IAAI,KAAK,QAAQ;EAC/B,SACE,MAAM,IAAI,MAAM,2BAA2B;CAC/C;AACF;;AAGA,SAAS,gBACP,WACA,MACA,eACQ;CACR,QAAQ,WAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK,aACH,OAAO,KAAK;EACd,KAAK;EACL,KAAK;EACL,KAAK,gBACH,OAAO,KAAK,KAAK,KAAK,SAAS,iBAAiB;EAClD,KAAK;EACL,KAAK;EACL,KAAK,gBACH,OAAO,KAAK,IAAI,KAAK,SAAS;EAChC,SACE,MAAM,IAAI,MAAM,2BAA2B;CAC/C;AACF;;;;AC1JA,MAAa,mBAAmB;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAa,mBAAmB;CAC9B;CACA;CACA;CACA;CACA;AACF;;;;ACPA,MAAM,cAAc;CAAE,GAAG;CAAG,GAAG;AAAE;;;;;;;;;AAUjC,SAAgB,cAAc,QAA+D;CAC3F,qBAAqB,OAAO,MAAM;CAElC,MAAM,wBAAQ,IAAI,IAAgC;CAElD,KAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,OAAO,KAAK,GAAG;EACvD,IAAI,KAAK,KAAK,CAAC,CAAC,WAAW,GACzB,MAAM,IAAI,MAAM,sCAAsC;EAGxD,MAAM,IAAI,MAAM,kBAAkB,MAAM,MAAM,OAAO,MAAM,CAAC;CAC9D;CAEA,OAAO;AACT;;;;;;;;AASA,SAAS,kBACP,MACA,QACA,cACoB;CACpB,mBAAmB,MAAM,OAAO,MAAM;CACtC,MAAM,SAAS,OAAO,UAAU;CAEhC,qBAAqB,OAAO,GAAG,gBAAgB,KAAK,WAAW;CAC/D,qBAAqB,OAAO,GAAG,gBAAgB,KAAK,WAAW;CAC/D,wBAAwB,MAAM,MAAM;CAEpC,OAAO,OAAO,OAAO;EACnB;EACA,QAAQ,OAAO,OAAO;GACpB,GAAG,aAAa,IAAI,OAAO,OAAO,IAAI,aAAa;GACnD,GAAG,aAAa,IAAI,OAAO,OAAO,IAAI,aAAa;GACnD,OAAO,OAAO,OAAO,QAAQ,aAAa;GAC1C,QAAQ,OAAO,OAAO,SAAS,aAAa;EAC9C,CAAC;EACD,OAAO,OAAO,SAAA;EACd,OAAO,OAAO,SAAA;EACd,QAAQ,OAAO,OAAO;GAAE,GAAG,OAAO;GAAG,GAAG,OAAO;EAAE,CAAC;CACpD,CAAC;AACH;;AAGA,SAAS,wBAAwB,MAAc,QAAgC;CAC7E,IAAI,OAAO,UAAU,KAAA,KAAa,CAAC,iBAAiB,SAAS,OAAO,KAAK,GACvE,MAAM,IAAI,MAAM,gBAAgB,KAAK,8BAA8B,OAAO,MAAM,EAAE;CAGpF,IAAI,OAAO,UAAU,KAAA,KAAa,CAAC,iBAAiB,SAAS,OAAO,KAAK,GACvE,MAAM,IAAI,MAAM,gBAAgB,KAAK,+BAA+B,OAAO,MAAM,EAAE;AAEvF;;AAGA,SAAS,qBAAqB,QAA4B;CACxD,eAAe,iBAAiB,MAAM;CAEtC,IAAI,OAAO,SAAS,KAAK,OAAO,UAAU,GACxC,MAAM,IAAI,MAAM,2DAA2D;AAE/E;;AAGA,SAAS,mBAAmB,MAAc,QAA4B;CACpE,eAAe,gBAAgB,KAAK,WAAW,MAAM;CAErD,IAAI,OAAO,QAAQ,KAAK,OAAO,SAAS,GACtC,MAAM,IAAI,MAAM,gBAAgB,KAAK,yCAAyC;AAElF;;AAGA,SAAS,eAAe,OAAe,QAA4B;CACjE,qBAAqB,OAAO,GAAG,GAAG,MAAM,GAAG;CAC3C,qBAAqB,OAAO,GAAG,GAAG,MAAM,GAAG;CAC3C,qBAAqB,OAAO,OAAO,GAAG,MAAM,OAAO;CACnD,qBAAqB,OAAO,QAAQ,GAAG,MAAM,QAAQ;AACvD;;AAGA,SAAS,qBAAqB,OAAe,OAAqB;CAChE,IAAI,CAAC,OAAO,SAAS,KAAK,GACxB,MAAM,IAAI,MAAM,GAAG,MAAM,0BAA0B;AAEvD;;;;;;;;;;;;;;;;;;;;;;;AClFA,SAAgB,aAAa,QAAwC;CACnE,MAAM,SAAS,iBAAiB,MAAM;CAEtC,IAAI,CAAC,YAAY,IAAI,KACnB,OAAO;CAGT,OAAO,kBAAkB,QAAQ,QAAQ,0BAA0B,OAAO,SAAS,CAAC;AACtF;;;;;;;;;;AAWA,SAAgB,iBAAiB,QAAwC;CACvE,MAAM,YAAY,IAAI,UAAU;CAChC,MAAM,8BAAc,IAAI,IAAiC;CACzD,IAAI,QAAQ,cAAc,MAAM;CAChC,IAAI,YAAY;CAEhB,OAAO;EACL;;;;;;;;EASA,QAAQ,MAAsC;GAC5C,cAAc;GACd,OAAO,MAAM,IAAI,IAAI;EACvB;;;;;;;;;EAUA,OAAO,UAAU,SAAe;GAC9B,cAAc;GACd,MAAM,OAAO,YAAY,QAAQ;GAEjC,IAAI,YAAY,IAAI,OAAO,GACzB,MAAM,IAAI,MAAM,iEAAiE;GAGnF,IAAI,QAAQ,WACV,MAAM,IAAI,MAAM,8CAA8C;GAGhE,oBAAoB,OAAO;GAC3B,MAAM,gBAAgB,qBAAqB,SAAS,IAAI;GAExD,UAAU,SAAS,OAAO;;GAG1B,MAAM,wBAA8B;IAClC,YAAY,OAAO,OAAO;GAC5B;GAEA,QAAQ,KAAK,aAAa,eAAe;GACzC,YAAY,IAAI,SAAS;IAAE;IAAU;GAAgB,CAAC;GACtD,mBAAmB,SAAS,aAAa;EAC3C;;;;;;;;EASA,KAAK,SAAS,UAAgB;GAC5B,cAAc;GACd,MAAM,aAAa,kBAAkB,OAAO;GAC5C,MAAM,OAAO,YAAY,QAAQ;GAEjC,qBAAqB,OAAO;GAC5B,MAAM,gBAAgB,qBAAqB,SAAS,IAAI;GAExD,YAAY,IAAI,SAAS;IAAE,GAAG;IAAY;GAAS,CAAC;GACpD,mBAAmB,SAAS,aAAa;EAC3C;;;;;;;;EASA,OAAO,SAAe;GACpB,cAAc;GACd,MAAM,aAAa,kBAAkB,OAAO;GAE5C,QAAQ,IAAI,aAAa,WAAW,eAAe;GACnD,YAAY,OAAO,OAAO;GAE1B,IAAI,QAAQ,WAAW,WACrB,UAAU,YAAY,OAAO;EAEjC;;;;;;;;;EAUA,OAAO,YAAkB;GACvB,cAAc;GACd,MAAM,YAAY,cAAc,UAAU;GAC1C,MAAM,qCAAqB,IAAI,IAAwD;GAEvF,KAAK,MAAM,CAAC,SAAS,EAAE,eAAe,aAAa;IACjD,qBAAqB,OAAO;IAC5B,MAAM,OAAO,UAAU,IAAI,QAAQ;IAEnC,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,uCAAuC,SAAS,GAAG;IAGrE,mBAAmB,IAAI,SAAS,qBAAqB,SAAS,IAAI,CAAC;GACrE;GAEA,QAAQ;GAER,KAAK,MAAM,CAAC,SAAS,kBAAkB,oBACrC,mBAAmB,SAAS,aAAa;EAE7C;;;;;;;;EASA,UAAgB;GACd,IAAI,WACF;GAGF,YAAY;GAEZ,KAAK,MAAM,CAAC,SAAS,eAAe,aAAa;IAC/C,QAAQ,IAAI,aAAa,WAAW,eAAe;IAEnD,IAAI,QAAQ,WAAW,WACrB,UAAU,YAAY,OAAO;GAEjC;GAEA,YAAY,MAAM;GAClB,wBAAQ,IAAI,IAAI;GAChB,UAAU,QAAQ;EACpB;CACF;;CAGA,SAAS,gBAAsB;EAC7B,IAAI,WACF,MAAM,IAAI,MAAM,2CAA2C;CAE/D;;CAGA,SAAS,YAAY,MAAkC;EACrD,MAAM,OAAO,MAAM,IAAI,IAAI;EAE3B,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,gBAAgB,KAAK,kBAAkB;EAGzD,OAAO;CACT;;CAGA,SAAS,kBAAkB,SAAsC;EAC/D,MAAM,aAAa,YAAY,IAAI,OAAO;EAE1C,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,MAAM,yCAAyC;EAG3D,OAAO;CACT;;;;;;;CAQA,SAAS,qBAAqB,SAA0B;EACtD,IAAI,QAAQ,WAAW,WACrB,MAAM,IAAI,MAAM,sEAAsE;CAE1F;;;;;;;CAQA,SAAS,oBAAoB,SAA0B;EACrD,IAAI,WAA6B;EAEjC,OAAO,aAAa,MAAM;GACxB,IAAI,aAAa,SACf,MAAM,IAAI,MAAM,6DAA6D;GAG/E,WAAW,SAAS;EACtB;CACF;AACF;;;;;;;;;;AC7PA,SAAgB,QAAQ,MAAwB,SAA+B;CAC7E,MAAM,EAAE,OAAO,WAAW;CAC1B,IACE,CAAC,OAAO,SAAS,KAAK,KACtB,SAAS,KACR,WAAW,KAAA,MAAc,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,IAEhE,MAAM,IAAI,MAAM,0DAA0D;CAG5E,MAAM,SAAS,KAAK,eAAe;CACnC,MAAM,aAAa,OAAO,QAAQ,IAAI,QAAQ,OAAO,QAAQ;CAC7D,MAAM,cAAc,WAAW,KAAA,KAAa,OAAO,SAAS,IAAI,SAAS,OAAO,SAAS;CACzF,KAAK,MAAM,IAAI,KAAK,IAAI,GAAG,YAAY,WAAW,CAAC;AACrD"}
@@ -0,0 +1,77 @@
1
+ import { ColorSource, Container, EventMode, PointData, SplitTextOptions, TextString, TextStyle, TextStyleOptions, Texture, WebGLRenderer } from "pixi.js";
2
+ //#region src/types/factories.d.ts
3
+ /** Properties shared by Replayable's Pixi display-object factories. */
4
+ interface DisplayObjectOptions {
5
+ readonly alpha?: number;
6
+ readonly pivot?: PointData;
7
+ readonly position?: PointData;
8
+ readonly rotation?: number;
9
+ readonly scale?: PointData;
10
+ readonly visible?: boolean;
11
+ readonly zIndex?: number;
12
+ }
13
+ /** Shared properties for display objects whose visual origin can be anchored. */
14
+ interface AnchorableDisplayObjectOptions extends DisplayObjectOptions {
15
+ readonly anchor?: PointData;
16
+ }
17
+ /** Appearance and interaction properties shared by sprite-based factories. */
18
+ interface SpriteDisplayOptions extends AnchorableDisplayObjectOptions {
19
+ readonly eventMode?: EventMode;
20
+ readonly tint?: ColorSource;
21
+ }
22
+ /** Options for creating a Pixi sprite from a loaded alias or existing texture. */
23
+ interface CreateSpriteOptions extends SpriteDisplayOptions {
24
+ readonly texture?: string | Texture;
25
+ }
26
+ /** Options for creating a resizable Pixi nine-slice sprite. */
27
+ interface CreateNineSliceSpriteOptions extends AnchorableDisplayObjectOptions {
28
+ readonly bottomHeight: number;
29
+ readonly height: number;
30
+ readonly leftWidth: number;
31
+ readonly rightWidth: number;
32
+ readonly texture: string | Texture;
33
+ readonly topHeight: number;
34
+ readonly width: number;
35
+ }
36
+ /** Options for creating a Pixi animated sprite from loaded texture frames. */
37
+ interface CreateAnimatedSpriteOptions extends AnchorableDisplayObjectOptions {
38
+ readonly animationSpeed?: number;
39
+ readonly autoPlay?: boolean;
40
+ readonly frames: readonly (string | Texture)[];
41
+ readonly loop?: boolean;
42
+ }
43
+ /** Options for creating Pixi canvas text without implicit fitting or localization. */
44
+ interface CreateTextOptions extends AnchorableDisplayObjectOptions {
45
+ readonly style: TextStyle | Partial<TextStyleOptions>;
46
+ readonly text: TextString;
47
+ }
48
+ /** Native split-text options with Replayable's shared transform defaults; no fitting or animation. */
49
+ interface CreateSplitTextOptions extends Omit<SplitTextOptions, keyof DisplayObjectOptions>, DisplayObjectOptions {}
50
+ //#endregion
51
+ //#region src/types/pixi.d.ts
52
+ /** Optional capability installed and owned by one Replayable Pixi instance. */
53
+ interface PixiIntegration {
54
+ /** Installs the capability and returns its matching cleanup operation. */
55
+ setup(): () => void;
56
+ }
57
+ /** Playable-safe renderer choices that remain under application control. */
58
+ interface CreatePixiOptions {
59
+ /** Enables multisample antialiasing for the default framebuffer. */
60
+ readonly antialias?: boolean;
61
+ /** Optional renderer capabilities installed before runtime asset loading begins. */
62
+ readonly integrations?: readonly PixiIntegration[];
63
+ /** Selects the browser's preferred GPU power profile. */
64
+ readonly powerPreference?: 'high-performance' | 'low-power';
65
+ /** Preserves the previous frame in a back buffer for effects that sample it. */
66
+ readonly useBackBuffer?: boolean;
67
+ }
68
+ /** Initialized Pixi renderer and the root stage owned by Replayable. */
69
+ interface ReplayablePixi {
70
+ readonly renderer: WebGLRenderer;
71
+ readonly stage: Container;
72
+ /** Releases Pixi lifecycle work and renderer-owned resources once. */
73
+ destroy(): void;
74
+ }
75
+ //#endregion
76
+ export { CreateAnimatedSpriteOptions as a, CreateSpriteOptions as c, SpriteDisplayOptions as d, AnchorableDisplayObjectOptions as i, CreateTextOptions as l, PixiIntegration as n, CreateNineSliceSpriteOptions as o, ReplayablePixi as r, CreateSplitTextOptions as s, CreatePixiOptions as t, DisplayObjectOptions as u };
77
+ //# sourceMappingURL=pixi-DXx3NI4T.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { n as PixiIntegration, u as DisplayObjectOptions } from "../pixi-DXx3NI4T.js";
2
+ import { Spine } from "@esotericsoftware/spine-pixi-v8";
3
+ //#region src/spine/create-spine-integration.d.ts
4
+ /** Creates the optional Pixi capability responsible for Replayable Spine assets. */
5
+ declare function createSpineIntegration(): PixiIntegration;
6
+ //#endregion
7
+ //#region src/types/spine.d.ts
8
+ /** Options for creating a Spine display object from a loaded Replayable asset. */
9
+ interface CreateSpineOptions extends DisplayObjectOptions {
10
+ /** Default crossfade duration, in seconds, between animations. */
11
+ readonly defaultMix?: number;
12
+ /** Generated Spine registry value identifying the loaded skeleton. */
13
+ readonly skeleton: string;
14
+ /** Playback multiplier applied to the skeleton's animation state. */
15
+ readonly speed?: number;
16
+ }
17
+ //#endregion
18
+ //#region src/spine/factories/create-spine.d.ts
19
+ /** Creates an unattached Spine display object from a loaded Replayable asset ID. */
20
+ declare function createSpine(options: CreateSpineOptions): Spine;
21
+ //#endregion
22
+ export { type CreateSpineOptions, createSpine, createSpineIntegration };
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,136 @@
1
+ import { t as applyContainerOptions } from "../apply-container-options-CloU_2Pk.js";
2
+ import { playable } from "@replayablejs/runtime";
3
+ import { Assets, Ticker } from "pixi.js";
4
+ import { AtlasAttachmentLoader, SkeletonBinary, SkeletonData, SkeletonJson, Spine, SpineTexture, TextureAtlas } from "@esotericsoftware/spine-pixi-v8";
5
+ //#region src/spine/loader/load-pixi-spine.ts
6
+ /**
7
+ * Loads one generated Spine asset and returns the skeleton data cached by runtime.
8
+ * Atlas text, page images, and skeleton data load concurrently. Once available,
9
+ * pages are bound to Pixi textures before Spine parses attachments. Instances made
10
+ * by createSpine share this data; each instance owns its own animation state.
11
+ */
12
+ async function loadPixiSpine(context) {
13
+ const { id, source } = context;
14
+ try {
15
+ const [atlasSource, textures, skeletonSource] = await Promise.all([
16
+ loadAtlasSource(context),
17
+ loadAtlasTextures(source),
18
+ loadSkeletonSource(context)
19
+ ]);
20
+ const atlas = new TextureAtlas(atlasSource);
21
+ bindAtlasTextures(id, atlas, textures);
22
+ const attachmentLoader = new AtlasAttachmentLoader(atlas);
23
+ if (skeletonSource.format === "json") return new SkeletonJson(attachmentLoader).readSkeletonData(skeletonSource.data);
24
+ return new SkeletonBinary(attachmentLoader).readSkeletonData(skeletonSource.data);
25
+ } catch (cause) {
26
+ throw new Error(`Failed to load Replayable Spine asset "${id}".`, { cause });
27
+ }
28
+ }
29
+ /** Inline mode carries atlas text directly; resource mode carries its fetchable URL. */
30
+ async function loadAtlasSource(context) {
31
+ if (context.assetMode === "inline") return context.source.atlas;
32
+ return (await fetchResource(context.source.atlas, "atlas")).text();
33
+ }
34
+ /**
35
+ * Resolves the generated representation into the input expected by Spine's parser.
36
+ * JSON may already be an object. Binary skeletons always arrive through a URL,
37
+ * including a data URL in inline exports. Embedded bytes are decoded locally;
38
+ * only external resource URLs go through fetch.
39
+ */
40
+ async function loadSkeletonSource(context) {
41
+ const { format, skel } = context.source;
42
+ if (format === "json") {
43
+ if (typeof skel !== "string") return {
44
+ data: skel,
45
+ format
46
+ };
47
+ return {
48
+ data: await (await fetchResource(skel, "JSON skeleton")).json(),
49
+ format
50
+ };
51
+ }
52
+ if (typeof skel !== "string") throw new Error("A binary Spine skeleton must be emitted as a resource URL.");
53
+ if (skel.startsWith("data:")) return {
54
+ data: decodeInlineSkeleton(skel),
55
+ format
56
+ };
57
+ const response = await fetchResource(skel, "binary skeleton");
58
+ return {
59
+ data: new Uint8Array(await response.arrayBuffer()),
60
+ format
61
+ };
62
+ }
63
+ /**
64
+ * Decodes the Base64 data URL emitted by the asset pipeline without a request.
65
+ * Meta's connect-src policy blocks fetch(data:...) even though the bytes are
66
+ * already embedded. atob preserves binary byte values, including zero and 255.
67
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src
68
+ */
69
+ function decodeInlineSkeleton(source) {
70
+ const separator = source.indexOf(",");
71
+ if (separator === -1 || !source.slice(0, separator).endsWith(";base64")) throw new Error("An inline binary Spine skeleton must be a Base64 data URL.");
72
+ const binary = atob(source.slice(separator + 1));
73
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
74
+ }
75
+ /** Loads cached Pixi textures in the atlas page order preserved by asset generation. */
76
+ async function loadAtlasTextures(source) {
77
+ return Promise.all(source.images.map(async (image) => {
78
+ const texture = await Assets.load(image);
79
+ texture.source.resolution = source.scale;
80
+ texture.update();
81
+ return texture;
82
+ }));
83
+ }
84
+ /**
85
+ * Pairs atlas pages with generated images by index, after checking their counts.
86
+ * SpineTexture adapts Pixi's texture source for attachment UVs and page sampling;
87
+ * it does not create another browser image loader.
88
+ */
89
+ function bindAtlasTextures(id, atlas, textures) {
90
+ if (atlas.pages.length !== textures.length) throw new Error(`Spine asset "${id}" contains ${atlas.pages.length} atlas pages but provides ${textures.length} images.`);
91
+ for (const [index, page] of atlas.pages.entries()) {
92
+ const texture = textures[index];
93
+ if (texture === void 0) throw new Error(`Spine asset "${id}" is missing atlas image ${index}.`);
94
+ page.setTexture(SpineTexture.from(texture.source));
95
+ }
96
+ }
97
+ /** Rejects HTTP failures with the resource kind and URL before attempting to parse it. */
98
+ async function fetchResource(source, description) {
99
+ const response = await fetch(source);
100
+ if (!response.ok) throw new Error(`Failed to fetch Spine ${description} "${source}" (${response.status}).`);
101
+ return response;
102
+ }
103
+ //#endregion
104
+ //#region src/spine/create-spine-integration.ts
105
+ /** Creates the optional Pixi capability responsible for Replayable Spine assets. */
106
+ function createSpineIntegration() {
107
+ return { setup() {
108
+ return playable.loader.register("spines", loadPixiSpine);
109
+ } };
110
+ }
111
+ //#endregion
112
+ //#region src/spine/factories/create-spine.ts
113
+ const DEFAULT_MIX_SECONDS = .2;
114
+ /** Creates an unattached Spine display object from a loaded Replayable asset ID. */
115
+ function createSpine(options) {
116
+ const skeletonData = resolveSkeletonData(options.skeleton);
117
+ const spine = new Spine({
118
+ autoUpdate: true,
119
+ skeletonData,
120
+ ticker: Ticker.shared
121
+ });
122
+ applyContainerOptions(spine, options);
123
+ spine.state.data.defaultMix = options.defaultMix ?? DEFAULT_MIX_SECONDS;
124
+ spine.state.timeScale = options.speed ?? 1;
125
+ return spine;
126
+ }
127
+ /** Keeps runtime cache access and validation out of playable application code. */
128
+ function resolveSkeletonData(id) {
129
+ const skeletonData = playable.loader.cache.spines?.[id];
130
+ if (!(skeletonData instanceof SkeletonData)) throw new Error(`Spine asset "${id}" has not been loaded.`);
131
+ return skeletonData;
132
+ }
133
+ //#endregion
134
+ export { createSpine, createSpineIntegration };
135
+
136
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/spine/loader/load-pixi-spine.ts","../../src/spine/create-spine-integration.ts","../../src/spine/factories/create-spine.ts"],"sourcesContent":["import {\n AtlasAttachmentLoader,\n SkeletonBinary,\n type SkeletonData,\n SkeletonJson,\n SpineTexture,\n TextureAtlas,\n} from '@esotericsoftware/spine-pixi-v8';\nimport type { AssetLoadContext } from '@replayablejs/runtime';\nimport { Assets, type Texture } from 'pixi.js';\n\nimport type { LoadedSkeleton } from '#types/spine.js';\n\n/**\n * Loads one generated Spine asset and returns the skeleton data cached by runtime.\n * Atlas text, page images, and skeleton data load concurrently. Once available,\n * pages are bound to Pixi textures before Spine parses attachments. Instances made\n * by createSpine share this data; each instance owns its own animation state.\n */\nexport async function loadPixiSpine(context: AssetLoadContext<'spines'>): Promise<SkeletonData> {\n const { id, source } = context;\n\n try {\n const [atlasSource, textures, skeletonSource] = await Promise.all([\n loadAtlasSource(context),\n loadAtlasTextures(source),\n loadSkeletonSource(context),\n ]);\n const atlas = new TextureAtlas(atlasSource);\n\n bindAtlasTextures(id, atlas, textures);\n\n const attachmentLoader = new AtlasAttachmentLoader(atlas);\n\n if (skeletonSource.format === 'json') {\n return new SkeletonJson(attachmentLoader).readSkeletonData(skeletonSource.data);\n }\n\n return new SkeletonBinary(attachmentLoader).readSkeletonData(skeletonSource.data);\n } catch (cause) {\n throw new Error(`Failed to load Replayable Spine asset \"${id}\".`, { cause });\n }\n}\n\n/** Inline mode carries atlas text directly; resource mode carries its fetchable URL. */\nasync function loadAtlasSource(context: AssetLoadContext<'spines'>): Promise<string> {\n if (context.assetMode === 'inline') {\n return context.source.atlas;\n }\n\n const response = await fetchResource(context.source.atlas, 'atlas');\n\n return response.text();\n}\n\n/**\n * Resolves the generated representation into the input expected by Spine's parser.\n * JSON may already be an object. Binary skeletons always arrive through a URL,\n * including a data URL in inline exports. Embedded bytes are decoded locally;\n * only external resource URLs go through fetch.\n */\nasync function loadSkeletonSource(context: AssetLoadContext<'spines'>): Promise<LoadedSkeleton> {\n const { format, skel } = context.source;\n\n if (format === 'json') {\n if (typeof skel !== 'string') {\n return { data: skel, format };\n }\n\n const response = await fetchResource(skel, 'JSON skeleton');\n\n return { data: await response.json(), format };\n }\n\n if (typeof skel !== 'string') {\n throw new Error('A binary Spine skeleton must be emitted as a resource URL.');\n }\n\n if (skel.startsWith('data:')) {\n return { data: decodeInlineSkeleton(skel), format };\n }\n\n const response = await fetchResource(skel, 'binary skeleton');\n\n return { data: new Uint8Array(await response.arrayBuffer()), format };\n}\n\n/**\n * Decodes the Base64 data URL emitted by the asset pipeline without a request.\n * Meta's connect-src policy blocks fetch(data:...) even though the bytes are\n * already embedded. atob preserves binary byte values, including zero and 255.\n * https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src\n */\nfunction decodeInlineSkeleton(source: string): Uint8Array {\n const separator = source.indexOf(',');\n if (separator === -1 || !source.slice(0, separator).endsWith(';base64')) {\n throw new Error('An inline binary Spine skeleton must be a Base64 data URL.');\n }\n\n const binary = atob(source.slice(separator + 1));\n\n return Uint8Array.from(binary, (character) => character.charCodeAt(0));\n}\n\n/** Loads cached Pixi textures in the atlas page order preserved by asset generation. */\nasync function loadAtlasTextures(\n source: AssetLoadContext<'spines'>['source'],\n): Promise<readonly Texture[]> {\n return Promise.all(\n source.images.map(async (image) => {\n const texture = await Assets.load<Texture>(image);\n\n // Generated atlas pages may use smaller physical bitmaps. Resolution\n // restores their authored logical size without changing atlas UVs.\n texture.source.resolution = source.scale;\n texture.update();\n\n return texture;\n }),\n );\n}\n\n/**\n * Pairs atlas pages with generated images by index, after checking their counts.\n * SpineTexture adapts Pixi's texture source for attachment UVs and page sampling;\n * it does not create another browser image loader.\n */\nfunction bindAtlasTextures(id: string, atlas: TextureAtlas, textures: readonly Texture[]): void {\n if (atlas.pages.length !== textures.length) {\n throw new Error(\n `Spine asset \"${id}\" contains ${atlas.pages.length} atlas pages but provides ${textures.length} images.`,\n );\n }\n\n for (const [index, page] of atlas.pages.entries()) {\n const texture = textures[index];\n\n if (texture === undefined) {\n throw new Error(`Spine asset \"${id}\" is missing atlas image ${index}.`);\n }\n\n page.setTexture(SpineTexture.from(texture.source));\n }\n}\n\n/** Rejects HTTP failures with the resource kind and URL before attempting to parse it. */\nasync function fetchResource(source: string, description: string): Promise<Response> {\n const response = await fetch(source);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch Spine ${description} \"${source}\" (${response.status}).`);\n }\n\n return response;\n}\n","import { playable } from '@replayablejs/runtime';\n\nimport { loadPixiSpine } from '#spine/loader/load-pixi-spine.js';\nimport type { PixiIntegration } from '#types/pixi.js';\n\n/** Creates the optional Pixi capability responsible for Replayable Spine assets. */\nexport function createSpineIntegration(): PixiIntegration {\n return {\n setup(): () => void {\n return playable.loader.register('spines', loadPixiSpine);\n },\n };\n}\n","import { SkeletonData, Spine } from '@esotericsoftware/spine-pixi-v8';\nimport { playable } from '@replayablejs/runtime';\nimport { Ticker } from 'pixi.js';\n\nimport { applyContainerOptions } from '#factories/apply-container-options.js';\nimport type { CreateSpineOptions } from '#types/spine.js';\n\nconst DEFAULT_MIX_SECONDS = 0.2;\n\n/** Creates an unattached Spine display object from a loaded Replayable asset ID. */\nexport function createSpine(options: CreateSpineOptions): Spine {\n const skeletonData = resolveSkeletonData(options.skeleton);\n const spine = new Spine({\n // createPixi disables this ticker's own RAF and advances it from\n // playable.update. Spine therefore shares Replayable's frame lifecycle,\n // while Spine.destroy() performs the corresponding listener cleanup.\n autoUpdate: true,\n skeletonData,\n ticker: Ticker.shared,\n });\n\n applyContainerOptions(spine, options);\n spine.state.data.defaultMix = options.defaultMix ?? DEFAULT_MIX_SECONDS;\n spine.state.timeScale = options.speed ?? 1;\n\n return spine;\n}\n\n/** Keeps runtime cache access and validation out of playable application code. */\nfunction resolveSkeletonData(id: string): SkeletonData {\n const skeletonData = playable.loader.cache.spines?.[id];\n\n if (!(skeletonData instanceof SkeletonData)) {\n throw new Error(`Spine asset \"${id}\" has not been loaded.`);\n }\n\n return skeletonData;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@replayablejs/pixi",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "PixiJS rendering integration for Replayable playables",
5
+ "homepage": "https://github.com/replayablejs/replayable#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/replayablejs/replayable/issues"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/replayablejs/replayable.git",
13
+ "directory": "packages/pixi"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "imports": {
21
+ "#factories/*": "./src/factories/*",
22
+ "#integrations/*": "./src/integrations/*",
23
+ "#lifecycle/*": "./src/lifecycle/*",
24
+ "#layout/*": "./src/layout/*",
25
+ "#loader/*": "./src/loader/*",
26
+ "#renderer/*": "./src/renderer/*",
27
+ "#spine/*": "./src/spine/*",
28
+ "#types/*": "./src/types/*"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./spine": {
36
+ "types": "./dist/spine/index.d.ts",
37
+ "default": "./dist/spine/index.js"
38
+ }
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@replayablejs/canvas": "0.1.0-alpha.0"
45
+ },
46
+ "devDependencies": {
47
+ "@esotericsoftware/spine-pixi-v8": "4.3.13",
48
+ "@types/node": "26.2.0",
49
+ "pixi.js": "8.20.1",
50
+ "tsdown": "0.22.14",
51
+ "typescript": "7.0.2",
52
+ "vitest": "4.1.10",
53
+ "@replayablejs/runtime": "0.1.0-alpha.0"
54
+ },
55
+ "peerDependencies": {
56
+ "@esotericsoftware/spine-pixi-v8": "~4.3.13",
57
+ "pixi.js": "^8.20.1",
58
+ "@replayablejs/runtime": "0.1.0-alpha.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@esotericsoftware/spine-pixi-v8": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "engines": {
66
+ "node": ">=24.0.0"
67
+ },
68
+ "scripts": {
69
+ "build": "tsdown",
70
+ "dev": "tsdown --watch",
71
+ "lint": "oxlint --type-aware --max-warnings 0 .",
72
+ "test": "vitest run --passWithNoTests",
73
+ "typecheck": "tsc --noEmit"
74
+ }
75
+ }