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.
package/dist/index.js ADDED
@@ -0,0 +1,242 @@
1
+ // src/core/constants.ts
2
+ var SPRITE_ANIMATION_DEFAULTS = {
3
+ fps: 12,
4
+ loop: true,
5
+ width: 128,
6
+ height: 128,
7
+ autoPlay: true,
8
+ renderer: "css"
9
+ };
10
+
11
+ // src/core/utils.ts
12
+ function getTotalFrames(rows, cols) {
13
+ return rows * cols;
14
+ }
15
+ function getFramePosition(frameIndex, cols) {
16
+ return {
17
+ row: Math.floor(frameIndex / cols),
18
+ col: frameIndex % cols
19
+ };
20
+ }
21
+ function getBackgroundPositionPercent(frameIndex, rows, cols) {
22
+ const { row, col } = getFramePosition(frameIndex, cols);
23
+ const x = cols <= 1 ? 0 : col / (cols - 1) * 100;
24
+ const y = rows <= 1 ? 0 : row / (rows - 1) * 100;
25
+ return { x, y };
26
+ }
27
+
28
+ // src/core/canvas-renderer.ts
29
+ function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
30
+ const ctx = canvas.getContext("2d");
31
+ if (!ctx) return;
32
+ const frameWidth = image.naturalWidth / cols;
33
+ const frameHeight = image.naturalHeight / rows;
34
+ const { row, col } = getFramePosition(frameIndex, cols);
35
+ canvas.width = width;
36
+ canvas.height = height;
37
+ ctx.clearRect(0, 0, width, height);
38
+ ctx.drawImage(
39
+ image,
40
+ col * frameWidth,
41
+ row * frameHeight,
42
+ frameWidth,
43
+ frameHeight,
44
+ 0,
45
+ 0,
46
+ width,
47
+ height
48
+ );
49
+ }
50
+
51
+ // src/core/css-renderer.ts
52
+ function applyCssFrame(target, src, frameIndex, rows, cols, width, height) {
53
+ const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);
54
+ target.style.backgroundImage = `url("${src}")`;
55
+ target.style.backgroundRepeat = "no-repeat";
56
+ target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
57
+ target.style.backgroundPosition = `${x}% ${y}%`;
58
+ target.style.width = `${width}px`;
59
+ target.style.height = `${height}px`;
60
+ target.style.display = "inline-block";
61
+ }
62
+ function resetCssRenderer(target) {
63
+ target.style.backgroundImage = "";
64
+ }
65
+
66
+ // src/core/sprite-animator.ts
67
+ var SpriteAnimator = class {
68
+ constructor(options) {
69
+ this.currentFrame = 0;
70
+ this.isPlaying = false;
71
+ this.isLoaded = false;
72
+ this.rafId = null;
73
+ this.lastTimestamp = 0;
74
+ this.accumulatedTime = 0;
75
+ this.image = null;
76
+ this.target = null;
77
+ this.listeners = /* @__PURE__ */ new Set();
78
+ this.destroyed = false;
79
+ this.tick = (timestamp) => {
80
+ if (!this.isPlaying || this.destroyed) return;
81
+ if (this.lastTimestamp === 0) {
82
+ this.lastTimestamp = timestamp;
83
+ }
84
+ const delta = timestamp - this.lastTimestamp;
85
+ this.lastTimestamp = timestamp;
86
+ this.accumulatedTime += delta;
87
+ const frameDuration = 1e3 / this.options.fps;
88
+ while (this.accumulatedTime >= frameDuration) {
89
+ this.accumulatedTime -= frameDuration;
90
+ this.advanceFrame();
91
+ }
92
+ this.rafId = requestAnimationFrame(this.tick);
93
+ };
94
+ this.options = {
95
+ ...SPRITE_ANIMATION_DEFAULTS,
96
+ ...options
97
+ };
98
+ this.loadImage();
99
+ }
100
+ attach(target) {
101
+ this.target = target;
102
+ if (this.isLoaded) {
103
+ this.render();
104
+ }
105
+ if (this.options.autoPlay) {
106
+ this.play();
107
+ }
108
+ }
109
+ play() {
110
+ if (this.destroyed || this.isPlaying) return;
111
+ this.isPlaying = true;
112
+ this.lastTimestamp = 0;
113
+ this.accumulatedTime = 0;
114
+ this.rafId = requestAnimationFrame(this.tick);
115
+ this.notify();
116
+ }
117
+ pause() {
118
+ if (!this.isPlaying) return;
119
+ this.isPlaying = false;
120
+ if (this.rafId !== null) {
121
+ cancelAnimationFrame(this.rafId);
122
+ this.rafId = null;
123
+ }
124
+ this.notify();
125
+ }
126
+ stop() {
127
+ this.pause();
128
+ this.currentFrame = 0;
129
+ this.render();
130
+ this.notify();
131
+ }
132
+ goToFrame(frame) {
133
+ const total = this.getTotalFrames();
134
+ this.currentFrame = Math.max(0, Math.min(frame, total - 1));
135
+ this.render();
136
+ this.options.onFrameChange?.(this.currentFrame);
137
+ this.notify();
138
+ }
139
+ getState() {
140
+ return {
141
+ currentFrame: this.currentFrame,
142
+ totalFrames: this.getTotalFrames(),
143
+ isPlaying: this.isPlaying,
144
+ isLoaded: this.isLoaded
145
+ };
146
+ }
147
+ subscribe(listener) {
148
+ this.listeners.add(listener);
149
+ listener(this.getState());
150
+ return () => this.listeners.delete(listener);
151
+ }
152
+ updateOptions(partial) {
153
+ const prevSrc = this.options.src;
154
+ const prevFps = this.options.fps;
155
+ this.options = { ...this.options, ...partial };
156
+ if (partial.src !== void 0 && partial.src !== prevSrc) {
157
+ this.loadImage();
158
+ } else if (this.isLoaded) {
159
+ this.render();
160
+ }
161
+ if (partial.fps !== void 0 && partial.fps !== prevFps) {
162
+ this.accumulatedTime = 0;
163
+ }
164
+ }
165
+ destroy() {
166
+ this.destroyed = true;
167
+ this.pause();
168
+ this.listeners.clear();
169
+ if (this.target && this.options.renderer === "css") {
170
+ resetCssRenderer(this.target);
171
+ }
172
+ this.target = null;
173
+ this.image = null;
174
+ }
175
+ getTotalFrames() {
176
+ return getTotalFrames(this.options.rows, this.options.cols);
177
+ }
178
+ loadImage() {
179
+ this.isLoaded = false;
180
+ const img = new Image();
181
+ img.crossOrigin = "anonymous";
182
+ img.onload = () => {
183
+ if (this.destroyed) return;
184
+ this.image = img;
185
+ this.isLoaded = true;
186
+ if (!this.options.width || !this.options.height) {
187
+ const frameWidth = img.naturalWidth / this.options.cols;
188
+ const frameHeight = img.naturalHeight / this.options.rows;
189
+ this.options.width = frameWidth;
190
+ this.options.height = frameHeight;
191
+ }
192
+ this.render();
193
+ this.notify();
194
+ if (this.options.autoPlay && this.target) {
195
+ this.play();
196
+ }
197
+ };
198
+ img.onerror = () => {
199
+ console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);
200
+ };
201
+ img.src = this.options.src;
202
+ }
203
+ advanceFrame() {
204
+ const total = this.getTotalFrames();
205
+ const next = this.currentFrame + 1;
206
+ if (next >= total) {
207
+ if (this.options.loop) {
208
+ this.currentFrame = 0;
209
+ } else {
210
+ this.currentFrame = total - 1;
211
+ this.pause();
212
+ this.options.onComplete?.();
213
+ }
214
+ } else {
215
+ this.currentFrame = next;
216
+ }
217
+ this.render();
218
+ this.options.onFrameChange?.(this.currentFrame);
219
+ this.notify();
220
+ }
221
+ render() {
222
+ if (!this.target || !this.isLoaded) return;
223
+ const { src, rows, cols, width, height, renderer } = this.options;
224
+ if (renderer === "canvas" && this.target instanceof HTMLCanvasElement && this.image) {
225
+ drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols, width, height);
226
+ } else if (renderer === "css" && this.target instanceof HTMLElement) {
227
+ applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);
228
+ }
229
+ }
230
+ notify() {
231
+ const state = this.getState();
232
+ this.listeners.forEach((listener) => listener(state));
233
+ }
234
+ };
235
+ export {
236
+ SPRITE_ANIMATION_DEFAULTS,
237
+ SpriteAnimator,
238
+ getBackgroundPositionPercent,
239
+ getFramePosition,
240
+ getTotalFrames
241
+ };
242
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,353 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react/index.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ Sprite: () => Sprite,
24
+ useSprite: () => useSprite
25
+ });
26
+ module.exports = __toCommonJS(react_exports);
27
+
28
+ // src/react/Sprite.tsx
29
+ var import_react2 = require("react");
30
+
31
+ // src/core/constants.ts
32
+ var SPRITE_ANIMATION_DEFAULTS = {
33
+ fps: 12,
34
+ loop: true,
35
+ width: 128,
36
+ height: 128,
37
+ autoPlay: true,
38
+ renderer: "css"
39
+ };
40
+
41
+ // src/core/utils.ts
42
+ function getTotalFrames(rows, cols) {
43
+ return rows * cols;
44
+ }
45
+ function getFramePosition(frameIndex, cols) {
46
+ return {
47
+ row: Math.floor(frameIndex / cols),
48
+ col: frameIndex % cols
49
+ };
50
+ }
51
+ function getBackgroundPositionPercent(frameIndex, rows, cols) {
52
+ const { row, col } = getFramePosition(frameIndex, cols);
53
+ const x = cols <= 1 ? 0 : col / (cols - 1) * 100;
54
+ const y = rows <= 1 ? 0 : row / (rows - 1) * 100;
55
+ return { x, y };
56
+ }
57
+
58
+ // src/core/canvas-renderer.ts
59
+ function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
60
+ const ctx = canvas.getContext("2d");
61
+ if (!ctx) return;
62
+ const frameWidth = image.naturalWidth / cols;
63
+ const frameHeight = image.naturalHeight / rows;
64
+ const { row, col } = getFramePosition(frameIndex, cols);
65
+ canvas.width = width;
66
+ canvas.height = height;
67
+ ctx.clearRect(0, 0, width, height);
68
+ ctx.drawImage(
69
+ image,
70
+ col * frameWidth,
71
+ row * frameHeight,
72
+ frameWidth,
73
+ frameHeight,
74
+ 0,
75
+ 0,
76
+ width,
77
+ height
78
+ );
79
+ }
80
+
81
+ // src/core/css-renderer.ts
82
+ function applyCssFrame(target, src, frameIndex, rows, cols, width, height) {
83
+ const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);
84
+ target.style.backgroundImage = `url("${src}")`;
85
+ target.style.backgroundRepeat = "no-repeat";
86
+ target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
87
+ target.style.backgroundPosition = `${x}% ${y}%`;
88
+ target.style.width = `${width}px`;
89
+ target.style.height = `${height}px`;
90
+ target.style.display = "inline-block";
91
+ }
92
+ function resetCssRenderer(target) {
93
+ target.style.backgroundImage = "";
94
+ }
95
+
96
+ // src/core/sprite-animator.ts
97
+ var SpriteAnimator = class {
98
+ constructor(options) {
99
+ this.currentFrame = 0;
100
+ this.isPlaying = false;
101
+ this.isLoaded = false;
102
+ this.rafId = null;
103
+ this.lastTimestamp = 0;
104
+ this.accumulatedTime = 0;
105
+ this.image = null;
106
+ this.target = null;
107
+ this.listeners = /* @__PURE__ */ new Set();
108
+ this.destroyed = false;
109
+ this.tick = (timestamp) => {
110
+ if (!this.isPlaying || this.destroyed) return;
111
+ if (this.lastTimestamp === 0) {
112
+ this.lastTimestamp = timestamp;
113
+ }
114
+ const delta = timestamp - this.lastTimestamp;
115
+ this.lastTimestamp = timestamp;
116
+ this.accumulatedTime += delta;
117
+ const frameDuration = 1e3 / this.options.fps;
118
+ while (this.accumulatedTime >= frameDuration) {
119
+ this.accumulatedTime -= frameDuration;
120
+ this.advanceFrame();
121
+ }
122
+ this.rafId = requestAnimationFrame(this.tick);
123
+ };
124
+ this.options = {
125
+ ...SPRITE_ANIMATION_DEFAULTS,
126
+ ...options
127
+ };
128
+ this.loadImage();
129
+ }
130
+ attach(target) {
131
+ this.target = target;
132
+ if (this.isLoaded) {
133
+ this.render();
134
+ }
135
+ if (this.options.autoPlay) {
136
+ this.play();
137
+ }
138
+ }
139
+ play() {
140
+ if (this.destroyed || this.isPlaying) return;
141
+ this.isPlaying = true;
142
+ this.lastTimestamp = 0;
143
+ this.accumulatedTime = 0;
144
+ this.rafId = requestAnimationFrame(this.tick);
145
+ this.notify();
146
+ }
147
+ pause() {
148
+ if (!this.isPlaying) return;
149
+ this.isPlaying = false;
150
+ if (this.rafId !== null) {
151
+ cancelAnimationFrame(this.rafId);
152
+ this.rafId = null;
153
+ }
154
+ this.notify();
155
+ }
156
+ stop() {
157
+ this.pause();
158
+ this.currentFrame = 0;
159
+ this.render();
160
+ this.notify();
161
+ }
162
+ goToFrame(frame) {
163
+ const total = this.getTotalFrames();
164
+ this.currentFrame = Math.max(0, Math.min(frame, total - 1));
165
+ this.render();
166
+ this.options.onFrameChange?.(this.currentFrame);
167
+ this.notify();
168
+ }
169
+ getState() {
170
+ return {
171
+ currentFrame: this.currentFrame,
172
+ totalFrames: this.getTotalFrames(),
173
+ isPlaying: this.isPlaying,
174
+ isLoaded: this.isLoaded
175
+ };
176
+ }
177
+ subscribe(listener) {
178
+ this.listeners.add(listener);
179
+ listener(this.getState());
180
+ return () => this.listeners.delete(listener);
181
+ }
182
+ updateOptions(partial) {
183
+ const prevSrc = this.options.src;
184
+ const prevFps = this.options.fps;
185
+ this.options = { ...this.options, ...partial };
186
+ if (partial.src !== void 0 && partial.src !== prevSrc) {
187
+ this.loadImage();
188
+ } else if (this.isLoaded) {
189
+ this.render();
190
+ }
191
+ if (partial.fps !== void 0 && partial.fps !== prevFps) {
192
+ this.accumulatedTime = 0;
193
+ }
194
+ }
195
+ destroy() {
196
+ this.destroyed = true;
197
+ this.pause();
198
+ this.listeners.clear();
199
+ if (this.target && this.options.renderer === "css") {
200
+ resetCssRenderer(this.target);
201
+ }
202
+ this.target = null;
203
+ this.image = null;
204
+ }
205
+ getTotalFrames() {
206
+ return getTotalFrames(this.options.rows, this.options.cols);
207
+ }
208
+ loadImage() {
209
+ this.isLoaded = false;
210
+ const img = new Image();
211
+ img.crossOrigin = "anonymous";
212
+ img.onload = () => {
213
+ if (this.destroyed) return;
214
+ this.image = img;
215
+ this.isLoaded = true;
216
+ if (!this.options.width || !this.options.height) {
217
+ const frameWidth = img.naturalWidth / this.options.cols;
218
+ const frameHeight = img.naturalHeight / this.options.rows;
219
+ this.options.width = frameWidth;
220
+ this.options.height = frameHeight;
221
+ }
222
+ this.render();
223
+ this.notify();
224
+ if (this.options.autoPlay && this.target) {
225
+ this.play();
226
+ }
227
+ };
228
+ img.onerror = () => {
229
+ console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);
230
+ };
231
+ img.src = this.options.src;
232
+ }
233
+ advanceFrame() {
234
+ const total = this.getTotalFrames();
235
+ const next = this.currentFrame + 1;
236
+ if (next >= total) {
237
+ if (this.options.loop) {
238
+ this.currentFrame = 0;
239
+ } else {
240
+ this.currentFrame = total - 1;
241
+ this.pause();
242
+ this.options.onComplete?.();
243
+ }
244
+ } else {
245
+ this.currentFrame = next;
246
+ }
247
+ this.render();
248
+ this.options.onFrameChange?.(this.currentFrame);
249
+ this.notify();
250
+ }
251
+ render() {
252
+ if (!this.target || !this.isLoaded) return;
253
+ const { src, rows, cols, width, height, renderer } = this.options;
254
+ if (renderer === "canvas" && this.target instanceof HTMLCanvasElement && this.image) {
255
+ drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols, width, height);
256
+ } else if (renderer === "css" && this.target instanceof HTMLElement) {
257
+ applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);
258
+ }
259
+ }
260
+ notify() {
261
+ const state = this.getState();
262
+ this.listeners.forEach((listener) => listener(state));
263
+ }
264
+ };
265
+
266
+ // src/react/useSprite.ts
267
+ var import_react = require("react");
268
+ function useSprite(options) {
269
+ const ref = (0, import_react.useRef)(null);
270
+ const animatorRef = (0, import_react.useRef)(null);
271
+ const [state, setState] = (0, import_react.useState)({
272
+ currentFrame: 0,
273
+ totalFrames: options.rows * options.cols,
274
+ isPlaying: false,
275
+ isLoaded: false
276
+ });
277
+ const { enabled = true, ...animatorOptions } = options;
278
+ (0, import_react.useEffect)(() => {
279
+ if (!enabled) return;
280
+ const animator = new SpriteAnimator(animatorOptions);
281
+ animatorRef.current = animator;
282
+ const unsubscribe = animator.subscribe(setState);
283
+ queueMicrotask(() => {
284
+ if (ref.current) {
285
+ animator.attach(ref.current);
286
+ }
287
+ });
288
+ return () => {
289
+ unsubscribe();
290
+ animator.destroy();
291
+ animatorRef.current = null;
292
+ };
293
+ }, [enabled]);
294
+ (0, import_react.useEffect)(() => {
295
+ animatorRef.current?.updateOptions(animatorOptions);
296
+ }, [
297
+ animatorOptions.src,
298
+ animatorOptions.rows,
299
+ animatorOptions.cols,
300
+ animatorOptions.fps,
301
+ animatorOptions.loop,
302
+ animatorOptions.width,
303
+ animatorOptions.height,
304
+ animatorOptions.autoPlay,
305
+ animatorOptions.renderer
306
+ ]);
307
+ return {
308
+ ref,
309
+ state,
310
+ play: () => animatorRef.current?.play(),
311
+ pause: () => animatorRef.current?.pause(),
312
+ stop: () => animatorRef.current?.stop(),
313
+ goToFrame: (frame) => animatorRef.current?.goToFrame(frame)
314
+ };
315
+ }
316
+
317
+ // src/react/Sprite.tsx
318
+ 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(options);
321
+ (0, import_react2.useImperativeHandle)(ref, () => ({ play, pause, stop, goToFrame }), [
322
+ play,
323
+ pause,
324
+ stop,
325
+ goToFrame
326
+ ]);
327
+ if (options.renderer === "canvas") {
328
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
329
+ "canvas",
330
+ {
331
+ ref: targetRef,
332
+ className,
333
+ style
334
+ }
335
+ );
336
+ }
337
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
338
+ "div",
339
+ {
340
+ ref: targetRef,
341
+ className,
342
+ style,
343
+ role: "img",
344
+ "aria-label": "Sprite animation"
345
+ }
346
+ );
347
+ });
348
+ // Annotate the CommonJS export names for ESM import in node:
349
+ 0 && (module.exports = {
350
+ Sprite,
351
+ useSprite
352
+ });
353
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}