mason-sprite 0.1.1 → 0.1.2
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.
- package/LICENSE +21 -0
- package/README.md +145 -6
- package/dist/index.cjs +57 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -5
- package/dist/index.d.ts +12 -5
- package/dist/index.js +55 -10
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +68 -16
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +6 -4
- package/dist/react/index.d.ts +6 -4
- package/dist/react/index.js +68 -16
- package/dist/react/index.js.map +1 -1
- package/dist/svelte/Sprite.svelte +3 -2
- package/dist/svelte/Sprite.svelte.d.ts.map +1 -1
- package/dist/vue/Sprite.vue.d.ts +2 -2
- package/dist/vue/Sprite.vue.d.ts.map +1 -1
- package/dist/vue/index.cjs +1 -1
- package/dist/vue/index.js +89 -69
- package/package.json +37 -26
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/constants.ts","../src/core/utils.ts","../src/core/canvas-renderer.ts","../src/core/css-renderer.ts","../src/core/sprite-animator.ts"],"sourcesContent":["import type { RendererMode } from './types.js';\n\nexport const SPRITE_ANIMATION_DEFAULTS = {\n fps: 12,\n loop: true,\n width: 128,\n height: 128,\n autoPlay: true,\n renderer: 'css' as RendererMode,\n} as const;\n","import type { FramePosition } from './types.js';\n\nexport function getTotalFrames(rows: number, cols: number): number {\n return rows * cols;\n}\n\nexport function getFramePosition(frameIndex: number, cols: number): FramePosition {\n return {\n row: Math.floor(frameIndex / cols),\n col: frameIndex % cols,\n };\n}\n\nexport function getBackgroundPositionPercent(\n frameIndex: number,\n rows: number,\n cols: number,\n): { x: number; y: number } {\n const { row, col } = getFramePosition(frameIndex, cols);\n const x = cols <= 1 ? 0 : (col / (cols - 1)) * 100;\n const y = rows <= 1 ? 0 : (row / (rows - 1)) * 100;\n return { x, y };\n}\n","import { getFramePosition } from './utils.js';\n\nexport function drawCanvasFrame(\n canvas: HTMLCanvasElement,\n image: HTMLImageElement,\n frameIndex: number,\n rows: number,\n cols: number,\n width: number,\n height: number,\n): void {\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const frameWidth = image.naturalWidth / cols;\n const frameHeight = image.naturalHeight / rows;\n const { row, col } = getFramePosition(frameIndex, cols);\n\n canvas.width = width;\n canvas.height = height;\n\n ctx.clearRect(0, 0, width, height);\n ctx.drawImage(\n image,\n col * frameWidth,\n row * frameHeight,\n frameWidth,\n frameHeight,\n 0,\n 0,\n width,\n height,\n );\n}\n","import { getBackgroundPositionPercent } from './utils.js';\n\nexport interface CssRendererTarget {\n style: CSSStyleDeclaration;\n}\n\nexport function applyCssFrame(\n target: CssRendererTarget,\n src: string,\n frameIndex: number,\n rows: number,\n cols: number,\n width: number,\n height: number,\n): void {\n const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);\n\n target.style.backgroundImage = `url(\"${src}\")`;\n target.style.backgroundRepeat = 'no-repeat';\n target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;\n target.style.backgroundPosition = `${x}% ${y}%`;\n target.style.width = `${width}px`;\n target.style.height = `${height}px`;\n target.style.display = 'inline-block';\n}\n\nexport function resetCssRenderer(target: CssRendererTarget): void {\n target.style.backgroundImage = '';\n}\n","import { SPRITE_ANIMATION_DEFAULTS } from './constants.js';\nimport { drawCanvasFrame } from './canvas-renderer.js';\nimport { applyCssFrame, resetCssRenderer } from './css-renderer.js';\nimport type { SpriteAnimationOptions, SpriteAnimationState } from './types.js';\nimport { getTotalFrames } from './utils.js';\n\ntype StateListener = (state: SpriteAnimationState) => void;\n\ntype ResolvedSpriteAnimationOptions = Required<\n Pick<SpriteAnimationOptions, 'src' | 'rows' | 'cols'>\n> &\n Required<Pick<SpriteAnimationOptions, 'fps' | 'loop' | 'width' | 'height' | 'autoPlay' | 'renderer'>> &\n Pick<SpriteAnimationOptions, 'onComplete' | 'onFrameChange'>;\n\nexport class SpriteAnimator {\n private options: ResolvedSpriteAnimationOptions;\n private currentFrame = 0;\n private isPlaying = false;\n private isLoaded = false;\n private rafId: number | null = null;\n private lastTimestamp = 0;\n private accumulatedTime = 0;\n private image: HTMLImageElement | null = null;\n private target: HTMLElement | HTMLCanvasElement | null = null;\n private listeners = new Set<StateListener>();\n private destroyed = false;\n\n constructor(options: SpriteAnimationOptions) {\n this.options = {\n ...SPRITE_ANIMATION_DEFAULTS,\n ...options,\n };\n this.loadImage();\n }\n\n attach(target: HTMLElement | HTMLCanvasElement): void {\n this.target = target;\n if (this.isLoaded) {\n this.render();\n }\n if (this.options.autoPlay) {\n this.play();\n }\n }\n\n play(): void {\n if (this.destroyed || this.isPlaying) return;\n this.isPlaying = true;\n this.lastTimestamp = 0;\n this.accumulatedTime = 0;\n this.rafId = requestAnimationFrame(this.tick);\n this.notify();\n }\n\n pause(): void {\n if (!this.isPlaying) return;\n this.isPlaying = false;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.notify();\n }\n\n stop(): void {\n this.pause();\n this.currentFrame = 0;\n this.render();\n this.notify();\n }\n\n goToFrame(frame: number): void {\n const total = this.getTotalFrames();\n this.currentFrame = Math.max(0, Math.min(frame, total - 1));\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n getState(): SpriteAnimationState {\n return {\n currentFrame: this.currentFrame,\n totalFrames: this.getTotalFrames(),\n isPlaying: this.isPlaying,\n isLoaded: this.isLoaded,\n };\n }\n\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => this.listeners.delete(listener);\n }\n\n updateOptions(partial: Partial<SpriteAnimationOptions>): void {\n const prevSrc = this.options.src;\n const prevFps = this.options.fps;\n this.options = { ...this.options, ...partial };\n\n if (partial.src !== undefined && partial.src !== prevSrc) {\n this.loadImage();\n } else if (this.isLoaded) {\n this.render();\n }\n\n if (partial.fps !== undefined && partial.fps !== prevFps) {\n this.accumulatedTime = 0;\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n this.pause();\n this.listeners.clear();\n if (this.target && this.options.renderer === 'css') {\n resetCssRenderer(this.target);\n }\n this.target = null;\n this.image = null;\n }\n\n private getTotalFrames(): number {\n return getTotalFrames(this.options.rows, this.options.cols);\n }\n\n private loadImage(): void {\n this.isLoaded = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n if (this.destroyed) return;\n this.image = img;\n this.isLoaded = true;\n\n if (!this.options.width || !this.options.height) {\n const frameWidth = img.naturalWidth / this.options.cols;\n const frameHeight = img.naturalHeight / this.options.rows;\n this.options.width = frameWidth;\n this.options.height = frameHeight;\n }\n\n this.render();\n this.notify();\n\n if (this.options.autoPlay && this.target) {\n this.play();\n }\n };\n img.onerror = () => {\n console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);\n };\n img.src = this.options.src;\n }\n\n private tick = (timestamp: number): void => {\n if (!this.isPlaying || this.destroyed) return;\n\n if (this.lastTimestamp === 0) {\n this.lastTimestamp = timestamp;\n }\n\n const delta = timestamp - this.lastTimestamp;\n this.lastTimestamp = timestamp;\n this.accumulatedTime += delta;\n\n const frameDuration = 1000 / this.options.fps;\n while (this.accumulatedTime >= frameDuration) {\n this.accumulatedTime -= frameDuration;\n this.advanceFrame();\n }\n\n this.rafId = requestAnimationFrame(this.tick);\n };\n\n private advanceFrame(): void {\n const total = this.getTotalFrames();\n const next = this.currentFrame + 1;\n\n if (next >= total) {\n if (this.options.loop) {\n this.currentFrame = 0;\n } else {\n this.currentFrame = total - 1;\n this.pause();\n this.options.onComplete?.();\n }\n } else {\n this.currentFrame = next;\n }\n\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n private render(): void {\n if (!this.target || !this.isLoaded) return;\n\n const { src, rows, cols, width, height, renderer } = this.options;\n\n if (renderer === 'canvas' && this.target instanceof HTMLCanvasElement && this.image) {\n drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols, width, height);\n } else if (renderer === 'css' && this.target instanceof HTMLElement) {\n applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);\n }\n }\n\n private notify(): void {\n const state = this.getState();\n this.listeners.forEach((listener) => listener(state));\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/constants.ts","../src/core/utils.ts","../src/core/canvas-renderer.ts","../src/core/css-renderer.ts","../src/core/sprite-animator.ts"],"sourcesContent":["import type { RendererMode } from './types.js';\n\nexport const SPRITE_ANIMATION_DEFAULTS = {\n fps: 12,\n loop: true,\n width: 128,\n height: 128,\n autoPlay: true,\n renderer: 'css' as RendererMode,\n} as const;\n","import type { FramePosition, SpriteSize } from './types.js';\n\n/** Converts a SpriteSize to a CSS length string. Numbers become `px`; strings pass through. */\nexport function toCssLength(size: SpriteSize): string {\n return typeof size === 'number' ? `${size}px` : size;\n}\n\nexport function getTotalFrames(rows: number, cols: number): number {\n return rows * cols;\n}\n\nexport function getFramePosition(frameIndex: number, cols: number): FramePosition {\n return {\n row: Math.floor(frameIndex / cols),\n col: frameIndex % cols,\n };\n}\n\nexport function getBackgroundPositionPercent(\n frameIndex: number,\n rows: number,\n cols: number,\n): { x: number; y: number } {\n const { row, col } = getFramePosition(frameIndex, cols);\n const x = cols <= 1 ? 0 : (col / (cols - 1)) * 100;\n const y = rows <= 1 ? 0 : (row / (rows - 1)) * 100;\n return { x, y };\n}\n","import { getFramePosition } from './utils.js';\n\nexport function drawCanvasFrame(\n canvas: HTMLCanvasElement,\n image: HTMLImageElement,\n frameIndex: number,\n rows: number,\n cols: number,\n): void {\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const displayWidth = canvas.clientWidth;\n const displayHeight = canvas.clientHeight;\n if (displayWidth === 0 || displayHeight === 0) return;\n\n const dpr = window.devicePixelRatio || 1;\n const pixelWidth = Math.round(displayWidth * dpr);\n const pixelHeight = Math.round(displayHeight * dpr);\n\n if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {\n canvas.width = pixelWidth;\n canvas.height = pixelHeight;\n }\n\n const frameWidth = image.naturalWidth / cols;\n const frameHeight = image.naturalHeight / rows;\n const { row, col } = getFramePosition(frameIndex, cols);\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, displayWidth, displayHeight);\n ctx.drawImage(\n image,\n col * frameWidth,\n row * frameHeight,\n frameWidth,\n frameHeight,\n 0,\n 0,\n displayWidth,\n displayHeight,\n );\n}\n","import type { SpriteSize } from './types.js';\nimport { getBackgroundPositionPercent, toCssLength } from './utils.js';\n\nexport interface CssRendererTarget {\n style: CSSStyleDeclaration;\n}\n\nexport function applyCssFrame(\n target: CssRendererTarget,\n src: string,\n frameIndex: number,\n rows: number,\n cols: number,\n width: SpriteSize,\n height: SpriteSize,\n): void {\n const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);\n\n target.style.backgroundImage = `url(\"${src}\")`;\n target.style.backgroundRepeat = 'no-repeat';\n target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;\n target.style.backgroundPosition = `${x}% ${y}%`;\n target.style.width = toCssLength(width);\n target.style.height = toCssLength(height);\n target.style.display = 'inline-block';\n}\n\nexport function resetCssRenderer(target: CssRendererTarget): void {\n target.style.backgroundImage = '';\n}\n","import { SPRITE_ANIMATION_DEFAULTS } from './constants.js';\nimport { drawCanvasFrame } from './canvas-renderer.js';\nimport { applyCssFrame, resetCssRenderer } from './css-renderer.js';\nimport type { SpriteAnimationOptions, SpriteAnimationState } from './types.js';\nimport { getTotalFrames, toCssLength } from './utils.js';\n\ntype StateListener = (state: SpriteAnimationState) => void;\n\ntype ResolvedSpriteAnimationOptions = Required<\n Pick<SpriteAnimationOptions, 'src' | 'rows' | 'cols'>\n> &\n Required<Pick<SpriteAnimationOptions, 'fps' | 'loop' | 'width' | 'height' | 'autoPlay' | 'renderer'>> &\n Pick<SpriteAnimationOptions, 'onComplete' | 'onFrameChange'>;\n\nexport class SpriteAnimator {\n private options: ResolvedSpriteAnimationOptions;\n private currentFrame = 0;\n private isPlaying = false;\n private isLoaded = false;\n private rafId: number | null = null;\n private lastTimestamp = 0;\n private accumulatedTime = 0;\n private image: HTMLImageElement | null = null;\n private target: HTMLElement | HTMLCanvasElement | null = null;\n private listeners = new Set<StateListener>();\n private destroyed = false;\n private resizeObserver: ResizeObserver | null = null;\n\n constructor(options: SpriteAnimationOptions) {\n this.options = {\n ...SPRITE_ANIMATION_DEFAULTS,\n ...options,\n };\n this.loadImage();\n }\n\n attach(target: HTMLElement | HTMLCanvasElement): void {\n this.target = target;\n this.applyCanvasDisplaySize();\n this.setupResizeObserver();\n if (this.isLoaded) {\n this.render();\n }\n if (this.options.autoPlay) {\n this.play();\n }\n }\n\n play(): void {\n if (this.destroyed || this.isPlaying) return;\n this.isPlaying = true;\n this.lastTimestamp = 0;\n this.accumulatedTime = 0;\n this.rafId = requestAnimationFrame(this.tick);\n this.notify();\n }\n\n pause(): void {\n if (!this.isPlaying) return;\n this.isPlaying = false;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.notify();\n }\n\n stop(): void {\n this.pause();\n this.currentFrame = 0;\n this.render();\n this.notify();\n }\n\n goToFrame(frame: number): void {\n const total = this.getTotalFrames();\n this.currentFrame = Math.max(0, Math.min(frame, total - 1));\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n getState(): SpriteAnimationState {\n return {\n currentFrame: this.currentFrame,\n totalFrames: this.getTotalFrames(),\n isPlaying: this.isPlaying,\n isLoaded: this.isLoaded,\n };\n }\n\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => this.listeners.delete(listener);\n }\n\n updateOptions(partial: Partial<SpriteAnimationOptions>): void {\n const prevSrc = this.options.src;\n const prevFps = this.options.fps;\n const prevRenderer = this.options.renderer;\n this.options = { ...this.options, ...partial };\n\n if (partial.src !== undefined && partial.src !== prevSrc) {\n this.loadImage();\n } else if (this.isLoaded) {\n this.render();\n }\n\n if (partial.fps !== undefined && partial.fps !== prevFps) {\n this.accumulatedTime = 0;\n }\n\n if (partial.width !== undefined || partial.height !== undefined) {\n this.applyCanvasDisplaySize();\n }\n\n if (partial.renderer !== undefined && partial.renderer !== prevRenderer) {\n this.setupResizeObserver();\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n this.pause();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.listeners.clear();\n if (this.target && this.options.renderer === 'css') {\n resetCssRenderer(this.target);\n }\n this.target = null;\n this.image = null;\n }\n\n private getTotalFrames(): number {\n return getTotalFrames(this.options.rows, this.options.cols);\n }\n\n private loadImage(): void {\n this.isLoaded = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n if (this.destroyed) return;\n this.image = img;\n this.isLoaded = true;\n\n if (!this.options.width || !this.options.height) {\n const frameWidth = img.naturalWidth / this.options.cols;\n const frameHeight = img.naturalHeight / this.options.rows;\n this.options.width = frameWidth;\n this.options.height = frameHeight;\n }\n\n this.render();\n this.notify();\n\n if (this.options.autoPlay && this.target) {\n this.play();\n }\n };\n img.onerror = () => {\n console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);\n };\n img.src = this.options.src;\n }\n\n private tick = (timestamp: number): void => {\n if (!this.isPlaying || this.destroyed) return;\n\n if (this.lastTimestamp === 0) {\n this.lastTimestamp = timestamp;\n }\n\n const delta = timestamp - this.lastTimestamp;\n this.lastTimestamp = timestamp;\n this.accumulatedTime += delta;\n\n const frameDuration = 1000 / this.options.fps;\n while (this.accumulatedTime >= frameDuration) {\n this.accumulatedTime -= frameDuration;\n this.advanceFrame();\n }\n\n this.rafId = requestAnimationFrame(this.tick);\n };\n\n private advanceFrame(): void {\n const total = this.getTotalFrames();\n const next = this.currentFrame + 1;\n\n if (next >= total) {\n if (this.options.loop) {\n this.currentFrame = 0;\n } else {\n this.currentFrame = total - 1;\n this.pause();\n this.options.onComplete?.();\n }\n } else {\n this.currentFrame = next;\n }\n\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n private render(): void {\n if (!this.target || !this.isLoaded) return;\n\n const { src, rows, cols, width, height, renderer } = this.options;\n\n if (renderer === 'canvas' && this.target instanceof HTMLCanvasElement && this.image) {\n drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols);\n } else if (renderer === 'css' && this.target instanceof HTMLElement) {\n applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);\n }\n }\n\n private notify(): void {\n const state = this.getState();\n this.listeners.forEach((listener) => listener(state));\n }\n\n private applyCanvasDisplaySize(): void {\n if (this.options.renderer !== 'canvas' || !(this.target instanceof HTMLCanvasElement)) {\n return;\n }\n this.target.style.width = toCssLength(this.options.width);\n this.target.style.height = toCssLength(this.options.height);\n }\n\n private setupResizeObserver(): void {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n\n if (\n this.options.renderer !== 'canvas' ||\n !(this.target instanceof HTMLCanvasElement) ||\n typeof ResizeObserver === 'undefined'\n ) {\n return;\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n if (this.isLoaded) {\n this.render();\n }\n });\n this.resizeObserver.observe(this.target);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/react/index.cjs
CHANGED
|
@@ -25,9 +25,6 @@ __export(react_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(react_exports);
|
|
27
27
|
|
|
28
|
-
// src/react/Sprite.tsx
|
|
29
|
-
var import_react2 = require("react");
|
|
30
|
-
|
|
31
28
|
// src/core/constants.ts
|
|
32
29
|
var SPRITE_ANIMATION_DEFAULTS = {
|
|
33
30
|
fps: 12,
|
|
@@ -39,6 +36,9 @@ var SPRITE_ANIMATION_DEFAULTS = {
|
|
|
39
36
|
};
|
|
40
37
|
|
|
41
38
|
// src/core/utils.ts
|
|
39
|
+
function toCssLength(size) {
|
|
40
|
+
return typeof size === "number" ? `${size}px` : size;
|
|
41
|
+
}
|
|
42
42
|
function getTotalFrames(rows, cols) {
|
|
43
43
|
return rows * cols;
|
|
44
44
|
}
|
|
@@ -56,15 +56,24 @@ function getBackgroundPositionPercent(frameIndex, rows, cols) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/core/canvas-renderer.ts
|
|
59
|
-
function drawCanvasFrame(canvas, image, frameIndex, rows, cols
|
|
59
|
+
function drawCanvasFrame(canvas, image, frameIndex, rows, cols) {
|
|
60
60
|
const ctx = canvas.getContext("2d");
|
|
61
61
|
if (!ctx) return;
|
|
62
|
+
const displayWidth = canvas.clientWidth;
|
|
63
|
+
const displayHeight = canvas.clientHeight;
|
|
64
|
+
if (displayWidth === 0 || displayHeight === 0) return;
|
|
65
|
+
const dpr = window.devicePixelRatio || 1;
|
|
66
|
+
const pixelWidth = Math.round(displayWidth * dpr);
|
|
67
|
+
const pixelHeight = Math.round(displayHeight * dpr);
|
|
68
|
+
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
|
|
69
|
+
canvas.width = pixelWidth;
|
|
70
|
+
canvas.height = pixelHeight;
|
|
71
|
+
}
|
|
62
72
|
const frameWidth = image.naturalWidth / cols;
|
|
63
73
|
const frameHeight = image.naturalHeight / rows;
|
|
64
74
|
const { row, col } = getFramePosition(frameIndex, cols);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
ctx.clearRect(0, 0, width, height);
|
|
75
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
76
|
+
ctx.clearRect(0, 0, displayWidth, displayHeight);
|
|
68
77
|
ctx.drawImage(
|
|
69
78
|
image,
|
|
70
79
|
col * frameWidth,
|
|
@@ -73,8 +82,8 @@ function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
|
|
|
73
82
|
frameHeight,
|
|
74
83
|
0,
|
|
75
84
|
0,
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
displayWidth,
|
|
86
|
+
displayHeight
|
|
78
87
|
);
|
|
79
88
|
}
|
|
80
89
|
|
|
@@ -85,8 +94,8 @@ function applyCssFrame(target, src, frameIndex, rows, cols, width, height) {
|
|
|
85
94
|
target.style.backgroundRepeat = "no-repeat";
|
|
86
95
|
target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
|
|
87
96
|
target.style.backgroundPosition = `${x}% ${y}%`;
|
|
88
|
-
target.style.width =
|
|
89
|
-
target.style.height =
|
|
97
|
+
target.style.width = toCssLength(width);
|
|
98
|
+
target.style.height = toCssLength(height);
|
|
90
99
|
target.style.display = "inline-block";
|
|
91
100
|
}
|
|
92
101
|
function resetCssRenderer(target) {
|
|
@@ -106,6 +115,7 @@ var SpriteAnimator = class {
|
|
|
106
115
|
this.target = null;
|
|
107
116
|
this.listeners = /* @__PURE__ */ new Set();
|
|
108
117
|
this.destroyed = false;
|
|
118
|
+
this.resizeObserver = null;
|
|
109
119
|
this.tick = (timestamp) => {
|
|
110
120
|
if (!this.isPlaying || this.destroyed) return;
|
|
111
121
|
if (this.lastTimestamp === 0) {
|
|
@@ -129,6 +139,8 @@ var SpriteAnimator = class {
|
|
|
129
139
|
}
|
|
130
140
|
attach(target) {
|
|
131
141
|
this.target = target;
|
|
142
|
+
this.applyCanvasDisplaySize();
|
|
143
|
+
this.setupResizeObserver();
|
|
132
144
|
if (this.isLoaded) {
|
|
133
145
|
this.render();
|
|
134
146
|
}
|
|
@@ -182,6 +194,7 @@ var SpriteAnimator = class {
|
|
|
182
194
|
updateOptions(partial) {
|
|
183
195
|
const prevSrc = this.options.src;
|
|
184
196
|
const prevFps = this.options.fps;
|
|
197
|
+
const prevRenderer = this.options.renderer;
|
|
185
198
|
this.options = { ...this.options, ...partial };
|
|
186
199
|
if (partial.src !== void 0 && partial.src !== prevSrc) {
|
|
187
200
|
this.loadImage();
|
|
@@ -191,10 +204,18 @@ var SpriteAnimator = class {
|
|
|
191
204
|
if (partial.fps !== void 0 && partial.fps !== prevFps) {
|
|
192
205
|
this.accumulatedTime = 0;
|
|
193
206
|
}
|
|
207
|
+
if (partial.width !== void 0 || partial.height !== void 0) {
|
|
208
|
+
this.applyCanvasDisplaySize();
|
|
209
|
+
}
|
|
210
|
+
if (partial.renderer !== void 0 && partial.renderer !== prevRenderer) {
|
|
211
|
+
this.setupResizeObserver();
|
|
212
|
+
}
|
|
194
213
|
}
|
|
195
214
|
destroy() {
|
|
196
215
|
this.destroyed = true;
|
|
197
216
|
this.pause();
|
|
217
|
+
this.resizeObserver?.disconnect();
|
|
218
|
+
this.resizeObserver = null;
|
|
198
219
|
this.listeners.clear();
|
|
199
220
|
if (this.target && this.options.renderer === "css") {
|
|
200
221
|
resetCssRenderer(this.target);
|
|
@@ -252,7 +273,7 @@ var SpriteAnimator = class {
|
|
|
252
273
|
if (!this.target || !this.isLoaded) return;
|
|
253
274
|
const { src, rows, cols, width, height, renderer } = this.options;
|
|
254
275
|
if (renderer === "canvas" && this.target instanceof HTMLCanvasElement && this.image) {
|
|
255
|
-
drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols
|
|
276
|
+
drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols);
|
|
256
277
|
} else if (renderer === "css" && this.target instanceof HTMLElement) {
|
|
257
278
|
applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);
|
|
258
279
|
}
|
|
@@ -261,8 +282,31 @@ var SpriteAnimator = class {
|
|
|
261
282
|
const state = this.getState();
|
|
262
283
|
this.listeners.forEach((listener) => listener(state));
|
|
263
284
|
}
|
|
285
|
+
applyCanvasDisplaySize() {
|
|
286
|
+
if (this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement)) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.target.style.width = toCssLength(this.options.width);
|
|
290
|
+
this.target.style.height = toCssLength(this.options.height);
|
|
291
|
+
}
|
|
292
|
+
setupResizeObserver() {
|
|
293
|
+
this.resizeObserver?.disconnect();
|
|
294
|
+
this.resizeObserver = null;
|
|
295
|
+
if (this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement) || typeof ResizeObserver === "undefined") {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
299
|
+
if (this.isLoaded) {
|
|
300
|
+
this.render();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
this.resizeObserver.observe(this.target);
|
|
304
|
+
}
|
|
264
305
|
};
|
|
265
306
|
|
|
307
|
+
// src/react/Sprite.tsx
|
|
308
|
+
var import_react2 = require("react");
|
|
309
|
+
|
|
266
310
|
// src/react/useSprite.ts
|
|
267
311
|
var import_react = require("react");
|
|
268
312
|
function useSprite(options) {
|
|
@@ -316,21 +360,29 @@ function useSprite(options) {
|
|
|
316
360
|
|
|
317
361
|
// src/react/Sprite.tsx
|
|
318
362
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
319
|
-
var Sprite = (0, import_react2.forwardRef)(function Sprite2({ className, style, ...options }, ref) {
|
|
320
|
-
const { ref: targetRef, play, pause, stop, goToFrame } = useSprite(
|
|
363
|
+
var Sprite = (0, import_react2.forwardRef)(function Sprite2({ className, style, width, height, ...options }, ref) {
|
|
364
|
+
const { ref: targetRef, play, pause, stop, goToFrame } = useSprite({
|
|
365
|
+
width,
|
|
366
|
+
height,
|
|
367
|
+
...options
|
|
368
|
+
});
|
|
321
369
|
(0, import_react2.useImperativeHandle)(ref, () => ({ play, pause, stop, goToFrame }), [
|
|
322
370
|
play,
|
|
323
371
|
pause,
|
|
324
372
|
stop,
|
|
325
373
|
goToFrame
|
|
326
374
|
]);
|
|
375
|
+
const sizeStyle = {
|
|
376
|
+
width: toCssLength(width ?? SPRITE_ANIMATION_DEFAULTS.width),
|
|
377
|
+
height: toCssLength(height ?? SPRITE_ANIMATION_DEFAULTS.height)
|
|
378
|
+
};
|
|
327
379
|
if (options.renderer === "canvas") {
|
|
328
380
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
329
381
|
"canvas",
|
|
330
382
|
{
|
|
331
383
|
ref: targetRef,
|
|
332
384
|
className,
|
|
333
|
-
style
|
|
385
|
+
style: { ...sizeStyle, ...style }
|
|
334
386
|
}
|
|
335
387
|
);
|
|
336
388
|
}
|
|
@@ -339,7 +391,7 @@ var Sprite = (0, import_react2.forwardRef)(function Sprite2({ className, style,
|
|
|
339
391
|
{
|
|
340
392
|
ref: targetRef,
|
|
341
393
|
className,
|
|
342
|
-
style,
|
|
394
|
+
style: { ...sizeStyle, ...style },
|
|
343
395
|
role: "img",
|
|
344
396
|
"aria-label": "Sprite animation"
|
|
345
397
|
}
|
package/dist/react/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts","../../src/react/Sprite.tsx","../../src/core/constants.ts","../../src/core/utils.ts","../../src/core/canvas-renderer.ts","../../src/core/css-renderer.ts","../../src/core/sprite-animator.ts","../../src/react/useSprite.ts"],"sourcesContent":["export { Sprite } from './Sprite.js';\nexport type { SpriteHandle, SpriteProps } from './Sprite.js';\nexport { useSprite } from './useSprite.js';\nexport type { UseSpriteOptions, UseSpriteReturn } from './useSprite.js';\n","import type { SpriteAnimationOptions } from '../core/types.js';\nimport { forwardRef, useImperativeHandle } from 'react';\nimport { useSprite } from './useSprite.js';\n\nexport interface SpriteProps extends SpriteAnimationOptions {\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport interface SpriteHandle {\n play: () => void;\n pause: () => void;\n stop: () => void;\n goToFrame: (frame: number) => void;\n}\n\nexport const Sprite = forwardRef<SpriteHandle, SpriteProps>(function Sprite(\n { className, style, ...options },\n ref,\n) {\n const { ref: targetRef, play, pause, stop, goToFrame } = useSprite(options);\n\n useImperativeHandle(ref, () => ({ play, pause, stop, goToFrame }), [\n play,\n pause,\n stop,\n goToFrame,\n ]);\n\n if (options.renderer === 'canvas') {\n return (\n <canvas\n ref={targetRef as React.RefObject<HTMLCanvasElement>}\n className={className}\n style={style}\n />\n );\n }\n\n return (\n <div\n ref={targetRef as React.RefObject<HTMLDivElement>}\n className={className}\n style={style}\n role=\"img\"\n aria-label=\"Sprite animation\"\n />\n );\n});\n","import type { RendererMode } from './types.js';\n\nexport const SPRITE_ANIMATION_DEFAULTS = {\n fps: 12,\n loop: true,\n width: 128,\n height: 128,\n autoPlay: true,\n renderer: 'css' as RendererMode,\n} as const;\n","import type { FramePosition } from './types.js';\n\nexport function getTotalFrames(rows: number, cols: number): number {\n return rows * cols;\n}\n\nexport function getFramePosition(frameIndex: number, cols: number): FramePosition {\n return {\n row: Math.floor(frameIndex / cols),\n col: frameIndex % cols,\n };\n}\n\nexport function getBackgroundPositionPercent(\n frameIndex: number,\n rows: number,\n cols: number,\n): { x: number; y: number } {\n const { row, col } = getFramePosition(frameIndex, cols);\n const x = cols <= 1 ? 0 : (col / (cols - 1)) * 100;\n const y = rows <= 1 ? 0 : (row / (rows - 1)) * 100;\n return { x, y };\n}\n","import { getFramePosition } from './utils.js';\n\nexport function drawCanvasFrame(\n canvas: HTMLCanvasElement,\n image: HTMLImageElement,\n frameIndex: number,\n rows: number,\n cols: number,\n width: number,\n height: number,\n): void {\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const frameWidth = image.naturalWidth / cols;\n const frameHeight = image.naturalHeight / rows;\n const { row, col } = getFramePosition(frameIndex, cols);\n\n canvas.width = width;\n canvas.height = height;\n\n ctx.clearRect(0, 0, width, height);\n ctx.drawImage(\n image,\n col * frameWidth,\n row * frameHeight,\n frameWidth,\n frameHeight,\n 0,\n 0,\n width,\n height,\n );\n}\n","import { getBackgroundPositionPercent } from './utils.js';\n\nexport interface CssRendererTarget {\n style: CSSStyleDeclaration;\n}\n\nexport function applyCssFrame(\n target: CssRendererTarget,\n src: string,\n frameIndex: number,\n rows: number,\n cols: number,\n width: number,\n height: number,\n): void {\n const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);\n\n target.style.backgroundImage = `url(\"${src}\")`;\n target.style.backgroundRepeat = 'no-repeat';\n target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;\n target.style.backgroundPosition = `${x}% ${y}%`;\n target.style.width = `${width}px`;\n target.style.height = `${height}px`;\n target.style.display = 'inline-block';\n}\n\nexport function resetCssRenderer(target: CssRendererTarget): void {\n target.style.backgroundImage = '';\n}\n","import { SPRITE_ANIMATION_DEFAULTS } from './constants.js';\nimport { drawCanvasFrame } from './canvas-renderer.js';\nimport { applyCssFrame, resetCssRenderer } from './css-renderer.js';\nimport type { SpriteAnimationOptions, SpriteAnimationState } from './types.js';\nimport { getTotalFrames } from './utils.js';\n\ntype StateListener = (state: SpriteAnimationState) => void;\n\ntype ResolvedSpriteAnimationOptions = Required<\n Pick<SpriteAnimationOptions, 'src' | 'rows' | 'cols'>\n> &\n Required<Pick<SpriteAnimationOptions, 'fps' | 'loop' | 'width' | 'height' | 'autoPlay' | 'renderer'>> &\n Pick<SpriteAnimationOptions, 'onComplete' | 'onFrameChange'>;\n\nexport class SpriteAnimator {\n private options: ResolvedSpriteAnimationOptions;\n private currentFrame = 0;\n private isPlaying = false;\n private isLoaded = false;\n private rafId: number | null = null;\n private lastTimestamp = 0;\n private accumulatedTime = 0;\n private image: HTMLImageElement | null = null;\n private target: HTMLElement | HTMLCanvasElement | null = null;\n private listeners = new Set<StateListener>();\n private destroyed = false;\n\n constructor(options: SpriteAnimationOptions) {\n this.options = {\n ...SPRITE_ANIMATION_DEFAULTS,\n ...options,\n };\n this.loadImage();\n }\n\n attach(target: HTMLElement | HTMLCanvasElement): void {\n this.target = target;\n if (this.isLoaded) {\n this.render();\n }\n if (this.options.autoPlay) {\n this.play();\n }\n }\n\n play(): void {\n if (this.destroyed || this.isPlaying) return;\n this.isPlaying = true;\n this.lastTimestamp = 0;\n this.accumulatedTime = 0;\n this.rafId = requestAnimationFrame(this.tick);\n this.notify();\n }\n\n pause(): void {\n if (!this.isPlaying) return;\n this.isPlaying = false;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.notify();\n }\n\n stop(): void {\n this.pause();\n this.currentFrame = 0;\n this.render();\n this.notify();\n }\n\n goToFrame(frame: number): void {\n const total = this.getTotalFrames();\n this.currentFrame = Math.max(0, Math.min(frame, total - 1));\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n getState(): SpriteAnimationState {\n return {\n currentFrame: this.currentFrame,\n totalFrames: this.getTotalFrames(),\n isPlaying: this.isPlaying,\n isLoaded: this.isLoaded,\n };\n }\n\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => this.listeners.delete(listener);\n }\n\n updateOptions(partial: Partial<SpriteAnimationOptions>): void {\n const prevSrc = this.options.src;\n const prevFps = this.options.fps;\n this.options = { ...this.options, ...partial };\n\n if (partial.src !== undefined && partial.src !== prevSrc) {\n this.loadImage();\n } else if (this.isLoaded) {\n this.render();\n }\n\n if (partial.fps !== undefined && partial.fps !== prevFps) {\n this.accumulatedTime = 0;\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n this.pause();\n this.listeners.clear();\n if (this.target && this.options.renderer === 'css') {\n resetCssRenderer(this.target);\n }\n this.target = null;\n this.image = null;\n }\n\n private getTotalFrames(): number {\n return getTotalFrames(this.options.rows, this.options.cols);\n }\n\n private loadImage(): void {\n this.isLoaded = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n if (this.destroyed) return;\n this.image = img;\n this.isLoaded = true;\n\n if (!this.options.width || !this.options.height) {\n const frameWidth = img.naturalWidth / this.options.cols;\n const frameHeight = img.naturalHeight / this.options.rows;\n this.options.width = frameWidth;\n this.options.height = frameHeight;\n }\n\n this.render();\n this.notify();\n\n if (this.options.autoPlay && this.target) {\n this.play();\n }\n };\n img.onerror = () => {\n console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);\n };\n img.src = this.options.src;\n }\n\n private tick = (timestamp: number): void => {\n if (!this.isPlaying || this.destroyed) return;\n\n if (this.lastTimestamp === 0) {\n this.lastTimestamp = timestamp;\n }\n\n const delta = timestamp - this.lastTimestamp;\n this.lastTimestamp = timestamp;\n this.accumulatedTime += delta;\n\n const frameDuration = 1000 / this.options.fps;\n while (this.accumulatedTime >= frameDuration) {\n this.accumulatedTime -= frameDuration;\n this.advanceFrame();\n }\n\n this.rafId = requestAnimationFrame(this.tick);\n };\n\n private advanceFrame(): void {\n const total = this.getTotalFrames();\n const next = this.currentFrame + 1;\n\n if (next >= total) {\n if (this.options.loop) {\n this.currentFrame = 0;\n } else {\n this.currentFrame = total - 1;\n this.pause();\n this.options.onComplete?.();\n }\n } else {\n this.currentFrame = next;\n }\n\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n private render(): void {\n if (!this.target || !this.isLoaded) return;\n\n const { src, rows, cols, width, height, renderer } = this.options;\n\n if (renderer === 'canvas' && this.target instanceof HTMLCanvasElement && this.image) {\n drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols, width, height);\n } else if (renderer === 'css' && this.target instanceof HTMLElement) {\n applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);\n }\n }\n\n private notify(): void {\n const state = this.getState();\n this.listeners.forEach((listener) => listener(state));\n }\n}\n","import {\n SpriteAnimator,\n type SpriteAnimationOptions,\n type SpriteAnimationState,\n} from '../core/index.js';\nimport { useEffect, useRef, useState } from 'react';\n\nexport interface UseSpriteOptions extends SpriteAnimationOptions {\n /** Skip auto-attach; useful when controlling the target manually */\n enabled?: boolean;\n}\n\nexport interface UseSpriteReturn {\n ref: React.RefObject<HTMLElement | HTMLCanvasElement | null>;\n state: SpriteAnimationState;\n play: () => void;\n pause: () => void;\n stop: () => void;\n goToFrame: (frame: number) => void;\n}\n\nexport function useSprite(options: UseSpriteOptions): UseSpriteReturn {\n const ref = useRef<HTMLElement | HTMLCanvasElement | null>(null);\n const animatorRef = useRef<SpriteAnimator | null>(null);\n const [state, setState] = useState<SpriteAnimationState>({\n currentFrame: 0,\n totalFrames: options.rows * options.cols,\n isPlaying: false,\n isLoaded: false,\n });\n\n const { enabled = true, ...animatorOptions } = options;\n\n useEffect(() => {\n if (!enabled) return;\n\n const animator = new SpriteAnimator(animatorOptions);\n animatorRef.current = animator;\n\n const unsubscribe = animator.subscribe(setState);\n\n queueMicrotask(() => {\n if (ref.current) {\n animator.attach(ref.current);\n }\n });\n\n return () => {\n unsubscribe();\n animator.destroy();\n animatorRef.current = null;\n };\n }, [enabled]);\n\n useEffect(() => {\n animatorRef.current?.updateOptions(animatorOptions);\n }, [\n animatorOptions.src,\n animatorOptions.rows,\n animatorOptions.cols,\n animatorOptions.fps,\n animatorOptions.loop,\n animatorOptions.width,\n animatorOptions.height,\n animatorOptions.autoPlay,\n animatorOptions.renderer,\n ]);\n\n return {\n ref,\n state,\n play: () => animatorRef.current?.play(),\n pause: () => animatorRef.current?.pause(),\n stop: () => animatorRef.current?.stop(),\n goToFrame: (frame) => animatorRef.current?.goToFrame(frame),\n };\n}\n"],"mappings":"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","names":["import_react","Sprite"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts","../../src/core/constants.ts","../../src/core/utils.ts","../../src/core/canvas-renderer.ts","../../src/core/css-renderer.ts","../../src/core/sprite-animator.ts","../../src/react/Sprite.tsx","../../src/react/useSprite.ts"],"sourcesContent":["export { Sprite } from './Sprite.js';\nexport type { SpriteHandle, SpriteProps } from './Sprite.js';\nexport { useSprite } from './useSprite.js';\nexport type { UseSpriteOptions, UseSpriteReturn } from './useSprite.js';\n","import type { RendererMode } from './types.js';\n\nexport const SPRITE_ANIMATION_DEFAULTS = {\n fps: 12,\n loop: true,\n width: 128,\n height: 128,\n autoPlay: true,\n renderer: 'css' as RendererMode,\n} as const;\n","import type { FramePosition, SpriteSize } from './types.js';\n\n/** Converts a SpriteSize to a CSS length string. Numbers become `px`; strings pass through. */\nexport function toCssLength(size: SpriteSize): string {\n return typeof size === 'number' ? `${size}px` : size;\n}\n\nexport function getTotalFrames(rows: number, cols: number): number {\n return rows * cols;\n}\n\nexport function getFramePosition(frameIndex: number, cols: number): FramePosition {\n return {\n row: Math.floor(frameIndex / cols),\n col: frameIndex % cols,\n };\n}\n\nexport function getBackgroundPositionPercent(\n frameIndex: number,\n rows: number,\n cols: number,\n): { x: number; y: number } {\n const { row, col } = getFramePosition(frameIndex, cols);\n const x = cols <= 1 ? 0 : (col / (cols - 1)) * 100;\n const y = rows <= 1 ? 0 : (row / (rows - 1)) * 100;\n return { x, y };\n}\n","import { getFramePosition } from './utils.js';\n\nexport function drawCanvasFrame(\n canvas: HTMLCanvasElement,\n image: HTMLImageElement,\n frameIndex: number,\n rows: number,\n cols: number,\n): void {\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const displayWidth = canvas.clientWidth;\n const displayHeight = canvas.clientHeight;\n if (displayWidth === 0 || displayHeight === 0) return;\n\n const dpr = window.devicePixelRatio || 1;\n const pixelWidth = Math.round(displayWidth * dpr);\n const pixelHeight = Math.round(displayHeight * dpr);\n\n if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {\n canvas.width = pixelWidth;\n canvas.height = pixelHeight;\n }\n\n const frameWidth = image.naturalWidth / cols;\n const frameHeight = image.naturalHeight / rows;\n const { row, col } = getFramePosition(frameIndex, cols);\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, displayWidth, displayHeight);\n ctx.drawImage(\n image,\n col * frameWidth,\n row * frameHeight,\n frameWidth,\n frameHeight,\n 0,\n 0,\n displayWidth,\n displayHeight,\n );\n}\n","import type { SpriteSize } from './types.js';\nimport { getBackgroundPositionPercent, toCssLength } from './utils.js';\n\nexport interface CssRendererTarget {\n style: CSSStyleDeclaration;\n}\n\nexport function applyCssFrame(\n target: CssRendererTarget,\n src: string,\n frameIndex: number,\n rows: number,\n cols: number,\n width: SpriteSize,\n height: SpriteSize,\n): void {\n const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);\n\n target.style.backgroundImage = `url(\"${src}\")`;\n target.style.backgroundRepeat = 'no-repeat';\n target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;\n target.style.backgroundPosition = `${x}% ${y}%`;\n target.style.width = toCssLength(width);\n target.style.height = toCssLength(height);\n target.style.display = 'inline-block';\n}\n\nexport function resetCssRenderer(target: CssRendererTarget): void {\n target.style.backgroundImage = '';\n}\n","import { SPRITE_ANIMATION_DEFAULTS } from './constants.js';\nimport { drawCanvasFrame } from './canvas-renderer.js';\nimport { applyCssFrame, resetCssRenderer } from './css-renderer.js';\nimport type { SpriteAnimationOptions, SpriteAnimationState } from './types.js';\nimport { getTotalFrames, toCssLength } from './utils.js';\n\ntype StateListener = (state: SpriteAnimationState) => void;\n\ntype ResolvedSpriteAnimationOptions = Required<\n Pick<SpriteAnimationOptions, 'src' | 'rows' | 'cols'>\n> &\n Required<Pick<SpriteAnimationOptions, 'fps' | 'loop' | 'width' | 'height' | 'autoPlay' | 'renderer'>> &\n Pick<SpriteAnimationOptions, 'onComplete' | 'onFrameChange'>;\n\nexport class SpriteAnimator {\n private options: ResolvedSpriteAnimationOptions;\n private currentFrame = 0;\n private isPlaying = false;\n private isLoaded = false;\n private rafId: number | null = null;\n private lastTimestamp = 0;\n private accumulatedTime = 0;\n private image: HTMLImageElement | null = null;\n private target: HTMLElement | HTMLCanvasElement | null = null;\n private listeners = new Set<StateListener>();\n private destroyed = false;\n private resizeObserver: ResizeObserver | null = null;\n\n constructor(options: SpriteAnimationOptions) {\n this.options = {\n ...SPRITE_ANIMATION_DEFAULTS,\n ...options,\n };\n this.loadImage();\n }\n\n attach(target: HTMLElement | HTMLCanvasElement): void {\n this.target = target;\n this.applyCanvasDisplaySize();\n this.setupResizeObserver();\n if (this.isLoaded) {\n this.render();\n }\n if (this.options.autoPlay) {\n this.play();\n }\n }\n\n play(): void {\n if (this.destroyed || this.isPlaying) return;\n this.isPlaying = true;\n this.lastTimestamp = 0;\n this.accumulatedTime = 0;\n this.rafId = requestAnimationFrame(this.tick);\n this.notify();\n }\n\n pause(): void {\n if (!this.isPlaying) return;\n this.isPlaying = false;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.notify();\n }\n\n stop(): void {\n this.pause();\n this.currentFrame = 0;\n this.render();\n this.notify();\n }\n\n goToFrame(frame: number): void {\n const total = this.getTotalFrames();\n this.currentFrame = Math.max(0, Math.min(frame, total - 1));\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n getState(): SpriteAnimationState {\n return {\n currentFrame: this.currentFrame,\n totalFrames: this.getTotalFrames(),\n isPlaying: this.isPlaying,\n isLoaded: this.isLoaded,\n };\n }\n\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => this.listeners.delete(listener);\n }\n\n updateOptions(partial: Partial<SpriteAnimationOptions>): void {\n const prevSrc = this.options.src;\n const prevFps = this.options.fps;\n const prevRenderer = this.options.renderer;\n this.options = { ...this.options, ...partial };\n\n if (partial.src !== undefined && partial.src !== prevSrc) {\n this.loadImage();\n } else if (this.isLoaded) {\n this.render();\n }\n\n if (partial.fps !== undefined && partial.fps !== prevFps) {\n this.accumulatedTime = 0;\n }\n\n if (partial.width !== undefined || partial.height !== undefined) {\n this.applyCanvasDisplaySize();\n }\n\n if (partial.renderer !== undefined && partial.renderer !== prevRenderer) {\n this.setupResizeObserver();\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n this.pause();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.listeners.clear();\n if (this.target && this.options.renderer === 'css') {\n resetCssRenderer(this.target);\n }\n this.target = null;\n this.image = null;\n }\n\n private getTotalFrames(): number {\n return getTotalFrames(this.options.rows, this.options.cols);\n }\n\n private loadImage(): void {\n this.isLoaded = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n if (this.destroyed) return;\n this.image = img;\n this.isLoaded = true;\n\n if (!this.options.width || !this.options.height) {\n const frameWidth = img.naturalWidth / this.options.cols;\n const frameHeight = img.naturalHeight / this.options.rows;\n this.options.width = frameWidth;\n this.options.height = frameHeight;\n }\n\n this.render();\n this.notify();\n\n if (this.options.autoPlay && this.target) {\n this.play();\n }\n };\n img.onerror = () => {\n console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);\n };\n img.src = this.options.src;\n }\n\n private tick = (timestamp: number): void => {\n if (!this.isPlaying || this.destroyed) return;\n\n if (this.lastTimestamp === 0) {\n this.lastTimestamp = timestamp;\n }\n\n const delta = timestamp - this.lastTimestamp;\n this.lastTimestamp = timestamp;\n this.accumulatedTime += delta;\n\n const frameDuration = 1000 / this.options.fps;\n while (this.accumulatedTime >= frameDuration) {\n this.accumulatedTime -= frameDuration;\n this.advanceFrame();\n }\n\n this.rafId = requestAnimationFrame(this.tick);\n };\n\n private advanceFrame(): void {\n const total = this.getTotalFrames();\n const next = this.currentFrame + 1;\n\n if (next >= total) {\n if (this.options.loop) {\n this.currentFrame = 0;\n } else {\n this.currentFrame = total - 1;\n this.pause();\n this.options.onComplete?.();\n }\n } else {\n this.currentFrame = next;\n }\n\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n private render(): void {\n if (!this.target || !this.isLoaded) return;\n\n const { src, rows, cols, width, height, renderer } = this.options;\n\n if (renderer === 'canvas' && this.target instanceof HTMLCanvasElement && this.image) {\n drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols);\n } else if (renderer === 'css' && this.target instanceof HTMLElement) {\n applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);\n }\n }\n\n private notify(): void {\n const state = this.getState();\n this.listeners.forEach((listener) => listener(state));\n }\n\n private applyCanvasDisplaySize(): void {\n if (this.options.renderer !== 'canvas' || !(this.target instanceof HTMLCanvasElement)) {\n return;\n }\n this.target.style.width = toCssLength(this.options.width);\n this.target.style.height = toCssLength(this.options.height);\n }\n\n private setupResizeObserver(): void {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n\n if (\n this.options.renderer !== 'canvas' ||\n !(this.target instanceof HTMLCanvasElement) ||\n typeof ResizeObserver === 'undefined'\n ) {\n return;\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n if (this.isLoaded) {\n this.render();\n }\n });\n this.resizeObserver.observe(this.target);\n }\n}\n","import type { SpriteAnimationOptions } from '../core/types.js';\nimport { SPRITE_ANIMATION_DEFAULTS, toCssLength } from '../core/index.js';\nimport { forwardRef, useImperativeHandle } from 'react';\nimport { useSprite } from './useSprite.js';\n\nexport interface SpriteProps extends SpriteAnimationOptions {\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport interface SpriteHandle {\n play: () => void;\n pause: () => void;\n stop: () => void;\n goToFrame: (frame: number) => void;\n}\n\nexport const Sprite = forwardRef<SpriteHandle, SpriteProps>(function Sprite(\n { className, style, width, height, ...options },\n ref,\n) {\n const { ref: targetRef, play, pause, stop, goToFrame } = useSprite({\n width,\n height,\n ...options,\n });\n\n useImperativeHandle(ref, () => ({ play, pause, stop, goToFrame }), [\n play,\n pause,\n stop,\n goToFrame,\n ]);\n\n const sizeStyle = {\n width: toCssLength(width ?? SPRITE_ANIMATION_DEFAULTS.width),\n height: toCssLength(height ?? SPRITE_ANIMATION_DEFAULTS.height),\n };\n\n if (options.renderer === 'canvas') {\n return (\n <canvas\n ref={targetRef as React.RefObject<HTMLCanvasElement>}\n className={className}\n style={{ ...sizeStyle, ...style }}\n />\n );\n }\n\n return (\n <div\n ref={targetRef as React.RefObject<HTMLDivElement>}\n className={className}\n style={{ ...sizeStyle, ...style }}\n role=\"img\"\n aria-label=\"Sprite animation\"\n />\n );\n});\n","import {\n SpriteAnimator,\n type SpriteAnimationOptions,\n type SpriteAnimationState,\n} from '../core/index.js';\nimport { useEffect, useRef, useState } from 'react';\n\nexport interface UseSpriteOptions extends SpriteAnimationOptions {\n /** Skip auto-attach; useful when controlling the target manually */\n enabled?: boolean;\n}\n\nexport interface UseSpriteReturn {\n ref: React.RefObject<HTMLElement | HTMLCanvasElement | null>;\n state: SpriteAnimationState;\n play: () => void;\n pause: () => void;\n stop: () => void;\n goToFrame: (frame: number) => void;\n}\n\nexport function useSprite(options: UseSpriteOptions): UseSpriteReturn {\n const ref = useRef<HTMLElement | HTMLCanvasElement | null>(null);\n const animatorRef = useRef<SpriteAnimator | null>(null);\n const [state, setState] = useState<SpriteAnimationState>({\n currentFrame: 0,\n totalFrames: options.rows * options.cols,\n isPlaying: false,\n isLoaded: false,\n });\n\n const { enabled = true, ...animatorOptions } = options;\n\n useEffect(() => {\n if (!enabled) return;\n\n const animator = new SpriteAnimator(animatorOptions);\n animatorRef.current = animator;\n\n const unsubscribe = animator.subscribe(setState);\n\n queueMicrotask(() => {\n if (ref.current) {\n animator.attach(ref.current);\n }\n });\n\n return () => {\n unsubscribe();\n animator.destroy();\n animatorRef.current = null;\n };\n }, [enabled]);\n\n useEffect(() => {\n animatorRef.current?.updateOptions(animatorOptions);\n }, [\n animatorOptions.src,\n animatorOptions.rows,\n animatorOptions.cols,\n animatorOptions.fps,\n animatorOptions.loop,\n animatorOptions.width,\n animatorOptions.height,\n animatorOptions.autoPlay,\n animatorOptions.renderer,\n ]);\n\n return {\n ref,\n state,\n play: () => animatorRef.current?.play(),\n pause: () => animatorRef.current?.pause(),\n stop: () => animatorRef.current?.stop(),\n goToFrame: (frame) => animatorRef.current?.goToFrame(frame),\n };\n}\n"],"mappings":"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","names":["import_react","Sprite"]}
|
package/dist/react/index.d.cts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
|
|
3
3
|
type RendererMode = 'css' | 'canvas';
|
|
4
|
+
/** CSS length (e.g. `128`, `'8rem'`, `'50%'`, `'10vw'`) */
|
|
5
|
+
type SpriteSize = number | string;
|
|
4
6
|
interface SpriteAnimationOptions {
|
|
5
7
|
/** Sprite sheet image URL (PNG, WebP, etc.) */
|
|
6
8
|
src: string;
|
|
@@ -12,10 +14,10 @@ interface SpriteAnimationOptions {
|
|
|
12
14
|
fps?: number;
|
|
13
15
|
/** Whether to loop the animation (default: true) */
|
|
14
16
|
loop?: boolean;
|
|
15
|
-
/** Display width
|
|
16
|
-
width?:
|
|
17
|
-
/** Display height
|
|
18
|
-
height?:
|
|
17
|
+
/** Display width — number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
|
|
18
|
+
width?: SpriteSize;
|
|
19
|
+
/** Display height — number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
|
|
20
|
+
height?: SpriteSize;
|
|
19
21
|
/** Start playing automatically (default: true) */
|
|
20
22
|
autoPlay?: boolean;
|
|
21
23
|
/** Rendering mode: CSS background-position or Canvas (default: 'css') */
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
|
|
3
3
|
type RendererMode = 'css' | 'canvas';
|
|
4
|
+
/** CSS length (e.g. `128`, `'8rem'`, `'50%'`, `'10vw'`) */
|
|
5
|
+
type SpriteSize = number | string;
|
|
4
6
|
interface SpriteAnimationOptions {
|
|
5
7
|
/** Sprite sheet image URL (PNG, WebP, etc.) */
|
|
6
8
|
src: string;
|
|
@@ -12,10 +14,10 @@ interface SpriteAnimationOptions {
|
|
|
12
14
|
fps?: number;
|
|
13
15
|
/** Whether to loop the animation (default: true) */
|
|
14
16
|
loop?: boolean;
|
|
15
|
-
/** Display width
|
|
16
|
-
width?:
|
|
17
|
-
/** Display height
|
|
18
|
-
height?:
|
|
17
|
+
/** Display width — number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
|
|
18
|
+
width?: SpriteSize;
|
|
19
|
+
/** Display height — number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
|
|
20
|
+
height?: SpriteSize;
|
|
19
21
|
/** Start playing automatically (default: true) */
|
|
20
22
|
autoPlay?: boolean;
|
|
21
23
|
/** Rendering mode: CSS background-position or Canvas (default: 'css') */
|
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/react/Sprite.tsx
|
|
2
|
-
import { forwardRef, useImperativeHandle } from "react";
|
|
3
|
-
|
|
4
1
|
// src/core/constants.ts
|
|
5
2
|
var SPRITE_ANIMATION_DEFAULTS = {
|
|
6
3
|
fps: 12,
|
|
@@ -12,6 +9,9 @@ var SPRITE_ANIMATION_DEFAULTS = {
|
|
|
12
9
|
};
|
|
13
10
|
|
|
14
11
|
// src/core/utils.ts
|
|
12
|
+
function toCssLength(size) {
|
|
13
|
+
return typeof size === "number" ? `${size}px` : size;
|
|
14
|
+
}
|
|
15
15
|
function getTotalFrames(rows, cols) {
|
|
16
16
|
return rows * cols;
|
|
17
17
|
}
|
|
@@ -29,15 +29,24 @@ function getBackgroundPositionPercent(frameIndex, rows, cols) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// src/core/canvas-renderer.ts
|
|
32
|
-
function drawCanvasFrame(canvas, image, frameIndex, rows, cols
|
|
32
|
+
function drawCanvasFrame(canvas, image, frameIndex, rows, cols) {
|
|
33
33
|
const ctx = canvas.getContext("2d");
|
|
34
34
|
if (!ctx) return;
|
|
35
|
+
const displayWidth = canvas.clientWidth;
|
|
36
|
+
const displayHeight = canvas.clientHeight;
|
|
37
|
+
if (displayWidth === 0 || displayHeight === 0) return;
|
|
38
|
+
const dpr = window.devicePixelRatio || 1;
|
|
39
|
+
const pixelWidth = Math.round(displayWidth * dpr);
|
|
40
|
+
const pixelHeight = Math.round(displayHeight * dpr);
|
|
41
|
+
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
|
|
42
|
+
canvas.width = pixelWidth;
|
|
43
|
+
canvas.height = pixelHeight;
|
|
44
|
+
}
|
|
35
45
|
const frameWidth = image.naturalWidth / cols;
|
|
36
46
|
const frameHeight = image.naturalHeight / rows;
|
|
37
47
|
const { row, col } = getFramePosition(frameIndex, cols);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ctx.clearRect(0, 0, width, height);
|
|
48
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
49
|
+
ctx.clearRect(0, 0, displayWidth, displayHeight);
|
|
41
50
|
ctx.drawImage(
|
|
42
51
|
image,
|
|
43
52
|
col * frameWidth,
|
|
@@ -46,8 +55,8 @@ function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
|
|
|
46
55
|
frameHeight,
|
|
47
56
|
0,
|
|
48
57
|
0,
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
displayWidth,
|
|
59
|
+
displayHeight
|
|
51
60
|
);
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -58,8 +67,8 @@ function applyCssFrame(target, src, frameIndex, rows, cols, width, height) {
|
|
|
58
67
|
target.style.backgroundRepeat = "no-repeat";
|
|
59
68
|
target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
|
|
60
69
|
target.style.backgroundPosition = `${x}% ${y}%`;
|
|
61
|
-
target.style.width =
|
|
62
|
-
target.style.height =
|
|
70
|
+
target.style.width = toCssLength(width);
|
|
71
|
+
target.style.height = toCssLength(height);
|
|
63
72
|
target.style.display = "inline-block";
|
|
64
73
|
}
|
|
65
74
|
function resetCssRenderer(target) {
|
|
@@ -79,6 +88,7 @@ var SpriteAnimator = class {
|
|
|
79
88
|
this.target = null;
|
|
80
89
|
this.listeners = /* @__PURE__ */ new Set();
|
|
81
90
|
this.destroyed = false;
|
|
91
|
+
this.resizeObserver = null;
|
|
82
92
|
this.tick = (timestamp) => {
|
|
83
93
|
if (!this.isPlaying || this.destroyed) return;
|
|
84
94
|
if (this.lastTimestamp === 0) {
|
|
@@ -102,6 +112,8 @@ var SpriteAnimator = class {
|
|
|
102
112
|
}
|
|
103
113
|
attach(target) {
|
|
104
114
|
this.target = target;
|
|
115
|
+
this.applyCanvasDisplaySize();
|
|
116
|
+
this.setupResizeObserver();
|
|
105
117
|
if (this.isLoaded) {
|
|
106
118
|
this.render();
|
|
107
119
|
}
|
|
@@ -155,6 +167,7 @@ var SpriteAnimator = class {
|
|
|
155
167
|
updateOptions(partial) {
|
|
156
168
|
const prevSrc = this.options.src;
|
|
157
169
|
const prevFps = this.options.fps;
|
|
170
|
+
const prevRenderer = this.options.renderer;
|
|
158
171
|
this.options = { ...this.options, ...partial };
|
|
159
172
|
if (partial.src !== void 0 && partial.src !== prevSrc) {
|
|
160
173
|
this.loadImage();
|
|
@@ -164,10 +177,18 @@ var SpriteAnimator = class {
|
|
|
164
177
|
if (partial.fps !== void 0 && partial.fps !== prevFps) {
|
|
165
178
|
this.accumulatedTime = 0;
|
|
166
179
|
}
|
|
180
|
+
if (partial.width !== void 0 || partial.height !== void 0) {
|
|
181
|
+
this.applyCanvasDisplaySize();
|
|
182
|
+
}
|
|
183
|
+
if (partial.renderer !== void 0 && partial.renderer !== prevRenderer) {
|
|
184
|
+
this.setupResizeObserver();
|
|
185
|
+
}
|
|
167
186
|
}
|
|
168
187
|
destroy() {
|
|
169
188
|
this.destroyed = true;
|
|
170
189
|
this.pause();
|
|
190
|
+
this.resizeObserver?.disconnect();
|
|
191
|
+
this.resizeObserver = null;
|
|
171
192
|
this.listeners.clear();
|
|
172
193
|
if (this.target && this.options.renderer === "css") {
|
|
173
194
|
resetCssRenderer(this.target);
|
|
@@ -225,7 +246,7 @@ var SpriteAnimator = class {
|
|
|
225
246
|
if (!this.target || !this.isLoaded) return;
|
|
226
247
|
const { src, rows, cols, width, height, renderer } = this.options;
|
|
227
248
|
if (renderer === "canvas" && this.target instanceof HTMLCanvasElement && this.image) {
|
|
228
|
-
drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols
|
|
249
|
+
drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols);
|
|
229
250
|
} else if (renderer === "css" && this.target instanceof HTMLElement) {
|
|
230
251
|
applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);
|
|
231
252
|
}
|
|
@@ -234,8 +255,31 @@ var SpriteAnimator = class {
|
|
|
234
255
|
const state = this.getState();
|
|
235
256
|
this.listeners.forEach((listener) => listener(state));
|
|
236
257
|
}
|
|
258
|
+
applyCanvasDisplaySize() {
|
|
259
|
+
if (this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.target.style.width = toCssLength(this.options.width);
|
|
263
|
+
this.target.style.height = toCssLength(this.options.height);
|
|
264
|
+
}
|
|
265
|
+
setupResizeObserver() {
|
|
266
|
+
this.resizeObserver?.disconnect();
|
|
267
|
+
this.resizeObserver = null;
|
|
268
|
+
if (this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement) || typeof ResizeObserver === "undefined") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
272
|
+
if (this.isLoaded) {
|
|
273
|
+
this.render();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
this.resizeObserver.observe(this.target);
|
|
277
|
+
}
|
|
237
278
|
};
|
|
238
279
|
|
|
280
|
+
// src/react/Sprite.tsx
|
|
281
|
+
import { forwardRef, useImperativeHandle } from "react";
|
|
282
|
+
|
|
239
283
|
// src/react/useSprite.ts
|
|
240
284
|
import { useEffect, useRef, useState } from "react";
|
|
241
285
|
function useSprite(options) {
|
|
@@ -289,21 +333,29 @@ function useSprite(options) {
|
|
|
289
333
|
|
|
290
334
|
// src/react/Sprite.tsx
|
|
291
335
|
import { jsx } from "react/jsx-runtime";
|
|
292
|
-
var Sprite = forwardRef(function Sprite2({ className, style, ...options }, ref) {
|
|
293
|
-
const { ref: targetRef, play, pause, stop, goToFrame } = useSprite(
|
|
336
|
+
var Sprite = forwardRef(function Sprite2({ className, style, width, height, ...options }, ref) {
|
|
337
|
+
const { ref: targetRef, play, pause, stop, goToFrame } = useSprite({
|
|
338
|
+
width,
|
|
339
|
+
height,
|
|
340
|
+
...options
|
|
341
|
+
});
|
|
294
342
|
useImperativeHandle(ref, () => ({ play, pause, stop, goToFrame }), [
|
|
295
343
|
play,
|
|
296
344
|
pause,
|
|
297
345
|
stop,
|
|
298
346
|
goToFrame
|
|
299
347
|
]);
|
|
348
|
+
const sizeStyle = {
|
|
349
|
+
width: toCssLength(width ?? SPRITE_ANIMATION_DEFAULTS.width),
|
|
350
|
+
height: toCssLength(height ?? SPRITE_ANIMATION_DEFAULTS.height)
|
|
351
|
+
};
|
|
300
352
|
if (options.renderer === "canvas") {
|
|
301
353
|
return /* @__PURE__ */ jsx(
|
|
302
354
|
"canvas",
|
|
303
355
|
{
|
|
304
356
|
ref: targetRef,
|
|
305
357
|
className,
|
|
306
|
-
style
|
|
358
|
+
style: { ...sizeStyle, ...style }
|
|
307
359
|
}
|
|
308
360
|
);
|
|
309
361
|
}
|
|
@@ -312,7 +364,7 @@ var Sprite = forwardRef(function Sprite2({ className, style, ...options }, ref)
|
|
|
312
364
|
{
|
|
313
365
|
ref: targetRef,
|
|
314
366
|
className,
|
|
315
|
-
style,
|
|
367
|
+
style: { ...sizeStyle, ...style },
|
|
316
368
|
role: "img",
|
|
317
369
|
"aria-label": "Sprite animation"
|
|
318
370
|
}
|