mason-sprite 0.1.0 → 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/README.md CHANGED
@@ -1,9 +1,42 @@
1
1
  # mason-sprite
2
2
 
3
- Lightweight sprite sheet animation for **React**, **Vue**, and **Svelte** — one package, subpath imports.
3
+ [![npm version](https://img.shields.io/npm/v/mason-sprite.svg)](https://www.npmjs.com/package/mason-sprite)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
5
+
6
+ **v0.1.2** — Lightweight sprite sheet animation for **React**, **Vue**, and **Svelte** — one package, subpath imports.
4
7
 
5
8
  Drop in a PNG or WebP sprite sheet, set `rows`, `cols`, and `fps` — and you're done. No Lottie, no timeline editor. Just a simple **CSS** or **Canvas** sprite player.
6
9
 
10
+ **Demo & docs:** [mason-sprite.com](https://mason-sprite.com)
11
+
12
+ ## Preview
13
+
14
+ One sprite sheet, a few props — animation on screen.
15
+
16
+ <table>
17
+ <tr>
18
+ <td align="center" width="50%">
19
+ <strong>Sprite sheet</strong><br />
20
+ <code>img-cat-run.webp</code> · 2 rows × 5 cols
21
+ <br /><br />
22
+ <img src="./docs/assets/readme/img-cat-run.webp" alt="Cat run sprite sheet — 2 rows, 5 columns" width="420" />
23
+ </td>
24
+ <td align="center" width="50%">
25
+ <strong>Rendered with mason-sprite</strong><br />
26
+ <code>rows={2}</code> · <code>cols={5}</code> · <code>fps={10}</code>
27
+ <br /><br />
28
+ <img src="./docs/assets/readme/img-cat-run.gif" alt="Cat run animation rendered by mason-sprite" width="140" />
29
+ </td>
30
+ </tr>
31
+ </table>
32
+
33
+ ```
34
+ img-cat-run.webp → rows × cols → looping animation
35
+ (WebP sheet) (2 × 5) (CSS or Canvas)
36
+ ```
37
+
38
+ Try it live on **[mason-sprite.com](https://mason-sprite.com)**.
39
+
7
40
  ## Install
8
41
 
9
42
  ```bash
@@ -26,13 +59,13 @@ Peer dependencies (install only what you use):
26
59
  import { SpriteAnimator } from 'mason-sprite';
27
60
 
28
61
  const animator = new SpriteAnimator({
29
- src: '/sprites/hero.png',
62
+ src: '/sprites/cat-run.webp',
30
63
  rows: 2,
31
64
  cols: 5,
32
65
  fps: 10,
33
66
  loop: true,
34
- width: 140,
35
- height: 140,
67
+ width: '8rem',
68
+ height: '8rem',
36
69
  });
37
70
 
38
71
  animator.attach(document.getElementById('sprite')!);
@@ -45,13 +78,13 @@ animator.play();
45
78
  import { Sprite } from 'mason-sprite/react';
46
79
 
47
80
  <Sprite
48
- src="/sprites/hero.png"
81
+ src="/sprites/cat-run.webp"
49
82
  rows={2}
50
83
  cols={5}
51
84
  fps={10}
52
85
  loop
53
- width={140}
54
- height={140}
86
+ width="8rem"
87
+ height="8rem"
55
88
  />
56
89
  ```
57
90
 
@@ -64,13 +97,13 @@ import { Sprite } from 'mason-sprite/vue';
64
97
 
65
98
  <template>
66
99
  <Sprite
67
- src="/sprites/hero.png"
100
+ src="/sprites/cat-run.webp"
68
101
  :rows="2"
69
102
  :cols="5"
70
103
  :fps="10"
71
104
  :loop="true"
72
- :width="140"
73
- :height="140"
105
+ width="8rem"
106
+ height="8rem"
74
107
  />
75
108
  </template>
76
109
  ```
@@ -83,13 +116,13 @@ import { Sprite } from 'mason-sprite/vue';
83
116
  </script>
84
117
 
85
118
  <Sprite
86
- src="/sprites/hero.png"
119
+ src="/sprites/cat-run.webp"
87
120
  rows={2}
88
121
  cols={5}
89
122
  fps={10}
90
123
  loop
91
- width={140}
92
- height={140}
124
+ width="8rem"
125
+ height="8rem"
93
126
  />
94
127
  ```
95
128
 
@@ -106,6 +139,8 @@ import { Sprite } from 'mason-sprite/vue';
106
139
 
107
140
  - PNG / WebP sprite sheet support
108
141
  - CSS or Canvas rendering
142
+ - Responsive sizing — `width` / `height` accept CSS lengths (`rem`, `em`, `%`, `vw`, etc.)
143
+ - Canvas mode uses `ResizeObserver` and `devicePixelRatio` for sharp rendering
109
144
  - `play`, `pause`, `stop`, `goToFrame` controls
110
145
  - Works with any uniform grid sprite sheet (`rows × cols`)
111
146
 
package/dist/index.cjs CHANGED
@@ -24,7 +24,8 @@ __export(core_exports, {
24
24
  SpriteAnimator: () => SpriteAnimator,
25
25
  getBackgroundPositionPercent: () => getBackgroundPositionPercent,
26
26
  getFramePosition: () => getFramePosition,
27
- getTotalFrames: () => getTotalFrames
27
+ getTotalFrames: () => getTotalFrames,
28
+ toCssLength: () => toCssLength
28
29
  });
29
30
  module.exports = __toCommonJS(core_exports);
30
31
 
@@ -39,6 +40,9 @@ var SPRITE_ANIMATION_DEFAULTS = {
39
40
  };
40
41
 
41
42
  // src/core/utils.ts
43
+ function toCssLength(size) {
44
+ return typeof size === "number" ? `${size}px` : size;
45
+ }
42
46
  function getTotalFrames(rows, cols) {
43
47
  return rows * cols;
44
48
  }
@@ -56,15 +60,24 @@ function getBackgroundPositionPercent(frameIndex, rows, cols) {
56
60
  }
57
61
 
58
62
  // src/core/canvas-renderer.ts
59
- function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
63
+ function drawCanvasFrame(canvas, image, frameIndex, rows, cols) {
60
64
  const ctx = canvas.getContext("2d");
61
65
  if (!ctx) return;
66
+ const displayWidth = canvas.clientWidth;
67
+ const displayHeight = canvas.clientHeight;
68
+ if (displayWidth === 0 || displayHeight === 0) return;
69
+ const dpr = window.devicePixelRatio || 1;
70
+ const pixelWidth = Math.round(displayWidth * dpr);
71
+ const pixelHeight = Math.round(displayHeight * dpr);
72
+ if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
73
+ canvas.width = pixelWidth;
74
+ canvas.height = pixelHeight;
75
+ }
62
76
  const frameWidth = image.naturalWidth / cols;
63
77
  const frameHeight = image.naturalHeight / rows;
64
78
  const { row, col } = getFramePosition(frameIndex, cols);
65
- canvas.width = width;
66
- canvas.height = height;
67
- ctx.clearRect(0, 0, width, height);
79
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
80
+ ctx.clearRect(0, 0, displayWidth, displayHeight);
68
81
  ctx.drawImage(
69
82
  image,
70
83
  col * frameWidth,
@@ -73,8 +86,8 @@ function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
73
86
  frameHeight,
74
87
  0,
75
88
  0,
76
- width,
77
- height
89
+ displayWidth,
90
+ displayHeight
78
91
  );
79
92
  }
80
93
 
@@ -85,8 +98,8 @@ function applyCssFrame(target, src, frameIndex, rows, cols, width, height) {
85
98
  target.style.backgroundRepeat = "no-repeat";
86
99
  target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
87
100
  target.style.backgroundPosition = `${x}% ${y}%`;
88
- target.style.width = `${width}px`;
89
- target.style.height = `${height}px`;
101
+ target.style.width = toCssLength(width);
102
+ target.style.height = toCssLength(height);
90
103
  target.style.display = "inline-block";
91
104
  }
92
105
  function resetCssRenderer(target) {
@@ -106,6 +119,7 @@ var SpriteAnimator = class {
106
119
  this.target = null;
107
120
  this.listeners = /* @__PURE__ */ new Set();
108
121
  this.destroyed = false;
122
+ this.resizeObserver = null;
109
123
  this.tick = (timestamp) => {
110
124
  if (!this.isPlaying || this.destroyed) return;
111
125
  if (this.lastTimestamp === 0) {
@@ -129,6 +143,8 @@ var SpriteAnimator = class {
129
143
  }
130
144
  attach(target) {
131
145
  this.target = target;
146
+ this.applyCanvasDisplaySize();
147
+ this.setupResizeObserver();
132
148
  if (this.isLoaded) {
133
149
  this.render();
134
150
  }
@@ -182,6 +198,7 @@ var SpriteAnimator = class {
182
198
  updateOptions(partial) {
183
199
  const prevSrc = this.options.src;
184
200
  const prevFps = this.options.fps;
201
+ const prevRenderer = this.options.renderer;
185
202
  this.options = { ...this.options, ...partial };
186
203
  if (partial.src !== void 0 && partial.src !== prevSrc) {
187
204
  this.loadImage();
@@ -191,10 +208,18 @@ var SpriteAnimator = class {
191
208
  if (partial.fps !== void 0 && partial.fps !== prevFps) {
192
209
  this.accumulatedTime = 0;
193
210
  }
211
+ if (partial.width !== void 0 || partial.height !== void 0) {
212
+ this.applyCanvasDisplaySize();
213
+ }
214
+ if (partial.renderer !== void 0 && partial.renderer !== prevRenderer) {
215
+ this.setupResizeObserver();
216
+ }
194
217
  }
195
218
  destroy() {
196
219
  this.destroyed = true;
197
220
  this.pause();
221
+ this.resizeObserver?.disconnect();
222
+ this.resizeObserver = null;
198
223
  this.listeners.clear();
199
224
  if (this.target && this.options.renderer === "css") {
200
225
  resetCssRenderer(this.target);
@@ -252,7 +277,7 @@ var SpriteAnimator = class {
252
277
  if (!this.target || !this.isLoaded) return;
253
278
  const { src, rows, cols, width, height, renderer } = this.options;
254
279
  if (renderer === "canvas" && this.target instanceof HTMLCanvasElement && this.image) {
255
- drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols, width, height);
280
+ drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols);
256
281
  } else if (renderer === "css" && this.target instanceof HTMLElement) {
257
282
  applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);
258
283
  }
@@ -261,6 +286,26 @@ var SpriteAnimator = class {
261
286
  const state = this.getState();
262
287
  this.listeners.forEach((listener) => listener(state));
263
288
  }
289
+ applyCanvasDisplaySize() {
290
+ if (this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement)) {
291
+ return;
292
+ }
293
+ this.target.style.width = toCssLength(this.options.width);
294
+ this.target.style.height = toCssLength(this.options.height);
295
+ }
296
+ setupResizeObserver() {
297
+ this.resizeObserver?.disconnect();
298
+ this.resizeObserver = null;
299
+ if (this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement) || typeof ResizeObserver === "undefined") {
300
+ return;
301
+ }
302
+ this.resizeObserver = new ResizeObserver(() => {
303
+ if (this.isLoaded) {
304
+ this.render();
305
+ }
306
+ });
307
+ this.resizeObserver.observe(this.target);
308
+ }
264
309
  };
265
310
  // Annotate the CommonJS export names for ESM import in node:
266
311
  0 && (module.exports = {
@@ -268,6 +313,7 @@ var SpriteAnimator = class {
268
313
  SpriteAnimator,
269
314
  getBackgroundPositionPercent,
270
315
  getFramePosition,
271
- getTotalFrames
316
+ getTotalFrames,
317
+ toCssLength
272
318
  });
273
319
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/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"],"sourcesContent":["export { SPRITE_ANIMATION_DEFAULTS } from './constants.js';\nexport { SpriteAnimator } from './sprite-animator.js';\nexport type {\n FramePosition,\n RendererMode,\n SpriteAnimationOptions,\n SpriteAnimationState,\n} from './types.js';\nexport {\n getBackgroundPositionPercent,\n getFramePosition,\n getTotalFrames,\n} from './utils.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 } 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/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"],"sourcesContent":["export { SPRITE_ANIMATION_DEFAULTS } from './constants.js';\nexport { SpriteAnimator } from './sprite-animator.js';\nexport type {\n FramePosition,\n RendererMode,\n SpriteAnimationOptions,\n SpriteAnimationState,\n SpriteSize,\n} from './types.js';\nexport {\n getBackgroundPositionPercent,\n getFramePosition,\n getTotalFrames,\n toCssLength,\n} from './utils.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"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,6 @@
1
1
  type RendererMode = 'css' | 'canvas';
2
+ /** CSS length (e.g. `128`, `'8rem'`, `'50%'`, `'10vw'`) */
3
+ type SpriteSize = number | string;
2
4
  interface SpriteAnimationOptions {
3
5
  /** Sprite sheet image URL (PNG, WebP, etc.) */
4
6
  src: string;
@@ -10,10 +12,10 @@ interface SpriteAnimationOptions {
10
12
  fps?: number;
11
13
  /** Whether to loop the animation (default: true) */
12
14
  loop?: boolean;
13
- /** Display width in pixels */
14
- width?: number;
15
- /** Display height in pixels */
16
- height?: number;
15
+ /** Display width number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
16
+ width?: SpriteSize;
17
+ /** Display height number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
18
+ height?: SpriteSize;
17
19
  /** Start playing automatically (default: true) */
18
20
  autoPlay?: boolean;
19
21
  /** Rendering mode: CSS background-position or Canvas (default: 'css') */
@@ -56,6 +58,7 @@ declare class SpriteAnimator {
56
58
  private target;
57
59
  private listeners;
58
60
  private destroyed;
61
+ private resizeObserver;
59
62
  constructor(options: SpriteAnimationOptions);
60
63
  attach(target: HTMLElement | HTMLCanvasElement): void;
61
64
  play(): void;
@@ -72,8 +75,12 @@ declare class SpriteAnimator {
72
75
  private advanceFrame;
73
76
  private render;
74
77
  private notify;
78
+ private applyCanvasDisplaySize;
79
+ private setupResizeObserver;
75
80
  }
76
81
 
82
+ /** Converts a SpriteSize to a CSS length string. Numbers become `px`; strings pass through. */
83
+ declare function toCssLength(size: SpriteSize): string;
77
84
  declare function getTotalFrames(rows: number, cols: number): number;
78
85
  declare function getFramePosition(frameIndex: number, cols: number): FramePosition;
79
86
  declare function getBackgroundPositionPercent(frameIndex: number, rows: number, cols: number): {
@@ -81,4 +88,4 @@ declare function getBackgroundPositionPercent(frameIndex: number, rows: number,
81
88
  y: number;
82
89
  };
83
90
 
84
- export { type FramePosition, type RendererMode, SPRITE_ANIMATION_DEFAULTS, type SpriteAnimationOptions, type SpriteAnimationState, SpriteAnimator, getBackgroundPositionPercent, getFramePosition, getTotalFrames };
91
+ export { type FramePosition, type RendererMode, SPRITE_ANIMATION_DEFAULTS, type SpriteAnimationOptions, type SpriteAnimationState, SpriteAnimator, type SpriteSize, getBackgroundPositionPercent, getFramePosition, getTotalFrames, toCssLength };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  type RendererMode = 'css' | 'canvas';
2
+ /** CSS length (e.g. `128`, `'8rem'`, `'50%'`, `'10vw'`) */
3
+ type SpriteSize = number | string;
2
4
  interface SpriteAnimationOptions {
3
5
  /** Sprite sheet image URL (PNG, WebP, etc.) */
4
6
  src: string;
@@ -10,10 +12,10 @@ interface SpriteAnimationOptions {
10
12
  fps?: number;
11
13
  /** Whether to loop the animation (default: true) */
12
14
  loop?: boolean;
13
- /** Display width in pixels */
14
- width?: number;
15
- /** Display height in pixels */
16
- height?: number;
15
+ /** Display width number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
16
+ width?: SpriteSize;
17
+ /** Display height number (px) or any CSS length (`rem`, `em`, `%`, `vw`, etc.) */
18
+ height?: SpriteSize;
17
19
  /** Start playing automatically (default: true) */
18
20
  autoPlay?: boolean;
19
21
  /** Rendering mode: CSS background-position or Canvas (default: 'css') */
@@ -56,6 +58,7 @@ declare class SpriteAnimator {
56
58
  private target;
57
59
  private listeners;
58
60
  private destroyed;
61
+ private resizeObserver;
59
62
  constructor(options: SpriteAnimationOptions);
60
63
  attach(target: HTMLElement | HTMLCanvasElement): void;
61
64
  play(): void;
@@ -72,8 +75,12 @@ declare class SpriteAnimator {
72
75
  private advanceFrame;
73
76
  private render;
74
77
  private notify;
78
+ private applyCanvasDisplaySize;
79
+ private setupResizeObserver;
75
80
  }
76
81
 
82
+ /** Converts a SpriteSize to a CSS length string. Numbers become `px`; strings pass through. */
83
+ declare function toCssLength(size: SpriteSize): string;
77
84
  declare function getTotalFrames(rows: number, cols: number): number;
78
85
  declare function getFramePosition(frameIndex: number, cols: number): FramePosition;
79
86
  declare function getBackgroundPositionPercent(frameIndex: number, rows: number, cols: number): {
@@ -81,4 +88,4 @@ declare function getBackgroundPositionPercent(frameIndex: number, rows: number,
81
88
  y: number;
82
89
  };
83
90
 
84
- export { type FramePosition, type RendererMode, SPRITE_ANIMATION_DEFAULTS, type SpriteAnimationOptions, type SpriteAnimationState, SpriteAnimator, getBackgroundPositionPercent, getFramePosition, getTotalFrames };
91
+ export { type FramePosition, type RendererMode, SPRITE_ANIMATION_DEFAULTS, type SpriteAnimationOptions, type SpriteAnimationState, SpriteAnimator, type SpriteSize, getBackgroundPositionPercent, getFramePosition, getTotalFrames, toCssLength };
package/dist/index.js CHANGED
@@ -9,6 +9,9 @@ var SPRITE_ANIMATION_DEFAULTS = {
9
9
  };
10
10
 
11
11
  // src/core/utils.ts
12
+ function toCssLength(size) {
13
+ return typeof size === "number" ? `${size}px` : size;
14
+ }
12
15
  function getTotalFrames(rows, cols) {
13
16
  return rows * cols;
14
17
  }
@@ -26,15 +29,24 @@ function getBackgroundPositionPercent(frameIndex, rows, cols) {
26
29
  }
27
30
 
28
31
  // src/core/canvas-renderer.ts
29
- function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
32
+ function drawCanvasFrame(canvas, image, frameIndex, rows, cols) {
30
33
  const ctx = canvas.getContext("2d");
31
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
+ }
32
45
  const frameWidth = image.naturalWidth / cols;
33
46
  const frameHeight = image.naturalHeight / rows;
34
47
  const { row, col } = getFramePosition(frameIndex, cols);
35
- canvas.width = width;
36
- canvas.height = height;
37
- ctx.clearRect(0, 0, width, height);
48
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
49
+ ctx.clearRect(0, 0, displayWidth, displayHeight);
38
50
  ctx.drawImage(
39
51
  image,
40
52
  col * frameWidth,
@@ -43,8 +55,8 @@ function drawCanvasFrame(canvas, image, frameIndex, rows, cols, width, height) {
43
55
  frameHeight,
44
56
  0,
45
57
  0,
46
- width,
47
- height
58
+ displayWidth,
59
+ displayHeight
48
60
  );
49
61
  }
50
62
 
@@ -55,8 +67,8 @@ function applyCssFrame(target, src, frameIndex, rows, cols, width, height) {
55
67
  target.style.backgroundRepeat = "no-repeat";
56
68
  target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
57
69
  target.style.backgroundPosition = `${x}% ${y}%`;
58
- target.style.width = `${width}px`;
59
- target.style.height = `${height}px`;
70
+ target.style.width = toCssLength(width);
71
+ target.style.height = toCssLength(height);
60
72
  target.style.display = "inline-block";
61
73
  }
62
74
  function resetCssRenderer(target) {
@@ -76,6 +88,7 @@ var SpriteAnimator = class {
76
88
  this.target = null;
77
89
  this.listeners = /* @__PURE__ */ new Set();
78
90
  this.destroyed = false;
91
+ this.resizeObserver = null;
79
92
  this.tick = (timestamp) => {
80
93
  if (!this.isPlaying || this.destroyed) return;
81
94
  if (this.lastTimestamp === 0) {
@@ -99,6 +112,8 @@ var SpriteAnimator = class {
99
112
  }
100
113
  attach(target) {
101
114
  this.target = target;
115
+ this.applyCanvasDisplaySize();
116
+ this.setupResizeObserver();
102
117
  if (this.isLoaded) {
103
118
  this.render();
104
119
  }
@@ -152,6 +167,7 @@ var SpriteAnimator = class {
152
167
  updateOptions(partial) {
153
168
  const prevSrc = this.options.src;
154
169
  const prevFps = this.options.fps;
170
+ const prevRenderer = this.options.renderer;
155
171
  this.options = { ...this.options, ...partial };
156
172
  if (partial.src !== void 0 && partial.src !== prevSrc) {
157
173
  this.loadImage();
@@ -161,10 +177,18 @@ var SpriteAnimator = class {
161
177
  if (partial.fps !== void 0 && partial.fps !== prevFps) {
162
178
  this.accumulatedTime = 0;
163
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
+ }
164
186
  }
165
187
  destroy() {
166
188
  this.destroyed = true;
167
189
  this.pause();
190
+ this.resizeObserver?.disconnect();
191
+ this.resizeObserver = null;
168
192
  this.listeners.clear();
169
193
  if (this.target && this.options.renderer === "css") {
170
194
  resetCssRenderer(this.target);
@@ -222,7 +246,7 @@ var SpriteAnimator = class {
222
246
  if (!this.target || !this.isLoaded) return;
223
247
  const { src, rows, cols, width, height, renderer } = this.options;
224
248
  if (renderer === "canvas" && this.target instanceof HTMLCanvasElement && this.image) {
225
- drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols, width, height);
249
+ drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols);
226
250
  } else if (renderer === "css" && this.target instanceof HTMLElement) {
227
251
  applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);
228
252
  }
@@ -231,12 +255,33 @@ var SpriteAnimator = class {
231
255
  const state = this.getState();
232
256
  this.listeners.forEach((listener) => listener(state));
233
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
+ }
234
278
  };
235
279
  export {
236
280
  SPRITE_ANIMATION_DEFAULTS,
237
281
  SpriteAnimator,
238
282
  getBackgroundPositionPercent,
239
283
  getFramePosition,
240
- getTotalFrames
284
+ getTotalFrames,
285
+ toCssLength
241
286
  };
242
287
  //# sourceMappingURL=index.js.map