@effing/annie-player 0.38.4 → 0.40.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
|
-
import {
|
|
2
|
+
import { annieFrames } from "@effing/annie";
|
|
3
3
|
var AnniePlayerCore = class {
|
|
4
4
|
src;
|
|
5
5
|
fps;
|
|
@@ -101,6 +101,7 @@ var AnniePlayerCore = class {
|
|
|
101
101
|
this.cleanup();
|
|
102
102
|
const imageFiles = [];
|
|
103
103
|
const imageLoadPromises = [];
|
|
104
|
+
const objectUrls = [];
|
|
104
105
|
try {
|
|
105
106
|
const response = await fetch(this.src);
|
|
106
107
|
if (!response.ok) {
|
|
@@ -110,35 +111,32 @@ var AnniePlayerCore = class {
|
|
|
110
111
|
throw new Error("Response body is null");
|
|
111
112
|
}
|
|
112
113
|
this.setStatus("Extracting frames...");
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
for await (const frame of annieFrames(response.body)) {
|
|
115
|
+
const blob = new Blob([frame.data], {
|
|
116
|
+
type: frame.contentType
|
|
117
|
+
});
|
|
117
118
|
const promise = new Promise((resolve, reject) => {
|
|
118
119
|
const img = new Image();
|
|
119
120
|
img.onload = () => {
|
|
120
|
-
imageFiles.push({
|
|
121
|
-
if (imageFiles.length === 1) {
|
|
122
|
-
this._dimensions = {
|
|
123
|
-
width: img.naturalWidth,
|
|
124
|
-
height: img.naturalHeight
|
|
125
|
-
};
|
|
126
|
-
}
|
|
121
|
+
imageFiles.push({ index: frame.index, image: img });
|
|
127
122
|
resolve();
|
|
128
123
|
};
|
|
129
124
|
img.onerror = (err) => {
|
|
130
|
-
reject(new Error(`Could not load image: ${
|
|
125
|
+
reject(new Error(`Could not load image: ${frame.name} (${err})`));
|
|
131
126
|
};
|
|
132
|
-
|
|
127
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
128
|
+
objectUrls.push(objectUrl);
|
|
129
|
+
img.src = objectUrl;
|
|
133
130
|
});
|
|
134
131
|
imageLoadPromises.push(promise);
|
|
135
|
-
});
|
|
136
|
-
await Promise.all(imageLoadPromises);
|
|
137
|
-
if (imageFiles.length === 0) {
|
|
138
|
-
throw new Error("No frames found in the TAR file.");
|
|
139
132
|
}
|
|
140
|
-
|
|
133
|
+
await Promise.all(imageLoadPromises);
|
|
134
|
+
imageFiles.sort((a, b) => a.index - b.index);
|
|
141
135
|
this.frames = imageFiles.map((f) => f.image);
|
|
136
|
+
this._dimensions = {
|
|
137
|
+
width: this.frames[0].naturalWidth,
|
|
138
|
+
height: this.frames[0].naturalHeight
|
|
139
|
+
};
|
|
142
140
|
this._isLoading = false;
|
|
143
141
|
this.setStatus(`Loaded ${this.frames.length} frames. Ready to play.`);
|
|
144
142
|
this.emit("load", {
|
|
@@ -149,6 +147,7 @@ var AnniePlayerCore = class {
|
|
|
149
147
|
this.play();
|
|
150
148
|
}
|
|
151
149
|
} catch (err) {
|
|
150
|
+
objectUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
152
151
|
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
153
152
|
this._isLoading = false;
|
|
154
153
|
this.setError(errorMessage);
|
|
@@ -266,4 +265,4 @@ var AnniePlayerCore = class {
|
|
|
266
265
|
export {
|
|
267
266
|
AnniePlayerCore
|
|
268
267
|
};
|
|
269
|
-
//# sourceMappingURL=chunk-
|
|
268
|
+
//# sourceMappingURL=chunk-KBVFZAAO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["import { annieFrames } from \"@effing/annie\";\n\nexport type AnniePlayerState = {\n status: string;\n error: string | null;\n isLoading: boolean;\n isPlaying: boolean;\n frameCount: number;\n currentFrame: number;\n dimensions: { width: number; height: number } | null;\n};\n\nexport type AnniePlayerEvents = {\n statechange: AnniePlayerState;\n load: { frameCount: number; dimensions: { width: number; height: number } };\n error: Error;\n};\n\nexport type AnniePlayerOptions = {\n src: string;\n fps?: number;\n autoPlay?: boolean;\n};\n\ntype EventCallback<T> = (data: T) => void;\n\n/**\n * Framework-agnostic Annie animation player.\n * Handles loading TAR archives of image frames and playing them on a canvas.\n */\nexport class AnniePlayerCore {\n private src: string;\n private fps: number;\n private autoPlay: boolean;\n private frameInterval: number;\n\n private frames: HTMLImageElement[] = [];\n private canvas: HTMLCanvasElement | null = null;\n private context: CanvasRenderingContext2D | null = null;\n\n private animationFrameId: number | null = null;\n private currentFrameIndex = 0;\n private lastFrameTime = 0;\n\n private _isLoading = false;\n private _isPlaying = false;\n private _error: string | null = null;\n private _status = \"Ready to load animation.\";\n private _dimensions: { width: number; height: number } | null = null;\n\n private listeners: Map<\n keyof AnniePlayerEvents,\n Set<EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>>\n > = new Map();\n\n constructor(options: AnniePlayerOptions) {\n this.src = options.src;\n this.fps = options.fps ?? 30;\n this.autoPlay = options.autoPlay ?? false;\n this.frameInterval = 1000 / this.fps;\n }\n\n /**\n * Attach a canvas element to render the animation on.\n */\n attachCanvas(canvas: HTMLCanvasElement): void {\n this.canvas = canvas;\n this.context = canvas.getContext(\"2d\");\n }\n\n /**\n * Detach the canvas and stop playback.\n */\n detachCanvas(): void {\n this.stop();\n this.canvas = null;\n this.context = null;\n }\n\n /**\n * Get the current state of the player.\n */\n getState(): AnniePlayerState {\n return {\n status: this._status,\n error: this._error,\n isLoading: this._isLoading,\n isPlaying: this._isPlaying,\n frameCount: this.frames.length,\n currentFrame: this.currentFrameIndex,\n dimensions: this._dimensions,\n };\n }\n\n /**\n * Subscribe to player events.\n */\n on<K extends keyof AnniePlayerEvents>(\n event: K,\n callback: EventCallback<AnniePlayerEvents[K]>,\n ): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n const callbacks = this.listeners.get(event)!;\n callbacks.add(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n\n return () => {\n callbacks.delete(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n };\n }\n\n private emit<K extends keyof AnniePlayerEvents>(\n event: K,\n data: AnniePlayerEvents[K],\n ): void {\n const callbacks = this.listeners.get(event);\n if (callbacks) {\n callbacks.forEach((cb) => cb(data));\n }\n }\n\n private emitStateChange(): void {\n this.emit(\"statechange\", this.getState());\n }\n\n private setStatus(status: string): void {\n this._status = status;\n this.emitStateChange();\n }\n\n private setError(error: string | null): void {\n this._error = error;\n if (error) {\n this.emit(\"error\", new Error(error));\n }\n this.emitStateChange();\n }\n\n /**\n * Load the animation from the source URL.\n */\n async load(): Promise<void> {\n if (this._isLoading) return;\n\n this.stop();\n this._isLoading = true;\n this._error = null;\n this.setStatus(\"Loading Annie file...\");\n this.cleanup();\n\n const imageFiles: { index: number; image: HTMLImageElement }[] = [];\n const imageLoadPromises: Promise<void>[] = [];\n const objectUrls: string[] = [];\n\n try {\n const response = await fetch(this.src);\n if (!response.ok) {\n throw new Error(`Failed to fetch animation: ${response.statusText}`);\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n this.setStatus(\"Extracting frames...\");\n\n for await (const frame of annieFrames(response.body)) {\n const blob = new Blob([frame.data as BlobPart], {\n type: frame.contentType,\n });\n\n const promise = new Promise<void>((resolve, reject) => {\n const img = new Image();\n img.onload = () => {\n imageFiles.push({ index: frame.index, image: img });\n resolve();\n };\n img.onerror = (err) => {\n reject(new Error(`Could not load image: ${frame.name} (${err})`));\n };\n const objectUrl = URL.createObjectURL(blob);\n objectUrls.push(objectUrl);\n img.src = objectUrl;\n });\n imageLoadPromises.push(promise);\n }\n\n await Promise.all(imageLoadPromises);\n\n imageFiles.sort((a, b) => a.index - b.index);\n this.frames = imageFiles.map((f) => f.image);\n this._dimensions = {\n width: this.frames[0].naturalWidth,\n height: this.frames[0].naturalHeight,\n };\n this._isLoading = false;\n this.setStatus(`Loaded ${this.frames.length} frames. Ready to play.`);\n\n this.emit(\"load\", {\n frameCount: this.frames.length,\n dimensions: this._dimensions!,\n });\n\n if (this.autoPlay && this.frames.length > 0) {\n this.play();\n }\n } catch (err) {\n // this.frames is still empty, so cleanup() can't reach these URLs\n objectUrls.forEach((url) => URL.revokeObjectURL(url));\n const errorMessage = err instanceof Error ? err.message : \"Unknown error\";\n this._isLoading = false;\n this.setError(errorMessage);\n this.setStatus(`Error: ${errorMessage}`);\n }\n }\n\n /**\n * Start playing the animation from the current frame.\n */\n play(): void {\n if (this._isPlaying || this.frames.length === 0) return;\n\n this._isPlaying = true;\n this.lastFrameTime = 0;\n this.drawFrame(this.currentFrameIndex);\n this.setStatus(\"Playing...\");\n this.animationFrameId = requestAnimationFrame(this.animate);\n }\n\n /**\n * Pause the animation at the current frame.\n */\n pause(): void {\n if (!this._isPlaying) return;\n\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.setStatus(\"Paused.\");\n }\n\n /**\n * Stop the animation and reset to the first frame.\n */\n stop(): void {\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.currentFrameIndex = 0;\n this.lastFrameTime = 0;\n }\n\n /**\n * Seek to a specific frame. The frame is drawn immediately. Playback state\n * is preserved — if playing, playback continues forward from the new frame.\n */\n seek(frameIndex: number): void {\n if (this.frames.length === 0) return;\n const clamped = Math.max(\n 0,\n Math.min(Math.floor(frameIndex), this.frames.length - 1),\n );\n this.lastFrameTime = 0;\n this.drawFrame(clamped);\n }\n\n private drawFrame(index: number): void {\n const indexChanged = index !== this.currentFrameIndex;\n if (!this.canvas || !this.context) {\n if (indexChanged) {\n this.currentFrameIndex = index;\n this.emitStateChange();\n }\n return;\n }\n const frame = this.frames[index];\n if (!frame) return;\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n if (indexChanged) {\n this.currentFrameIndex = index;\n this.emitStateChange();\n }\n }\n\n private animate = (timestamp: number): void => {\n if (!this._isPlaying || this.frames.length === 0) {\n this.animationFrameId = null;\n return;\n }\n\n if (!this.canvas || !this.context) {\n this.animationFrameId = null;\n return;\n }\n\n if (this.lastFrameTime === 0) {\n this.lastFrameTime = timestamp;\n }\n\n const elapsed = timestamp - this.lastFrameTime;\n\n if (elapsed >= this.frameInterval) {\n this.lastFrameTime = timestamp - (elapsed % this.frameInterval);\n const nextIndex = (this.currentFrameIndex + 1) % this.frames.length;\n this.drawFrame(nextIndex);\n }\n\n this.animationFrameId = requestAnimationFrame(this.animate);\n };\n\n /**\n * Clean up resources (revoke blob URLs).\n */\n cleanup(): void {\n this.frames.forEach((img) => {\n if (img.src.startsWith(\"blob:\")) {\n URL.revokeObjectURL(img.src);\n }\n });\n this.frames = [];\n }\n\n /**\n * Destroy the player and clean up all resources.\n */\n destroy(): void {\n this.stop();\n this.cleanup();\n this.detachCanvas();\n this.listeners.clear();\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
package/dist/react/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effing/annie-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Annie animation player that runs in the browser",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@
|
|
25
|
+
"@effing/annie": "0.40.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/react": "^19.0.0",
|
|
29
|
+
"jsdom": "^29.1.1",
|
|
29
30
|
"tsup": "^8.0.0",
|
|
30
|
-
"typescript": "^5.9.3"
|
|
31
|
+
"typescript": "^5.9.3",
|
|
32
|
+
"vitest": "^3.2.4"
|
|
31
33
|
},
|
|
32
34
|
"peerDependencies": {
|
|
33
35
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
},
|
|
50
52
|
"scripts": {
|
|
51
53
|
"build": "tsup",
|
|
52
|
-
"typecheck": "tsc --noEmit"
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run"
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["import { untar } from \"@andrewbranch/untar.js\";\n\nexport type AnniePlayerState = {\n status: string;\n error: string | null;\n isLoading: boolean;\n isPlaying: boolean;\n frameCount: number;\n currentFrame: number;\n dimensions: { width: number; height: number } | null;\n};\n\nexport type AnniePlayerEvents = {\n statechange: AnniePlayerState;\n load: { frameCount: number; dimensions: { width: number; height: number } };\n error: Error;\n};\n\nexport type AnniePlayerOptions = {\n src: string;\n fps?: number;\n autoPlay?: boolean;\n};\n\ntype EventCallback<T> = (data: T) => void;\n\n/**\n * Framework-agnostic Annie animation player.\n * Handles loading TAR archives of image frames and playing them on a canvas.\n */\nexport class AnniePlayerCore {\n private src: string;\n private fps: number;\n private autoPlay: boolean;\n private frameInterval: number;\n\n private frames: HTMLImageElement[] = [];\n private canvas: HTMLCanvasElement | null = null;\n private context: CanvasRenderingContext2D | null = null;\n\n private animationFrameId: number | null = null;\n private currentFrameIndex = 0;\n private lastFrameTime = 0;\n\n private _isLoading = false;\n private _isPlaying = false;\n private _error: string | null = null;\n private _status = \"Ready to load animation.\";\n private _dimensions: { width: number; height: number } | null = null;\n\n private listeners: Map<\n keyof AnniePlayerEvents,\n Set<EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>>\n > = new Map();\n\n constructor(options: AnniePlayerOptions) {\n this.src = options.src;\n this.fps = options.fps ?? 30;\n this.autoPlay = options.autoPlay ?? false;\n this.frameInterval = 1000 / this.fps;\n }\n\n /**\n * Attach a canvas element to render the animation on.\n */\n attachCanvas(canvas: HTMLCanvasElement): void {\n this.canvas = canvas;\n this.context = canvas.getContext(\"2d\");\n }\n\n /**\n * Detach the canvas and stop playback.\n */\n detachCanvas(): void {\n this.stop();\n this.canvas = null;\n this.context = null;\n }\n\n /**\n * Get the current state of the player.\n */\n getState(): AnniePlayerState {\n return {\n status: this._status,\n error: this._error,\n isLoading: this._isLoading,\n isPlaying: this._isPlaying,\n frameCount: this.frames.length,\n currentFrame: this.currentFrameIndex,\n dimensions: this._dimensions,\n };\n }\n\n /**\n * Subscribe to player events.\n */\n on<K extends keyof AnniePlayerEvents>(\n event: K,\n callback: EventCallback<AnniePlayerEvents[K]>,\n ): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n const callbacks = this.listeners.get(event)!;\n callbacks.add(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n\n return () => {\n callbacks.delete(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n };\n }\n\n private emit<K extends keyof AnniePlayerEvents>(\n event: K,\n data: AnniePlayerEvents[K],\n ): void {\n const callbacks = this.listeners.get(event);\n if (callbacks) {\n callbacks.forEach((cb) => cb(data));\n }\n }\n\n private emitStateChange(): void {\n this.emit(\"statechange\", this.getState());\n }\n\n private setStatus(status: string): void {\n this._status = status;\n this.emitStateChange();\n }\n\n private setError(error: string | null): void {\n this._error = error;\n if (error) {\n this.emit(\"error\", new Error(error));\n }\n this.emitStateChange();\n }\n\n /**\n * Load the animation from the source URL.\n */\n async load(): Promise<void> {\n if (this._isLoading) return;\n\n this.stop();\n this._isLoading = true;\n this._error = null;\n this.setStatus(\"Loading Annie file...\");\n this.cleanup();\n\n const imageFiles: { name: string; image: HTMLImageElement }[] = [];\n const imageLoadPromises: Promise<void>[] = [];\n\n try {\n const response = await fetch(this.src);\n if (!response.ok) {\n throw new Error(`Failed to fetch animation: ${response.statusText}`);\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n this.setStatus(\"Extracting frames...\");\n\n untar(await response.arrayBuffer()).forEach((file) => {\n if (!file.name || file.typeflag !== \"0\") return;\n if (!file.fileData) return;\n\n const blob = new Blob([new Uint8Array(file.fileData)]);\n\n const promise = new Promise<void>((resolve, reject) => {\n const img = new Image();\n img.onload = () => {\n imageFiles.push({ name: file.name!, image: img });\n if (imageFiles.length === 1) {\n this._dimensions = {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n resolve();\n };\n img.onerror = (err) => {\n reject(new Error(`Could not load image: ${file.name} (${err})`));\n };\n img.src = URL.createObjectURL(blob);\n });\n imageLoadPromises.push(promise);\n });\n\n await Promise.all(imageLoadPromises);\n\n if (imageFiles.length === 0) {\n throw new Error(\"No frames found in the TAR file.\");\n }\n\n imageFiles.sort((a, b) => a.name.localeCompare(b.name));\n this.frames = imageFiles.map((f) => f.image);\n this._isLoading = false;\n this.setStatus(`Loaded ${this.frames.length} frames. Ready to play.`);\n\n this.emit(\"load\", {\n frameCount: this.frames.length,\n dimensions: this._dimensions!,\n });\n\n if (this.autoPlay && this.frames.length > 0) {\n this.play();\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : \"Unknown error\";\n this._isLoading = false;\n this.setError(errorMessage);\n this.setStatus(`Error: ${errorMessage}`);\n }\n }\n\n /**\n * Start playing the animation from the current frame.\n */\n play(): void {\n if (this._isPlaying || this.frames.length === 0) return;\n\n this._isPlaying = true;\n this.lastFrameTime = 0;\n this.drawFrame(this.currentFrameIndex);\n this.setStatus(\"Playing...\");\n this.animationFrameId = requestAnimationFrame(this.animate);\n }\n\n /**\n * Pause the animation at the current frame.\n */\n pause(): void {\n if (!this._isPlaying) return;\n\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.setStatus(\"Paused.\");\n }\n\n /**\n * Stop the animation and reset to the first frame.\n */\n stop(): void {\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.currentFrameIndex = 0;\n this.lastFrameTime = 0;\n }\n\n /**\n * Seek to a specific frame. The frame is drawn immediately. Playback state\n * is preserved — if playing, playback continues forward from the new frame.\n */\n seek(frameIndex: number): void {\n if (this.frames.length === 0) return;\n const clamped = Math.max(\n 0,\n Math.min(Math.floor(frameIndex), this.frames.length - 1),\n );\n this.lastFrameTime = 0;\n this.drawFrame(clamped);\n }\n\n private drawFrame(index: number): void {\n const indexChanged = index !== this.currentFrameIndex;\n if (!this.canvas || !this.context) {\n if (indexChanged) {\n this.currentFrameIndex = index;\n this.emitStateChange();\n }\n return;\n }\n const frame = this.frames[index];\n if (!frame) return;\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n if (indexChanged) {\n this.currentFrameIndex = index;\n this.emitStateChange();\n }\n }\n\n private animate = (timestamp: number): void => {\n if (!this._isPlaying || this.frames.length === 0) {\n this.animationFrameId = null;\n return;\n }\n\n if (!this.canvas || !this.context) {\n this.animationFrameId = null;\n return;\n }\n\n if (this.lastFrameTime === 0) {\n this.lastFrameTime = timestamp;\n }\n\n const elapsed = timestamp - this.lastFrameTime;\n\n if (elapsed >= this.frameInterval) {\n this.lastFrameTime = timestamp - (elapsed % this.frameInterval);\n const nextIndex = (this.currentFrameIndex + 1) % this.frames.length;\n this.drawFrame(nextIndex);\n }\n\n this.animationFrameId = requestAnimationFrame(this.animate);\n };\n\n /**\n * Clean up resources (revoke blob URLs).\n */\n cleanup(): void {\n this.frames.forEach((img) => {\n if (img.src.startsWith(\"blob:\")) {\n URL.revokeObjectURL(img.src);\n }\n });\n this.frames = [];\n }\n\n /**\n * Destroy the player and clean up all resources.\n */\n destroy(): void {\n this.stop();\n this.cleanup();\n this.detachCanvas();\n this.listeners.clear();\n }\n}\n"],"mappings":";AAAA,SAAS,aAAa;AA8Bf,IAAM,kBAAN,MAAsB;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,SAA6B,CAAC;AAAA,EAC9B,SAAmC;AAAA,EACnC,UAA2C;AAAA,EAE3C,mBAAkC;AAAA,EAClC,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAEhB,aAAa;AAAA,EACb,aAAa;AAAA,EACb,SAAwB;AAAA,EACxB,UAAU;AAAA,EACV,cAAwD;AAAA,EAExD,YAGJ,oBAAI,IAAI;AAAA,EAEZ,YAAY,SAA6B;AACvC,SAAK,MAAM,QAAQ;AACnB,SAAK,MAAM,QAAQ,OAAO;AAC1B,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,gBAAgB,MAAO,KAAK;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,QAAiC;AAC5C,SAAK,SAAS;AACd,SAAK,UAAU,OAAO,WAAW,IAAI;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA,EAKA,eAAqB;AACnB,SAAK,KAAK;AACV,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,WAA6B;AAC3B,WAAO;AAAA,MACL,QAAQ,KAAK;AAAA,MACb,OAAO,KAAK;AAAA,MACZ,WAAW,KAAK;AAAA,MAChB,WAAW,KAAK;AAAA,MAChB,YAAY,KAAK,OAAO;AAAA,MACxB,cAAc,KAAK;AAAA,MACnB,YAAY,KAAK;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,GACE,OACA,UACY;AACZ,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,oBAAI,IAAI,CAAC;AAAA,IACrC;AACA,UAAM,YAAY,KAAK,UAAU,IAAI,KAAK;AAC1C,cAAU;AAAA,MACR;AAAA,IACF;AAEA,WAAO,MAAM;AACX,gBAAU;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,KACN,OACA,MACM;AACN,UAAM,YAAY,KAAK,UAAU,IAAI,KAAK;AAC1C,QAAI,WAAW;AACb,gBAAU,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IACpC;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,SAAK,KAAK,eAAe,KAAK,SAAS,CAAC;AAAA,EAC1C;AAAA,EAEQ,UAAU,QAAsB;AACtC,SAAK,UAAU;AACf,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEQ,SAAS,OAA4B;AAC3C,SAAK,SAAS;AACd,QAAI,OAAO;AACT,WAAK,KAAK,SAAS,IAAI,MAAM,KAAK,CAAC;AAAA,IACrC;AACA,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,QAAI,KAAK,WAAY;AAErB,SAAK,KAAK;AACV,SAAK,aAAa;AAClB,SAAK,SAAS;AACd,SAAK,UAAU,uBAAuB;AACtC,SAAK,QAAQ;AAEb,UAAM,aAA0D,CAAC;AACjE,UAAM,oBAAqC,CAAC;AAE5C,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK,GAAG;AACrC,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI,MAAM,8BAA8B,SAAS,UAAU,EAAE;AAAA,MACrE;AACA,UAAI,CAAC,SAAS,MAAM;AAClB,cAAM,IAAI,MAAM,uBAAuB;AAAA,MACzC;AAEA,WAAK,UAAU,sBAAsB;AAErC,YAAM,MAAM,SAAS,YAAY,CAAC,EAAE,QAAQ,CAAC,SAAS;AACpD,YAAI,CAAC,KAAK,QAAQ,KAAK,aAAa,IAAK;AACzC,YAAI,CAAC,KAAK,SAAU;AAEpB,cAAM,OAAO,IAAI,KAAK,CAAC,IAAI,WAAW,KAAK,QAAQ,CAAC,CAAC;AAErD,cAAM,UAAU,IAAI,QAAc,CAAC,SAAS,WAAW;AACrD,gBAAM,MAAM,IAAI,MAAM;AACtB,cAAI,SAAS,MAAM;AACjB,uBAAW,KAAK,EAAE,MAAM,KAAK,MAAO,OAAO,IAAI,CAAC;AAChD,gBAAI,WAAW,WAAW,GAAG;AAC3B,mBAAK,cAAc;AAAA,gBACjB,OAAO,IAAI;AAAA,gBACX,QAAQ,IAAI;AAAA,cACd;AAAA,YACF;AACA,oBAAQ;AAAA,UACV;AACA,cAAI,UAAU,CAAC,QAAQ;AACrB,mBAAO,IAAI,MAAM,yBAAyB,KAAK,IAAI,KAAK,GAAG,GAAG,CAAC;AAAA,UACjE;AACA,cAAI,MAAM,IAAI,gBAAgB,IAAI;AAAA,QACpC,CAAC;AACD,0BAAkB,KAAK,OAAO;AAAA,MAChC,CAAC;AAED,YAAM,QAAQ,IAAI,iBAAiB;AAEnC,UAAI,WAAW,WAAW,GAAG;AAC3B,cAAM,IAAI,MAAM,kCAAkC;AAAA,MACpD;AAEA,iBAAW,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACtD,WAAK,SAAS,WAAW,IAAI,CAAC,MAAM,EAAE,KAAK;AAC3C,WAAK,aAAa;AAClB,WAAK,UAAU,UAAU,KAAK,OAAO,MAAM,yBAAyB;AAEpE,WAAK,KAAK,QAAQ;AAAA,QAChB,YAAY,KAAK,OAAO;AAAA,QACxB,YAAY,KAAK;AAAA,MACnB,CAAC;AAED,UAAI,KAAK,YAAY,KAAK,OAAO,SAAS,GAAG;AAC3C,aAAK,KAAK;AAAA,MACZ;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,eAAe,eAAe,QAAQ,IAAI,UAAU;AAC1D,WAAK,aAAa;AAClB,WAAK,SAAS,YAAY;AAC1B,WAAK,UAAU,UAAU,YAAY,EAAE;AAAA,IACzC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,QAAI,KAAK,cAAc,KAAK,OAAO,WAAW,EAAG;AAEjD,SAAK,aAAa;AAClB,SAAK,gBAAgB;AACrB,SAAK,UAAU,KAAK,iBAAiB;AACrC,SAAK,UAAU,YAAY;AAC3B,SAAK,mBAAmB,sBAAsB,KAAK,OAAO;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,QAAI,CAAC,KAAK,WAAY;AAEtB,SAAK,aAAa;AAClB,QAAI,KAAK,qBAAqB,MAAM;AAClC,2BAAqB,KAAK,gBAAgB;AAC1C,WAAK,mBAAmB;AAAA,IAC1B;AACA,SAAK,UAAU,SAAS;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,SAAK,aAAa;AAClB,QAAI,KAAK,qBAAqB,MAAM;AAClC,2BAAqB,KAAK,gBAAgB;AAC1C,WAAK,mBAAmB;AAAA,IAC1B;AACA,SAAK,oBAAoB;AACzB,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK,YAA0B;AAC7B,QAAI,KAAK,OAAO,WAAW,EAAG;AAC9B,UAAM,UAAU,KAAK;AAAA,MACnB;AAAA,MACA,KAAK,IAAI,KAAK,MAAM,UAAU,GAAG,KAAK,OAAO,SAAS,CAAC;AAAA,IACzD;AACA,SAAK,gBAAgB;AACrB,SAAK,UAAU,OAAO;AAAA,EACxB;AAAA,EAEQ,UAAU,OAAqB;AACrC,UAAM,eAAe,UAAU,KAAK;AACpC,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,SAAS;AACjC,UAAI,cAAc;AAChB,aAAK,oBAAoB;AACzB,aAAK,gBAAgB;AAAA,MACvB;AACA;AAAA,IACF;AACA,UAAM,QAAQ,KAAK,OAAO,KAAK;AAC/B,QAAI,CAAC,MAAO;AACZ,SAAK,QAAQ,UAAU,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AAClE,SAAK,QAAQ,UAAU,OAAO,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AACzE,QAAI,cAAc;AAChB,WAAK,oBAAoB;AACzB,WAAK,gBAAgB;AAAA,IACvB;AAAA,EACF;AAAA,EAEQ,UAAU,CAAC,cAA4B;AAC7C,QAAI,CAAC,KAAK,cAAc,KAAK,OAAO,WAAW,GAAG;AAChD,WAAK,mBAAmB;AACxB;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,SAAS;AACjC,WAAK,mBAAmB;AACxB;AAAA,IACF;AAEA,QAAI,KAAK,kBAAkB,GAAG;AAC5B,WAAK,gBAAgB;AAAA,IACvB;AAEA,UAAM,UAAU,YAAY,KAAK;AAEjC,QAAI,WAAW,KAAK,eAAe;AACjC,WAAK,gBAAgB,YAAa,UAAU,KAAK;AACjD,YAAM,aAAa,KAAK,oBAAoB,KAAK,KAAK,OAAO;AAC7D,WAAK,UAAU,SAAS;AAAA,IAC1B;AAEA,SAAK,mBAAmB,sBAAsB,KAAK,OAAO;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,SAAK,OAAO,QAAQ,CAAC,QAAQ;AAC3B,UAAI,IAAI,IAAI,WAAW,OAAO,GAAG;AAC/B,YAAI,gBAAgB,IAAI,GAAG;AAAA,MAC7B;AAAA,IACF,CAAC;AACD,SAAK,SAAS,CAAC;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,SAAK,KAAK;AACV,SAAK,QAAQ;AACb,SAAK,aAAa;AAClB,SAAK,UAAU,MAAM;AAAA,EACvB;AACF;","names":[]}
|