mason-sprite 0.1.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,61 @@
1
+ import * as react from 'react';
2
+
3
+ type RendererMode = 'css' | 'canvas';
4
+ interface SpriteAnimationOptions {
5
+ /** Sprite sheet image URL (PNG, WebP, etc.) */
6
+ src: string;
7
+ /** Number of rows in the sprite sheet */
8
+ rows: number;
9
+ /** Number of columns in the sprite sheet */
10
+ cols: number;
11
+ /** Frames per second (default: 12) */
12
+ fps?: number;
13
+ /** Whether to loop the animation (default: true) */
14
+ loop?: boolean;
15
+ /** Display width in pixels */
16
+ width?: number;
17
+ /** Display height in pixels */
18
+ height?: number;
19
+ /** Start playing automatically (default: true) */
20
+ autoPlay?: boolean;
21
+ /** Rendering mode: CSS background-position or Canvas (default: 'css') */
22
+ renderer?: RendererMode;
23
+ /** Called when a non-looping animation completes */
24
+ onComplete?: () => void;
25
+ /** Called on each frame change */
26
+ onFrameChange?: (frame: number) => void;
27
+ }
28
+ interface SpriteAnimationState {
29
+ currentFrame: number;
30
+ totalFrames: number;
31
+ isPlaying: boolean;
32
+ isLoaded: boolean;
33
+ }
34
+
35
+ interface SpriteProps extends SpriteAnimationOptions {
36
+ className?: string;
37
+ style?: React.CSSProperties;
38
+ }
39
+ interface SpriteHandle {
40
+ play: () => void;
41
+ pause: () => void;
42
+ stop: () => void;
43
+ goToFrame: (frame: number) => void;
44
+ }
45
+ declare const Sprite: react.ForwardRefExoticComponent<SpriteProps & react.RefAttributes<SpriteHandle>>;
46
+
47
+ interface UseSpriteOptions extends SpriteAnimationOptions {
48
+ /** Skip auto-attach; useful when controlling the target manually */
49
+ enabled?: boolean;
50
+ }
51
+ interface UseSpriteReturn {
52
+ ref: React.RefObject<HTMLElement | HTMLCanvasElement | null>;
53
+ state: SpriteAnimationState;
54
+ play: () => void;
55
+ pause: () => void;
56
+ stop: () => void;
57
+ goToFrame: (frame: number) => void;
58
+ }
59
+ declare function useSprite(options: UseSpriteOptions): UseSpriteReturn;
60
+
61
+ export { Sprite, type SpriteHandle, type SpriteProps, type UseSpriteOptions, type UseSpriteReturn, useSprite };
@@ -0,0 +1,61 @@
1
+ import * as react from 'react';
2
+
3
+ type RendererMode = 'css' | 'canvas';
4
+ interface SpriteAnimationOptions {
5
+ /** Sprite sheet image URL (PNG, WebP, etc.) */
6
+ src: string;
7
+ /** Number of rows in the sprite sheet */
8
+ rows: number;
9
+ /** Number of columns in the sprite sheet */
10
+ cols: number;
11
+ /** Frames per second (default: 12) */
12
+ fps?: number;
13
+ /** Whether to loop the animation (default: true) */
14
+ loop?: boolean;
15
+ /** Display width in pixels */
16
+ width?: number;
17
+ /** Display height in pixels */
18
+ height?: number;
19
+ /** Start playing automatically (default: true) */
20
+ autoPlay?: boolean;
21
+ /** Rendering mode: CSS background-position or Canvas (default: 'css') */
22
+ renderer?: RendererMode;
23
+ /** Called when a non-looping animation completes */
24
+ onComplete?: () => void;
25
+ /** Called on each frame change */
26
+ onFrameChange?: (frame: number) => void;
27
+ }
28
+ interface SpriteAnimationState {
29
+ currentFrame: number;
30
+ totalFrames: number;
31
+ isPlaying: boolean;
32
+ isLoaded: boolean;
33
+ }
34
+
35
+ interface SpriteProps extends SpriteAnimationOptions {
36
+ className?: string;
37
+ style?: React.CSSProperties;
38
+ }
39
+ interface SpriteHandle {
40
+ play: () => void;
41
+ pause: () => void;
42
+ stop: () => void;
43
+ goToFrame: (frame: number) => void;
44
+ }
45
+ declare const Sprite: react.ForwardRefExoticComponent<SpriteProps & react.RefAttributes<SpriteHandle>>;
46
+
47
+ interface UseSpriteOptions extends SpriteAnimationOptions {
48
+ /** Skip auto-attach; useful when controlling the target manually */
49
+ enabled?: boolean;
50
+ }
51
+ interface UseSpriteReturn {
52
+ ref: React.RefObject<HTMLElement | HTMLCanvasElement | null>;
53
+ state: SpriteAnimationState;
54
+ play: () => void;
55
+ pause: () => void;
56
+ stop: () => void;
57
+ goToFrame: (frame: number) => void;
58
+ }
59
+ declare function useSprite(options: UseSpriteOptions): UseSpriteReturn;
60
+
61
+ export { Sprite, type SpriteHandle, type SpriteProps, type UseSpriteOptions, type UseSpriteReturn, useSprite };
@@ -0,0 +1,325 @@
1
+ // src/react/Sprite.tsx
2
+ import { forwardRef, useImperativeHandle } from "react";
3
+
4
+ // src/core/constants.ts
5
+ var SPRITE_ANIMATION_DEFAULTS = {
6
+ fps: 12,
7
+ loop: true,
8
+ width: 128,
9
+ height: 128,
10
+ autoPlay: true,
11
+ renderer: "css"
12
+ };
13
+
14
+ // src/core/utils.ts
15
+ function getTotalFrames(rows, cols) {
16
+ return rows * cols;
17
+ }
18
+ function getFramePosition(frameIndex, cols) {
19
+ return {
20
+ row: Math.floor(frameIndex / cols),
21
+ col: frameIndex % cols
22
+ };
23
+ }
24
+ function getBackgroundPositionPercent(frameIndex, rows, cols) {
25
+ const { row, col } = getFramePosition(frameIndex, cols);
26
+ const x = cols <= 1 ? 0 : col / (cols - 1) * 100;
27
+ const y = rows <= 1 ? 0 : row / (rows - 1) * 100;
28
+ return { x, y };
29
+ }
30
+
31
+ // src/core/canvas-renderer.ts
32
+ function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
33
+ const ctx = canvas.getContext("2d");
34
+ if (!ctx) return;
35
+ const frameWidth = image.naturalWidth / cols;
36
+ const frameHeight = image.naturalHeight / rows;
37
+ const { row, col } = getFramePosition(frameIndex, cols);
38
+ canvas.width = width;
39
+ canvas.height = height;
40
+ ctx.clearRect(0, 0, width, height);
41
+ ctx.drawImage(
42
+ image,
43
+ col * frameWidth,
44
+ row * frameHeight,
45
+ frameWidth,
46
+ frameHeight,
47
+ 0,
48
+ 0,
49
+ width,
50
+ height
51
+ );
52
+ }
53
+
54
+ // src/core/css-renderer.ts
55
+ function applyCssFrame(target, src, frameIndex, rows, cols, width, height) {
56
+ const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);
57
+ target.style.backgroundImage = `url("${src}")`;
58
+ target.style.backgroundRepeat = "no-repeat";
59
+ target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
60
+ target.style.backgroundPosition = `${x}% ${y}%`;
61
+ target.style.width = `${width}px`;
62
+ target.style.height = `${height}px`;
63
+ target.style.display = "inline-block";
64
+ }
65
+ function resetCssRenderer(target) {
66
+ target.style.backgroundImage = "";
67
+ }
68
+
69
+ // src/core/sprite-animator.ts
70
+ var SpriteAnimator = class {
71
+ constructor(options) {
72
+ this.currentFrame = 0;
73
+ this.isPlaying = false;
74
+ this.isLoaded = false;
75
+ this.rafId = null;
76
+ this.lastTimestamp = 0;
77
+ this.accumulatedTime = 0;
78
+ this.image = null;
79
+ this.target = null;
80
+ this.listeners = /* @__PURE__ */ new Set();
81
+ this.destroyed = false;
82
+ this.tick = (timestamp) => {
83
+ if (!this.isPlaying || this.destroyed) return;
84
+ if (this.lastTimestamp === 0) {
85
+ this.lastTimestamp = timestamp;
86
+ }
87
+ const delta = timestamp - this.lastTimestamp;
88
+ this.lastTimestamp = timestamp;
89
+ this.accumulatedTime += delta;
90
+ const frameDuration = 1e3 / this.options.fps;
91
+ while (this.accumulatedTime >= frameDuration) {
92
+ this.accumulatedTime -= frameDuration;
93
+ this.advanceFrame();
94
+ }
95
+ this.rafId = requestAnimationFrame(this.tick);
96
+ };
97
+ this.options = {
98
+ ...SPRITE_ANIMATION_DEFAULTS,
99
+ ...options
100
+ };
101
+ this.loadImage();
102
+ }
103
+ attach(target) {
104
+ this.target = target;
105
+ if (this.isLoaded) {
106
+ this.render();
107
+ }
108
+ if (this.options.autoPlay) {
109
+ this.play();
110
+ }
111
+ }
112
+ play() {
113
+ if (this.destroyed || this.isPlaying) return;
114
+ this.isPlaying = true;
115
+ this.lastTimestamp = 0;
116
+ this.accumulatedTime = 0;
117
+ this.rafId = requestAnimationFrame(this.tick);
118
+ this.notify();
119
+ }
120
+ pause() {
121
+ if (!this.isPlaying) return;
122
+ this.isPlaying = false;
123
+ if (this.rafId !== null) {
124
+ cancelAnimationFrame(this.rafId);
125
+ this.rafId = null;
126
+ }
127
+ this.notify();
128
+ }
129
+ stop() {
130
+ this.pause();
131
+ this.currentFrame = 0;
132
+ this.render();
133
+ this.notify();
134
+ }
135
+ goToFrame(frame) {
136
+ const total = this.getTotalFrames();
137
+ this.currentFrame = Math.max(0, Math.min(frame, total - 1));
138
+ this.render();
139
+ this.options.onFrameChange?.(this.currentFrame);
140
+ this.notify();
141
+ }
142
+ getState() {
143
+ return {
144
+ currentFrame: this.currentFrame,
145
+ totalFrames: this.getTotalFrames(),
146
+ isPlaying: this.isPlaying,
147
+ isLoaded: this.isLoaded
148
+ };
149
+ }
150
+ subscribe(listener) {
151
+ this.listeners.add(listener);
152
+ listener(this.getState());
153
+ return () => this.listeners.delete(listener);
154
+ }
155
+ updateOptions(partial) {
156
+ const prevSrc = this.options.src;
157
+ const prevFps = this.options.fps;
158
+ this.options = { ...this.options, ...partial };
159
+ if (partial.src !== void 0 && partial.src !== prevSrc) {
160
+ this.loadImage();
161
+ } else if (this.isLoaded) {
162
+ this.render();
163
+ }
164
+ if (partial.fps !== void 0 && partial.fps !== prevFps) {
165
+ this.accumulatedTime = 0;
166
+ }
167
+ }
168
+ destroy() {
169
+ this.destroyed = true;
170
+ this.pause();
171
+ this.listeners.clear();
172
+ if (this.target && this.options.renderer === "css") {
173
+ resetCssRenderer(this.target);
174
+ }
175
+ this.target = null;
176
+ this.image = null;
177
+ }
178
+ getTotalFrames() {
179
+ return getTotalFrames(this.options.rows, this.options.cols);
180
+ }
181
+ loadImage() {
182
+ this.isLoaded = false;
183
+ const img = new Image();
184
+ img.crossOrigin = "anonymous";
185
+ img.onload = () => {
186
+ if (this.destroyed) return;
187
+ this.image = img;
188
+ this.isLoaded = true;
189
+ if (!this.options.width || !this.options.height) {
190
+ const frameWidth = img.naturalWidth / this.options.cols;
191
+ const frameHeight = img.naturalHeight / this.options.rows;
192
+ this.options.width = frameWidth;
193
+ this.options.height = frameHeight;
194
+ }
195
+ this.render();
196
+ this.notify();
197
+ if (this.options.autoPlay && this.target) {
198
+ this.play();
199
+ }
200
+ };
201
+ img.onerror = () => {
202
+ console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);
203
+ };
204
+ img.src = this.options.src;
205
+ }
206
+ advanceFrame() {
207
+ const total = this.getTotalFrames();
208
+ const next = this.currentFrame + 1;
209
+ if (next >= total) {
210
+ if (this.options.loop) {
211
+ this.currentFrame = 0;
212
+ } else {
213
+ this.currentFrame = total - 1;
214
+ this.pause();
215
+ this.options.onComplete?.();
216
+ }
217
+ } else {
218
+ this.currentFrame = next;
219
+ }
220
+ this.render();
221
+ this.options.onFrameChange?.(this.currentFrame);
222
+ this.notify();
223
+ }
224
+ render() {
225
+ if (!this.target || !this.isLoaded) return;
226
+ const { src, rows, cols, width, height, renderer } = this.options;
227
+ if (renderer === "canvas" && this.target instanceof HTMLCanvasElement && this.image) {
228
+ drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols, width, height);
229
+ } else if (renderer === "css" && this.target instanceof HTMLElement) {
230
+ applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);
231
+ }
232
+ }
233
+ notify() {
234
+ const state = this.getState();
235
+ this.listeners.forEach((listener) => listener(state));
236
+ }
237
+ };
238
+
239
+ // src/react/useSprite.ts
240
+ import { useEffect, useRef, useState } from "react";
241
+ function useSprite(options) {
242
+ const ref = useRef(null);
243
+ const animatorRef = useRef(null);
244
+ const [state, setState] = useState({
245
+ currentFrame: 0,
246
+ totalFrames: options.rows * options.cols,
247
+ isPlaying: false,
248
+ isLoaded: false
249
+ });
250
+ const { enabled = true, ...animatorOptions } = options;
251
+ useEffect(() => {
252
+ if (!enabled) return;
253
+ const animator = new SpriteAnimator(animatorOptions);
254
+ animatorRef.current = animator;
255
+ const unsubscribe = animator.subscribe(setState);
256
+ queueMicrotask(() => {
257
+ if (ref.current) {
258
+ animator.attach(ref.current);
259
+ }
260
+ });
261
+ return () => {
262
+ unsubscribe();
263
+ animator.destroy();
264
+ animatorRef.current = null;
265
+ };
266
+ }, [enabled]);
267
+ useEffect(() => {
268
+ animatorRef.current?.updateOptions(animatorOptions);
269
+ }, [
270
+ animatorOptions.src,
271
+ animatorOptions.rows,
272
+ animatorOptions.cols,
273
+ animatorOptions.fps,
274
+ animatorOptions.loop,
275
+ animatorOptions.width,
276
+ animatorOptions.height,
277
+ animatorOptions.autoPlay,
278
+ animatorOptions.renderer
279
+ ]);
280
+ return {
281
+ ref,
282
+ state,
283
+ play: () => animatorRef.current?.play(),
284
+ pause: () => animatorRef.current?.pause(),
285
+ stop: () => animatorRef.current?.stop(),
286
+ goToFrame: (frame) => animatorRef.current?.goToFrame(frame)
287
+ };
288
+ }
289
+
290
+ // src/react/Sprite.tsx
291
+ 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(options);
294
+ useImperativeHandle(ref, () => ({ play, pause, stop, goToFrame }), [
295
+ play,
296
+ pause,
297
+ stop,
298
+ goToFrame
299
+ ]);
300
+ if (options.renderer === "canvas") {
301
+ return /* @__PURE__ */ jsx(
302
+ "canvas",
303
+ {
304
+ ref: targetRef,
305
+ className,
306
+ style
307
+ }
308
+ );
309
+ }
310
+ return /* @__PURE__ */ jsx(
311
+ "div",
312
+ {
313
+ ref: targetRef,
314
+ className,
315
+ style,
316
+ role: "img",
317
+ "aria-label": "Sprite animation"
318
+ }
319
+ );
320
+ });
321
+ export {
322
+ Sprite,
323
+ useSprite
324
+ };
325
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../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":["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":["Sprite"]}
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import {
3
+ SPRITE_ANIMATION_DEFAULTS,
4
+ SpriteAnimator,
5
+ type SpriteAnimationOptions,
6
+ } from 'mason-sprite';
7
+
8
+ interface Props extends SpriteAnimationOptions {
9
+ class?: string;
10
+ }
11
+
12
+ let {
13
+ src,
14
+ rows,
15
+ cols,
16
+ fps = SPRITE_ANIMATION_DEFAULTS.fps,
17
+ loop = SPRITE_ANIMATION_DEFAULTS.loop,
18
+ width = SPRITE_ANIMATION_DEFAULTS.width,
19
+ height = SPRITE_ANIMATION_DEFAULTS.height,
20
+ autoPlay = SPRITE_ANIMATION_DEFAULTS.autoPlay,
21
+ renderer = SPRITE_ANIMATION_DEFAULTS.renderer,
22
+ onComplete,
23
+ onFrameChange,
24
+ class: className = '',
25
+ }: Props = $props();
26
+
27
+ let target = $state<HTMLElement | HTMLCanvasElement | undefined>();
28
+ let animator = $state<SpriteAnimator | null>(null);
29
+
30
+ $effect(() => {
31
+ const el = target;
32
+ if (!el) return;
33
+
34
+ const instance = new SpriteAnimator({
35
+ src,
36
+ rows,
37
+ cols,
38
+ fps,
39
+ loop,
40
+ width,
41
+ height,
42
+ autoPlay,
43
+ renderer,
44
+ onComplete,
45
+ onFrameChange,
46
+ });
47
+
48
+ instance.attach(el);
49
+ animator = instance;
50
+
51
+ return () => {
52
+ instance.destroy();
53
+ animator = null;
54
+ };
55
+ });
56
+
57
+ export function play() {
58
+ animator?.play();
59
+ }
60
+
61
+ export function pause() {
62
+ animator?.pause();
63
+ }
64
+
65
+ export function stop() {
66
+ animator?.stop();
67
+ }
68
+
69
+ export function goToFrame(frame: number) {
70
+ animator?.goToFrame(frame);
71
+ }
72
+
73
+ export function getState() {
74
+ return animator?.getState();
75
+ }
76
+ </script>
77
+
78
+ {#if renderer === 'canvas'}
79
+ <canvas bind:this={target} class={className} style="width: {width}px; height: {height}px;"></canvas>
80
+ {:else}
81
+ <div
82
+ bind:this={target}
83
+ class={className}
84
+ role="img"
85
+ aria-label="Sprite animation"
86
+ style="width: {width}px; height: {height}px;"
87
+ ></div>
88
+ {/if}
@@ -0,0 +1,14 @@
1
+ import { type SpriteAnimationOptions } from 'mason-sprite';
2
+ interface Props extends SpriteAnimationOptions {
3
+ class?: string;
4
+ }
5
+ declare const Sprite: import("svelte").Component<Props, {
6
+ play: () => void;
7
+ pause: () => void;
8
+ stop: () => void;
9
+ goToFrame: (frame: number) => void;
10
+ getState: () => import("mason-sprite").SpriteAnimationState | undefined;
11
+ }, "">;
12
+ type Sprite = ReturnType<typeof Sprite>;
13
+ export default Sprite;
14
+ //# sourceMappingURL=Sprite.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sprite.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/Sprite.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAGH,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAGtB,UAAU,KAAM,SAAQ,sBAAsB;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+EH,QAAA,MAAM,MAAM;;;;uBAjBiB,MAAM;;MAiBiB,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default as Sprite } from './Sprite.svelte';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1 @@
1
+ export { default as Sprite } from './Sprite.svelte';
@@ -0,0 +1,28 @@
1
+ import { SpriteAnimationOptions } from '../core/index.js';
2
+ type __VLS_Props = SpriteAnimationOptions & {
3
+ class?: string;
4
+ };
5
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {
6
+ play: () => void | undefined;
7
+ pause: () => void | undefined;
8
+ stop: () => void | undefined;
9
+ goToFrame: (frame: number) => void | undefined;
10
+ getState: () => import('../core/types.js').SpriteAnimationState | undefined;
11
+ }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
12
+ complete: () => any;
13
+ frameChange: (frame: number) => any;
14
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ onComplete?: (() => any) | undefined;
16
+ onFrameChange?: ((frame: number) => any) | undefined;
17
+ }>, {
18
+ fps: number;
19
+ loop: boolean;
20
+ width: number;
21
+ height: number;
22
+ autoPlay: boolean;
23
+ renderer: import('../core/types.js').RendererMode;
24
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
25
+ targetRef: HTMLDivElement;
26
+ }, any>;
27
+ export default _default;
28
+ //# sourceMappingURL=Sprite.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sprite.vue.d.ts","sourceRoot":"","sources":["../../src/vue/Sprite.vue"],"names":[],"mappings":"AA2FA,OAAO,EAA6C,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAG1G,KAAK,WAAW,GAAG,sBAAsB,GAAG;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;;;;;uBAgEe,MAAM;;;;;;;;;;;;;;;;;;AAqE3B,wBAUG"}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("vue"),u={fps:12,loop:!0,width:128,height:128,autoPlay:!0,renderer:"css"};function f(i,t){return i*t}function d(i,t){return{row:Math.floor(i/t),col:i%t}}function g(i,t,r){const{row:e,col:o}=d(i,r),n=r<=1?0:o/(r-1)*100,s=t<=1?0:e/(t-1)*100;return{x:n,y:s}}function y(i,t,r,e,o,n,s){const l=i.getContext("2d");if(!l)return;const h=t.naturalWidth/o,c=t.naturalHeight/e,{row:m,col:p}=d(r,o);i.width=n,i.height=s,l.clearRect(0,0,n,s),l.drawImage(t,p*h,m*c,h,c,0,0,n,s)}function F(i,t,r,e,o,n,s){const{x:l,y:h}=g(r,e,o);i.style.backgroundImage=`url("${t}")`,i.style.backgroundRepeat="no-repeat",i.style.backgroundSize=`${o*100}% ${e*100}%`,i.style.backgroundPosition=`${l}% ${h}%`,i.style.width=`${n}px`,i.style.height=`${s}px`,i.style.display="inline-block"}function w(i){i.style.backgroundImage=""}class T{constructor(t){this.currentFrame=0,this.isPlaying=!1,this.isLoaded=!1,this.rafId=null,this.lastTimestamp=0,this.accumulatedTime=0,this.image=null,this.target=null,this.listeners=new Set,this.destroyed=!1,this.tick=r=>{if(!this.isPlaying||this.destroyed)return;this.lastTimestamp===0&&(this.lastTimestamp=r);const e=r-this.lastTimestamp;this.lastTimestamp=r,this.accumulatedTime+=e;const o=1e3/this.options.fps;for(;this.accumulatedTime>=o;)this.accumulatedTime-=o,this.advanceFrame();this.rafId=requestAnimationFrame(this.tick)},this.options={...u,...t},this.loadImage()}attach(t){this.target=t,this.isLoaded&&this.render(),this.options.autoPlay&&this.play()}play(){this.destroyed||this.isPlaying||(this.isPlaying=!0,this.lastTimestamp=0,this.accumulatedTime=0,this.rafId=requestAnimationFrame(this.tick),this.notify())}pause(){this.isPlaying&&(this.isPlaying=!1,this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.notify())}stop(){this.pause(),this.currentFrame=0,this.render(),this.notify()}goToFrame(t){var e,o;const r=this.getTotalFrames();this.currentFrame=Math.max(0,Math.min(t,r-1)),this.render(),(o=(e=this.options).onFrameChange)==null||o.call(e,this.currentFrame),this.notify()}getState(){return{currentFrame:this.currentFrame,totalFrames:this.getTotalFrames(),isPlaying:this.isPlaying,isLoaded:this.isLoaded}}subscribe(t){return this.listeners.add(t),t(this.getState()),()=>this.listeners.delete(t)}updateOptions(t){const r=this.options.src,e=this.options.fps;this.options={...this.options,...t},t.src!==void 0&&t.src!==r?this.loadImage():this.isLoaded&&this.render(),t.fps!==void 0&&t.fps!==e&&(this.accumulatedTime=0)}destroy(){this.destroyed=!0,this.pause(),this.listeners.clear(),this.target&&this.options.renderer==="css"&&w(this.target),this.target=null,this.image=null}getTotalFrames(){return f(this.options.rows,this.options.cols)}loadImage(){this.isLoaded=!1;const t=new Image;t.crossOrigin="anonymous",t.onload=()=>{if(!this.destroyed){if(this.image=t,this.isLoaded=!0,!this.options.width||!this.options.height){const r=t.naturalWidth/this.options.cols,e=t.naturalHeight/this.options.rows;this.options.width=r,this.options.height=e}this.render(),this.notify(),this.options.autoPlay&&this.target&&this.play()}},t.onerror=()=>{console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`)},t.src=this.options.src}advanceFrame(){var e,o,n,s;const t=this.getTotalFrames(),r=this.currentFrame+1;r>=t?this.options.loop?this.currentFrame=0:(this.currentFrame=t-1,this.pause(),(o=(e=this.options).onComplete)==null||o.call(e)):this.currentFrame=r,this.render(),(s=(n=this.options).onFrameChange)==null||s.call(n,this.currentFrame),this.notify()}render(){if(!this.target||!this.isLoaded)return;const{src:t,rows:r,cols:e,width:o,height:n,renderer:s}=this.options;s==="canvas"&&this.target instanceof HTMLCanvasElement&&this.image?y(this.target,this.image,this.currentFrame,r,e,o,n):s==="css"&&this.target instanceof HTMLElement&&F(this.target,t,this.currentFrame,r,e,o,n)}notify(){const t=this.getState();this.listeners.forEach(r=>r(t))}}const P=a.defineComponent({__name:"Sprite",props:a.mergeDefaults({src:{},rows:{},cols:{},fps:{},loop:{type:Boolean},width:{},height:{},autoPlay:{type:Boolean},renderer:{},onComplete:{type:Function},onFrameChange:{type:Function},class:{}},u),emits:["complete","frameChange"],setup(i,{expose:t,emit:r}){const e=i,o=r,n=a.ref(null);let s=null;function l(){s==null||s.destroy(),s=new T({src:e.src,rows:e.rows,cols:e.cols,fps:e.fps,loop:e.loop,width:e.width,height:e.height,autoPlay:e.autoPlay,renderer:e.renderer,onComplete:()=>o("complete"),onFrameChange:h=>o("frameChange",h)}),n.value&&s.attach(n.value)}return a.onMounted(l),a.onBeforeUnmount(()=>{s==null||s.destroy(),s=null}),a.watch(()=>[e.src,e.rows,e.cols,e.fps,e.loop,e.width,e.height,e.autoPlay,e.renderer],l),t({play:()=>s==null?void 0:s.play(),pause:()=>s==null?void 0:s.pause(),stop:()=>s==null?void 0:s.stop(),goToFrame:h=>s==null?void 0:s.goToFrame(h),getState:()=>s==null?void 0:s.getState()}),(h,c)=>i.renderer==="canvas"?(a.openBlock(),a.createElementBlock("canvas",{key:0,ref_key:"targetRef",ref:n,class:a.normalizeClass(e.class),style:a.normalizeStyle({width:`${i.width}px`,height:`${i.height}px`})},null,6)):(a.openBlock(),a.createElementBlock("div",{key:1,ref_key:"targetRef",ref:n,class:a.normalizeClass(e.class),role:"img","aria-label":"Sprite animation",style:a.normalizeStyle({width:`${i.width}px`,height:`${i.height}px`})},null,6))}});exports.Sprite=P;
@@ -0,0 +1,2 @@
1
+ export { default as Sprite } from './Sprite.vue';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vue/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,cAAc,CAAC"}