mason-sprite 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +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"]}
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","../../src/react/Sprite.tsx","../../src/react/useSprite.ts"],"sourcesContent":["import type { RendererMode } from './types.js';\n\nexport const SPRITE_ANIMATION_DEFAULTS = {\n fps: 12,\n loop: true,\n width: 128,\n height: 128,\n autoPlay: true,\n renderer: 'css' as RendererMode,\n} as const;\n","import type { FramePosition, SpriteSize } from './types.js';\n\n/** Converts a SpriteSize to a CSS length string. Numbers become `px`; strings pass through. */\nexport function toCssLength(size: SpriteSize): string {\n return typeof size === 'number' ? `${size}px` : size;\n}\n\nexport function getTotalFrames(rows: number, cols: number): number {\n return rows * cols;\n}\n\nexport function getFramePosition(frameIndex: number, cols: number): FramePosition {\n return {\n row: Math.floor(frameIndex / cols),\n col: frameIndex % cols,\n };\n}\n\nexport function getBackgroundPositionPercent(\n frameIndex: number,\n rows: number,\n cols: number,\n): { x: number; y: number } {\n const { row, col } = getFramePosition(frameIndex, cols);\n const x = cols <= 1 ? 0 : (col / (cols - 1)) * 100;\n const y = rows <= 1 ? 0 : (row / (rows - 1)) * 100;\n return { x, y };\n}\n","import { getFramePosition } from './utils.js';\n\nexport function drawCanvasFrame(\n canvas: HTMLCanvasElement,\n image: HTMLImageElement,\n frameIndex: number,\n rows: number,\n cols: number,\n): void {\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const displayWidth = canvas.clientWidth;\n const displayHeight = canvas.clientHeight;\n if (displayWidth === 0 || displayHeight === 0) return;\n\n const dpr = window.devicePixelRatio || 1;\n const pixelWidth = Math.round(displayWidth * dpr);\n const pixelHeight = Math.round(displayHeight * dpr);\n\n if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {\n canvas.width = pixelWidth;\n canvas.height = pixelHeight;\n }\n\n const frameWidth = image.naturalWidth / cols;\n const frameHeight = image.naturalHeight / rows;\n const { row, col } = getFramePosition(frameIndex, cols);\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, displayWidth, displayHeight);\n ctx.drawImage(\n image,\n col * frameWidth,\n row * frameHeight,\n frameWidth,\n frameHeight,\n 0,\n 0,\n displayWidth,\n displayHeight,\n );\n}\n","import type { SpriteSize } from './types.js';\nimport { getBackgroundPositionPercent, toCssLength } from './utils.js';\n\nexport interface CssRendererTarget {\n style: CSSStyleDeclaration;\n}\n\nexport function applyCssFrame(\n target: CssRendererTarget,\n src: string,\n frameIndex: number,\n rows: number,\n cols: number,\n width: SpriteSize,\n height: SpriteSize,\n): void {\n const { x, y } = getBackgroundPositionPercent(frameIndex, rows, cols);\n\n target.style.backgroundImage = `url(\"${src}\")`;\n target.style.backgroundRepeat = 'no-repeat';\n target.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;\n target.style.backgroundPosition = `${x}% ${y}%`;\n target.style.width = toCssLength(width);\n target.style.height = toCssLength(height);\n target.style.display = 'inline-block';\n}\n\nexport function resetCssRenderer(target: CssRendererTarget): void {\n target.style.backgroundImage = '';\n}\n","import { SPRITE_ANIMATION_DEFAULTS } from './constants.js';\nimport { drawCanvasFrame } from './canvas-renderer.js';\nimport { applyCssFrame, resetCssRenderer } from './css-renderer.js';\nimport type { SpriteAnimationOptions, SpriteAnimationState } from './types.js';\nimport { getTotalFrames, toCssLength } from './utils.js';\n\ntype StateListener = (state: SpriteAnimationState) => void;\n\ntype ResolvedSpriteAnimationOptions = Required<\n Pick<SpriteAnimationOptions, 'src' | 'rows' | 'cols'>\n> &\n Required<Pick<SpriteAnimationOptions, 'fps' | 'loop' | 'width' | 'height' | 'autoPlay' | 'renderer'>> &\n Pick<SpriteAnimationOptions, 'onComplete' | 'onFrameChange'>;\n\nexport class SpriteAnimator {\n private options: ResolvedSpriteAnimationOptions;\n private currentFrame = 0;\n private isPlaying = false;\n private isLoaded = false;\n private rafId: number | null = null;\n private lastTimestamp = 0;\n private accumulatedTime = 0;\n private image: HTMLImageElement | null = null;\n private target: HTMLElement | HTMLCanvasElement | null = null;\n private listeners = new Set<StateListener>();\n private destroyed = false;\n private resizeObserver: ResizeObserver | null = null;\n\n constructor(options: SpriteAnimationOptions) {\n this.options = {\n ...SPRITE_ANIMATION_DEFAULTS,\n ...options,\n };\n this.loadImage();\n }\n\n attach(target: HTMLElement | HTMLCanvasElement): void {\n this.target = target;\n this.applyCanvasDisplaySize();\n this.setupResizeObserver();\n if (this.isLoaded) {\n this.render();\n }\n if (this.options.autoPlay) {\n this.play();\n }\n }\n\n play(): void {\n if (this.destroyed || this.isPlaying) return;\n this.isPlaying = true;\n this.lastTimestamp = 0;\n this.accumulatedTime = 0;\n this.rafId = requestAnimationFrame(this.tick);\n this.notify();\n }\n\n pause(): void {\n if (!this.isPlaying) return;\n this.isPlaying = false;\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.notify();\n }\n\n stop(): void {\n this.pause();\n this.currentFrame = 0;\n this.render();\n this.notify();\n }\n\n goToFrame(frame: number): void {\n const total = this.getTotalFrames();\n this.currentFrame = Math.max(0, Math.min(frame, total - 1));\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n getState(): SpriteAnimationState {\n return {\n currentFrame: this.currentFrame,\n totalFrames: this.getTotalFrames(),\n isPlaying: this.isPlaying,\n isLoaded: this.isLoaded,\n };\n }\n\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => this.listeners.delete(listener);\n }\n\n updateOptions(partial: Partial<SpriteAnimationOptions>): void {\n const prevSrc = this.options.src;\n const prevFps = this.options.fps;\n const prevRenderer = this.options.renderer;\n this.options = { ...this.options, ...partial };\n\n if (partial.src !== undefined && partial.src !== prevSrc) {\n this.loadImage();\n } else if (this.isLoaded) {\n this.render();\n }\n\n if (partial.fps !== undefined && partial.fps !== prevFps) {\n this.accumulatedTime = 0;\n }\n\n if (partial.width !== undefined || partial.height !== undefined) {\n this.applyCanvasDisplaySize();\n }\n\n if (partial.renderer !== undefined && partial.renderer !== prevRenderer) {\n this.setupResizeObserver();\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n this.pause();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.listeners.clear();\n if (this.target && this.options.renderer === 'css') {\n resetCssRenderer(this.target);\n }\n this.target = null;\n this.image = null;\n }\n\n private getTotalFrames(): number {\n return getTotalFrames(this.options.rows, this.options.cols);\n }\n\n private loadImage(): void {\n this.isLoaded = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n if (this.destroyed) return;\n this.image = img;\n this.isLoaded = true;\n\n if (!this.options.width || !this.options.height) {\n const frameWidth = img.naturalWidth / this.options.cols;\n const frameHeight = img.naturalHeight / this.options.rows;\n this.options.width = frameWidth;\n this.options.height = frameHeight;\n }\n\n this.render();\n this.notify();\n\n if (this.options.autoPlay && this.target) {\n this.play();\n }\n };\n img.onerror = () => {\n console.error(`[SpriteAnimator] Failed to load image: ${this.options.src}`);\n };\n img.src = this.options.src;\n }\n\n private tick = (timestamp: number): void => {\n if (!this.isPlaying || this.destroyed) return;\n\n if (this.lastTimestamp === 0) {\n this.lastTimestamp = timestamp;\n }\n\n const delta = timestamp - this.lastTimestamp;\n this.lastTimestamp = timestamp;\n this.accumulatedTime += delta;\n\n const frameDuration = 1000 / this.options.fps;\n while (this.accumulatedTime >= frameDuration) {\n this.accumulatedTime -= frameDuration;\n this.advanceFrame();\n }\n\n this.rafId = requestAnimationFrame(this.tick);\n };\n\n private advanceFrame(): void {\n const total = this.getTotalFrames();\n const next = this.currentFrame + 1;\n\n if (next >= total) {\n if (this.options.loop) {\n this.currentFrame = 0;\n } else {\n this.currentFrame = total - 1;\n this.pause();\n this.options.onComplete?.();\n }\n } else {\n this.currentFrame = next;\n }\n\n this.render();\n this.options.onFrameChange?.(this.currentFrame);\n this.notify();\n }\n\n private render(): void {\n if (!this.target || !this.isLoaded) return;\n\n const { src, rows, cols, width, height, renderer } = this.options;\n\n if (renderer === 'canvas' && this.target instanceof HTMLCanvasElement && this.image) {\n drawCanvasFrame(this.target, this.image, this.currentFrame, rows, cols);\n } else if (renderer === 'css' && this.target instanceof HTMLElement) {\n applyCssFrame(this.target, src, this.currentFrame, rows, cols, width, height);\n }\n }\n\n private notify(): void {\n const state = this.getState();\n this.listeners.forEach((listener) => listener(state));\n }\n\n private applyCanvasDisplaySize(): void {\n if (this.options.renderer !== 'canvas' || !(this.target instanceof HTMLCanvasElement)) {\n return;\n }\n this.target.style.width = toCssLength(this.options.width);\n this.target.style.height = toCssLength(this.options.height);\n }\n\n private setupResizeObserver(): void {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n\n if (\n this.options.renderer !== 'canvas' ||\n !(this.target instanceof HTMLCanvasElement) ||\n typeof ResizeObserver === 'undefined'\n ) {\n return;\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n if (this.isLoaded) {\n this.render();\n }\n });\n this.resizeObserver.observe(this.target);\n }\n}\n","import type { SpriteAnimationOptions } from '../core/types.js';\nimport { SPRITE_ANIMATION_DEFAULTS, toCssLength } from '../core/index.js';\nimport { forwardRef, useImperativeHandle } from 'react';\nimport { useSprite } from './useSprite.js';\n\nexport interface SpriteProps extends SpriteAnimationOptions {\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport interface SpriteHandle {\n play: () => void;\n pause: () => void;\n stop: () => void;\n goToFrame: (frame: number) => void;\n}\n\nexport const Sprite = forwardRef<SpriteHandle, SpriteProps>(function Sprite(\n { className, style, width, height, ...options },\n ref,\n) {\n const { ref: targetRef, play, pause, stop, goToFrame } = useSprite({\n width,\n height,\n ...options,\n });\n\n useImperativeHandle(ref, () => ({ play, pause, stop, goToFrame }), [\n play,\n pause,\n stop,\n goToFrame,\n ]);\n\n const sizeStyle = {\n width: toCssLength(width ?? SPRITE_ANIMATION_DEFAULTS.width),\n height: toCssLength(height ?? SPRITE_ANIMATION_DEFAULTS.height),\n };\n\n if (options.renderer === 'canvas') {\n return (\n <canvas\n ref={targetRef as React.RefObject<HTMLCanvasElement>}\n className={className}\n style={{ ...sizeStyle, ...style }}\n />\n );\n }\n\n return (\n <div\n ref={targetRef as React.RefObject<HTMLDivElement>}\n className={className}\n style={{ ...sizeStyle, ...style }}\n role=\"img\"\n aria-label=\"Sprite animation\"\n />\n );\n});\n","import {\n SpriteAnimator,\n type SpriteAnimationOptions,\n type SpriteAnimationState,\n} from '../core/index.js';\nimport { useEffect, useRef, useState } from 'react';\n\nexport interface UseSpriteOptions extends SpriteAnimationOptions {\n /** Skip auto-attach; useful when controlling the target manually */\n enabled?: boolean;\n}\n\nexport interface UseSpriteReturn {\n ref: React.RefObject<HTMLElement | HTMLCanvasElement | null>;\n state: SpriteAnimationState;\n play: () => void;\n pause: () => void;\n stop: () => void;\n goToFrame: (frame: number) => void;\n}\n\nexport function useSprite(options: UseSpriteOptions): UseSpriteReturn {\n const ref = useRef<HTMLElement | HTMLCanvasElement | null>(null);\n const animatorRef = useRef<SpriteAnimator | null>(null);\n const [state, setState] = useState<SpriteAnimationState>({\n currentFrame: 0,\n totalFrames: options.rows * options.cols,\n isPlaying: false,\n isLoaded: false,\n });\n\n const { enabled = true, ...animatorOptions } = options;\n\n useEffect(() => {\n if (!enabled) return;\n\n const animator = new SpriteAnimator(animatorOptions);\n animatorRef.current = animator;\n\n const unsubscribe = animator.subscribe(setState);\n\n queueMicrotask(() => {\n if (ref.current) {\n animator.attach(ref.current);\n }\n });\n\n return () => {\n unsubscribe();\n animator.destroy();\n animatorRef.current = null;\n };\n }, [enabled]);\n\n useEffect(() => {\n animatorRef.current?.updateOptions(animatorOptions);\n }, [\n animatorOptions.src,\n animatorOptions.rows,\n animatorOptions.cols,\n animatorOptions.fps,\n animatorOptions.loop,\n animatorOptions.width,\n animatorOptions.height,\n animatorOptions.autoPlay,\n animatorOptions.renderer,\n ]);\n\n return {\n ref,\n state,\n play: () => animatorRef.current?.play(),\n pause: () => animatorRef.current?.pause(),\n stop: () => animatorRef.current?.stop(),\n goToFrame: (frame) => animatorRef.current?.goToFrame(frame),\n };\n}\n"],"mappings":"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","names":["Sprite"]}
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  SPRITE_ANIMATION_DEFAULTS,
4
4
  SpriteAnimator,
5
+ toCssLength,
5
6
  type SpriteAnimationOptions,
6
7
  } from 'mason-sprite';
7
8
 
@@ -76,13 +77,13 @@
76
77
  </script>
77
78
 
78
79
  {#if renderer === 'canvas'}
79
- <canvas bind:this={target} class={className} style="width: {width}px; height: {height}px;"></canvas>
80
+ <canvas bind:this={target} class={className} style="width: {toCssLength(width)}; height: {toCssLength(height)};"></canvas>
80
81
  {:else}
81
82
  <div
82
83
  bind:this={target}
83
84
  class={className}
84
85
  role="img"
85
86
  aria-label="Sprite animation"
86
- style="width: {width}px; height: {height}px;"
87
+ style="width: {toCssLength(width)}; height: {toCssLength(height)};"
87
88
  ></div>
88
89
  {/if}
@@ -1 +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"}
1
+ {"version":3,"file":"Sprite.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/Sprite.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAIH,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"}
@@ -17,8 +17,8 @@ declare const _default: import('vue').DefineComponent<__VLS_Props, {
17
17
  }>, {
18
18
  fps: number;
19
19
  loop: boolean;
20
- width: number;
21
- height: number;
20
+ width: import('../core/types.js').SpriteSize;
21
+ height: import('../core/types.js').SpriteSize;
22
22
  autoPlay: boolean;
23
23
  renderer: import('../core/types.js').RendererMode;
24
24
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
@@ -1 +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"}
1
+ {"version":3,"file":"Sprite.vue.d.ts","sourceRoot":"","sources":["../../src/vue/Sprite.vue"],"names":[],"mappings":"AAgGA,OAAO,EAA0D,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAGvH,KAAK,WAAW,GAAG,sBAAsB,GAAG;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;;;;;uBAqEe,MAAM;;;;;;;;;;;;;;;;;;AAsE3B,wBAUG"}
@@ -1 +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;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("vue"),f={fps:12,loop:!0,width:128,height:128,autoPlay:!0,renderer:"css"};function d(s){return typeof s=="number"?`${s}px`:s}function v(s,t){return s*t}function g(s,t){return{row:Math.floor(s/t),col:s%t}}function w(s,t,r){const{row:e,col:n}=g(s,r),a=r<=1?0:n/(r-1)*100,o=t<=1?0:e/(t-1)*100;return{x:a,y:o}}function T(s,t,r,e,n){const a=s.getContext("2d");if(!a)return;const o=s.clientWidth,i=s.clientHeight;if(o===0||i===0)return;const l=window.devicePixelRatio||1,c=Math.round(o*l),u=Math.round(i*l);(s.width!==c||s.height!==u)&&(s.width=c,s.height=u);const p=t.naturalWidth/n,m=t.naturalHeight/e,{row:y,col:F}=g(r,n);a.setTransform(l,0,0,l,0,0),a.clearRect(0,0,o,i),a.drawImage(t,F*p,y*m,p,m,0,0,o,i)}function b(s,t,r,e,n,a,o){const{x:i,y:l}=w(r,e,n);s.style.backgroundImage=`url("${t}")`,s.style.backgroundRepeat="no-repeat",s.style.backgroundSize=`${n*100}% ${e*100}%`,s.style.backgroundPosition=`${i}% ${l}%`,s.style.width=d(a),s.style.height=d(o),s.style.display="inline-block"}function C(s){s.style.backgroundImage=""}class S{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.resizeObserver=null,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 n=1e3/this.options.fps;for(;this.accumulatedTime>=n;)this.accumulatedTime-=n,this.advanceFrame();this.rafId=requestAnimationFrame(this.tick)},this.options={...f,...t},this.loadImage()}attach(t){this.target=t,this.applyCanvasDisplaySize(),this.setupResizeObserver(),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,n;const r=this.getTotalFrames();this.currentFrame=Math.max(0,Math.min(t,r-1)),this.render(),(n=(e=this.options).onFrameChange)==null||n.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,n=this.options.renderer;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),(t.width!==void 0||t.height!==void 0)&&this.applyCanvasDisplaySize(),t.renderer!==void 0&&t.renderer!==n&&this.setupResizeObserver()}destroy(){var t;this.destroyed=!0,this.pause(),(t=this.resizeObserver)==null||t.disconnect(),this.resizeObserver=null,this.listeners.clear(),this.target&&this.options.renderer==="css"&&C(this.target),this.target=null,this.image=null}getTotalFrames(){return v(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,n,a,o;const t=this.getTotalFrames(),r=this.currentFrame+1;r>=t?this.options.loop?this.currentFrame=0:(this.currentFrame=t-1,this.pause(),(n=(e=this.options).onComplete)==null||n.call(e)):this.currentFrame=r,this.render(),(o=(a=this.options).onFrameChange)==null||o.call(a,this.currentFrame),this.notify()}render(){if(!this.target||!this.isLoaded)return;const{src:t,rows:r,cols:e,width:n,height:a,renderer:o}=this.options;o==="canvas"&&this.target instanceof HTMLCanvasElement&&this.image?T(this.target,this.image,this.currentFrame,r,e):o==="css"&&this.target instanceof HTMLElement&&b(this.target,t,this.currentFrame,r,e,n,a)}notify(){const t=this.getState();this.listeners.forEach(r=>r(t))}applyCanvasDisplaySize(){this.options.renderer!=="canvas"||!(this.target instanceof HTMLCanvasElement)||(this.target.style.width=d(this.options.width),this.target.style.height=d(this.options.height))}setupResizeObserver(){var t;(t=this.resizeObserver)==null||t.disconnect(),this.resizeObserver=null,!(this.options.renderer!=="canvas"||!(this.target instanceof HTMLCanvasElement)||typeof ResizeObserver>"u")&&(this.resizeObserver=new ResizeObserver(()=>{this.isLoaded&&this.render()}),this.resizeObserver.observe(this.target))}}const P=h.defineComponent({__name:"Sprite",props:h.mergeDefaults({src:{},rows:{},cols:{},fps:{},loop:{type:Boolean},width:{},height:{},autoPlay:{type:Boolean},renderer:{},onComplete:{type:Function},onFrameChange:{type:Function},class:{}},f),emits:["complete","frameChange"],setup(s,{expose:t,emit:r}){const e=s,n=h.computed(()=>({width:d(e.width),height:d(e.height)})),a=r,o=h.ref(null);let i=null;function l(){i==null||i.destroy(),i=new S({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:()=>a("complete"),onFrameChange:c=>a("frameChange",c)}),o.value&&i.attach(o.value)}return h.onMounted(l),h.onBeforeUnmount(()=>{i==null||i.destroy(),i=null}),h.watch(()=>[e.src,e.rows,e.cols,e.fps,e.loop,e.width,e.height,e.autoPlay,e.renderer],l),t({play:()=>i==null?void 0:i.play(),pause:()=>i==null?void 0:i.pause(),stop:()=>i==null?void 0:i.stop(),goToFrame:c=>i==null?void 0:i.goToFrame(c),getState:()=>i==null?void 0:i.getState()}),(c,u)=>s.renderer==="canvas"?(h.openBlock(),h.createElementBlock("canvas",{key:0,ref_key:"targetRef",ref:o,class:h.normalizeClass(e.class),style:h.normalizeStyle(n.value)},null,6)):(h.openBlock(),h.createElementBlock("div",{key:1,ref_key:"targetRef",ref:o,class:h.normalizeClass(e.class),role:"img","aria-label":"Sprite animation",style:h.normalizeStyle(n.value)},null,6))}});exports.Sprite=P;
package/dist/vue/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { defineComponent as F, ref as w, onMounted as T, onBeforeUnmount as P, watch as k, openBlock as c, createElementBlock as u, normalizeStyle as d, normalizeClass as m, mergeDefaults as C } from "vue";
2
- const p = {
1
+ import { defineComponent as b, computed as C, ref as P, onMounted as S, onBeforeUnmount as z, watch as k, openBlock as m, createElementBlock as f, normalizeStyle as g, normalizeClass as y, mergeDefaults as I } from "vue";
2
+ const F = {
3
3
  fps: 12,
4
4
  loop: !0,
5
5
  width: 128,
@@ -7,60 +7,67 @@ const p = {
7
7
  autoPlay: !0,
8
8
  renderer: "css"
9
9
  };
10
- function S(i, t) {
11
- return i * t;
10
+ function c(s) {
11
+ return typeof s == "number" ? `${s}px` : s;
12
12
  }
13
- function f(i, t) {
13
+ function L(s, t) {
14
+ return s * t;
15
+ }
16
+ function v(s, t) {
14
17
  return {
15
- row: Math.floor(i / t),
16
- col: i % t
18
+ row: Math.floor(s / t),
19
+ col: s % t
17
20
  };
18
21
  }
19
- function I(i, t, r) {
20
- const { row: e, col: o } = f(i, r), n = r <= 1 ? 0 : o / (r - 1) * 100, s = t <= 1 ? 0 : e / (t - 1) * 100;
21
- return { x: n, y: s };
22
+ function O(s, t, r) {
23
+ const { row: e, col: n } = v(s, r), a = r <= 1 ? 0 : n / (r - 1) * 100, o = t <= 1 ? 0 : e / (t - 1) * 100;
24
+ return { x: a, y: o };
22
25
  }
23
- function x(i, t, r, e, o, n, s) {
24
- const h = i.getContext("2d");
25
- if (!h) return;
26
- const a = t.naturalWidth / o, l = t.naturalHeight / e, { row: g, col: y } = f(r, o);
27
- i.width = n, i.height = s, h.clearRect(0, 0, n, s), h.drawImage(
26
+ function R(s, t, r, e, n) {
27
+ const a = s.getContext("2d");
28
+ if (!a) return;
29
+ const o = s.clientWidth, i = s.clientHeight;
30
+ if (o === 0 || i === 0) return;
31
+ const h = window.devicePixelRatio || 1, l = Math.round(o * h), d = Math.round(i * h);
32
+ (s.width !== l || s.height !== d) && (s.width = l, s.height = d);
33
+ const u = t.naturalWidth / n, p = t.naturalHeight / e, { row: w, col: T } = v(r, n);
34
+ a.setTransform(h, 0, 0, h, 0, 0), a.clearRect(0, 0, o, i), a.drawImage(
28
35
  t,
29
- y * a,
30
- g * l,
31
- a,
32
- l,
36
+ T * u,
37
+ w * p,
38
+ u,
39
+ p,
33
40
  0,
34
41
  0,
35
- n,
36
- s
42
+ o,
43
+ i
37
44
  );
38
45
  }
39
- function v(i, t, r, e, o, n, s) {
40
- const { x: h, y: a } = I(r, e, o);
41
- i.style.backgroundImage = `url("${t}")`, i.style.backgroundRepeat = "no-repeat", i.style.backgroundSize = `${o * 100}% ${e * 100}%`, i.style.backgroundPosition = `${h}% ${a}%`, i.style.width = `${n}px`, i.style.height = `${s}px`, i.style.display = "inline-block";
46
+ function x(s, t, r, e, n, a, o) {
47
+ const { x: i, y: h } = O(r, e, n);
48
+ s.style.backgroundImage = `url("${t}")`, s.style.backgroundRepeat = "no-repeat", s.style.backgroundSize = `${n * 100}% ${e * 100}%`, s.style.backgroundPosition = `${i}% ${h}%`, s.style.width = c(a), s.style.height = c(o), s.style.display = "inline-block";
42
49
  }
43
- function $(i) {
44
- i.style.backgroundImage = "";
50
+ function H(s) {
51
+ s.style.backgroundImage = "";
45
52
  }
46
- class L {
53
+ class M {
47
54
  constructor(t) {
48
- 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 = /* @__PURE__ */ new Set(), this.destroyed = !1, this.tick = (r) => {
55
+ 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 = /* @__PURE__ */ new Set(), this.destroyed = !1, this.resizeObserver = null, this.tick = (r) => {
49
56
  if (!this.isPlaying || this.destroyed) return;
50
57
  this.lastTimestamp === 0 && (this.lastTimestamp = r);
51
58
  const e = r - this.lastTimestamp;
52
59
  this.lastTimestamp = r, this.accumulatedTime += e;
53
- const o = 1e3 / this.options.fps;
54
- for (; this.accumulatedTime >= o; )
55
- this.accumulatedTime -= o, this.advanceFrame();
60
+ const n = 1e3 / this.options.fps;
61
+ for (; this.accumulatedTime >= n; )
62
+ this.accumulatedTime -= n, this.advanceFrame();
56
63
  this.rafId = requestAnimationFrame(this.tick);
57
64
  }, this.options = {
58
- ...p,
65
+ ...F,
59
66
  ...t
60
67
  }, this.loadImage();
61
68
  }
62
69
  attach(t) {
63
- this.target = t, this.isLoaded && this.render(), this.options.autoPlay && this.play();
70
+ this.target = t, this.applyCanvasDisplaySize(), this.setupResizeObserver(), this.isLoaded && this.render(), this.options.autoPlay && this.play();
64
71
  }
65
72
  play() {
66
73
  this.destroyed || this.isPlaying || (this.isPlaying = !0, this.lastTimestamp = 0, this.accumulatedTime = 0, this.rafId = requestAnimationFrame(this.tick), this.notify());
@@ -72,9 +79,9 @@ class L {
72
79
  this.pause(), this.currentFrame = 0, this.render(), this.notify();
73
80
  }
74
81
  goToFrame(t) {
75
- var e, o;
82
+ var e, n;
76
83
  const r = this.getTotalFrames();
77
- 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();
84
+ this.currentFrame = Math.max(0, Math.min(t, r - 1)), this.render(), (n = (e = this.options).onFrameChange) == null || n.call(e, this.currentFrame), this.notify();
78
85
  }
79
86
  getState() {
80
87
  return {
@@ -88,14 +95,15 @@ class L {
88
95
  return this.listeners.add(t), t(this.getState()), () => this.listeners.delete(t);
89
96
  }
90
97
  updateOptions(t) {
91
- const r = this.options.src, e = this.options.fps;
92
- 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);
98
+ const r = this.options.src, e = this.options.fps, n = this.options.renderer;
99
+ 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), (t.width !== void 0 || t.height !== void 0) && this.applyCanvasDisplaySize(), t.renderer !== void 0 && t.renderer !== n && this.setupResizeObserver();
93
100
  }
94
101
  destroy() {
95
- this.destroyed = !0, this.pause(), this.listeners.clear(), this.target && this.options.renderer === "css" && $(this.target), this.target = null, this.image = null;
102
+ var t;
103
+ this.destroyed = !0, this.pause(), (t = this.resizeObserver) == null || t.disconnect(), this.resizeObserver = null, this.listeners.clear(), this.target && this.options.renderer === "css" && H(this.target), this.target = null, this.image = null;
96
104
  }
97
105
  getTotalFrames() {
98
- return S(this.options.rows, this.options.cols);
106
+ return L(this.options.rows, this.options.cols);
99
107
  }
100
108
  loadImage() {
101
109
  this.isLoaded = !1;
@@ -113,23 +121,32 @@ class L {
113
121
  }, t.src = this.options.src;
114
122
  }
115
123
  advanceFrame() {
116
- var e, o, n, s;
124
+ var e, n, a, o;
117
125
  const t = this.getTotalFrames(), r = this.currentFrame + 1;
118
- 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();
126
+ r >= t ? this.options.loop ? this.currentFrame = 0 : (this.currentFrame = t - 1, this.pause(), (n = (e = this.options).onComplete) == null || n.call(e)) : this.currentFrame = r, this.render(), (o = (a = this.options).onFrameChange) == null || o.call(a, this.currentFrame), this.notify();
119
127
  }
120
128
  render() {
121
129
  if (!this.target || !this.isLoaded) return;
122
- const { src: t, rows: r, cols: e, width: o, height: n, renderer: s } = this.options;
123
- s === "canvas" && this.target instanceof HTMLCanvasElement && this.image ? x(this.target, this.image, this.currentFrame, r, e, o, n) : s === "css" && this.target instanceof HTMLElement && v(this.target, t, this.currentFrame, r, e, o, n);
130
+ const { src: t, rows: r, cols: e, width: n, height: a, renderer: o } = this.options;
131
+ o === "canvas" && this.target instanceof HTMLCanvasElement && this.image ? R(this.target, this.image, this.currentFrame, r, e) : o === "css" && this.target instanceof HTMLElement && x(this.target, t, this.currentFrame, r, e, n, a);
124
132
  }
125
133
  notify() {
126
134
  const t = this.getState();
127
135
  this.listeners.forEach((r) => r(t));
128
136
  }
137
+ applyCanvasDisplaySize() {
138
+ this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement) || (this.target.style.width = c(this.options.width), this.target.style.height = c(this.options.height));
139
+ }
140
+ setupResizeObserver() {
141
+ var t;
142
+ (t = this.resizeObserver) == null || t.disconnect(), this.resizeObserver = null, !(this.options.renderer !== "canvas" || !(this.target instanceof HTMLCanvasElement) || typeof ResizeObserver > "u") && (this.resizeObserver = new ResizeObserver(() => {
143
+ this.isLoaded && this.render();
144
+ }), this.resizeObserver.observe(this.target));
145
+ }
129
146
  }
130
- const A = /* @__PURE__ */ F({
147
+ const E = /* @__PURE__ */ b({
131
148
  __name: "Sprite",
132
- props: /* @__PURE__ */ C({
149
+ props: /* @__PURE__ */ I({
133
150
  src: {},
134
151
  rows: {},
135
152
  cols: {},
@@ -142,13 +159,16 @@ const A = /* @__PURE__ */ F({
142
159
  onComplete: { type: Function },
143
160
  onFrameChange: { type: Function },
144
161
  class: {}
145
- }, p),
162
+ }, F),
146
163
  emits: ["complete", "frameChange"],
147
- setup(i, { expose: t, emit: r }) {
148
- const e = i, o = r, n = w(null);
149
- let s = null;
164
+ setup(s, { expose: t, emit: r }) {
165
+ const e = s, n = C(() => ({
166
+ width: c(e.width),
167
+ height: c(e.height)
168
+ })), a = r, o = P(null);
169
+ let i = null;
150
170
  function h() {
151
- s == null || s.destroy(), s = new L({
171
+ i == null || i.destroy(), i = new M({
152
172
  src: e.src,
153
173
  rows: e.rows,
154
174
  cols: e.cols,
@@ -158,12 +178,12 @@ const A = /* @__PURE__ */ F({
158
178
  height: e.height,
159
179
  autoPlay: e.autoPlay,
160
180
  renderer: e.renderer,
161
- onComplete: () => o("complete"),
162
- onFrameChange: (a) => o("frameChange", a)
163
- }), n.value && s.attach(n.value);
181
+ onComplete: () => a("complete"),
182
+ onFrameChange: (l) => a("frameChange", l)
183
+ }), o.value && i.attach(o.value);
164
184
  }
165
- return T(h), P(() => {
166
- s == null || s.destroy(), s = null;
185
+ return S(h), z(() => {
186
+ i == null || i.destroy(), i = null;
167
187
  }), k(
168
188
  () => [
169
189
  e.src,
@@ -178,28 +198,28 @@ const A = /* @__PURE__ */ F({
178
198
  ],
179
199
  h
180
200
  ), t({
181
- play: () => s == null ? void 0 : s.play(),
182
- pause: () => s == null ? void 0 : s.pause(),
183
- stop: () => s == null ? void 0 : s.stop(),
184
- goToFrame: (a) => s == null ? void 0 : s.goToFrame(a),
185
- getState: () => s == null ? void 0 : s.getState()
186
- }), (a, l) => i.renderer === "canvas" ? (c(), u("canvas", {
201
+ play: () => i == null ? void 0 : i.play(),
202
+ pause: () => i == null ? void 0 : i.pause(),
203
+ stop: () => i == null ? void 0 : i.stop(),
204
+ goToFrame: (l) => i == null ? void 0 : i.goToFrame(l),
205
+ getState: () => i == null ? void 0 : i.getState()
206
+ }), (l, d) => s.renderer === "canvas" ? (m(), f("canvas", {
187
207
  key: 0,
188
208
  ref_key: "targetRef",
189
- ref: n,
190
- class: m(e.class),
191
- style: d({ width: `${i.width}px`, height: `${i.height}px` })
192
- }, null, 6)) : (c(), u("div", {
209
+ ref: o,
210
+ class: y(e.class),
211
+ style: g(n.value)
212
+ }, null, 6)) : (m(), f("div", {
193
213
  key: 1,
194
214
  ref_key: "targetRef",
195
- ref: n,
196
- class: m(e.class),
215
+ ref: o,
216
+ class: y(e.class),
197
217
  role: "img",
198
218
  "aria-label": "Sprite animation",
199
- style: d({ width: `${i.width}px`, height: `${i.height}px` })
219
+ style: g(n.value)
200
220
  }, null, 6));
201
221
  }
202
222
  });
203
223
  export {
204
- A as Sprite
224
+ E as Sprite
205
225
  };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "mason-sprite",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Lightweight sprite sheet animation for React, Vue, and Svelte",
5
+ "author": "mason <fe.hyunsu@gmail.com>",
5
6
  "type": "module",
6
7
  "main": "./dist/index.cjs",
7
8
  "module": "./dist/index.js",
@@ -45,16 +46,29 @@
45
46
  },
46
47
  "files": [
47
48
  "dist",
48
- "README.md"
49
+ "README.md",
50
+ "LICENSE"
49
51
  ],
50
52
  "repository": {
51
53
  "type": "git",
52
- "url": "git+https://github.com/FE-HyunSu/sprite-motion.git",
53
- "directory": "packages/mason-sprite"
54
+ "url": "git+https://github.com/FE-HyunSu/mason-sprite.git"
54
55
  },
55
- "homepage": "https://github.com/FE-HyunSu/sprite-motion#readme",
56
+ "homepage": "https://mason-sprite.com",
56
57
  "bugs": {
57
- "url": "https://github.com/FE-HyunSu/sprite-motion/issues"
58
+ "url": "https://github.com/FE-HyunSu/mason-sprite/issues"
59
+ },
60
+ "scripts": {
61
+ "build": "pnpm run build:js && pnpm run build:vue && pnpm run build:svelte",
62
+ "build:js": "tsup",
63
+ "build:vue": "vite build --config vite.vue.config.ts",
64
+ "build:svelte": "svelte-package --input src/svelte --output dist/svelte",
65
+ "clean": "rm -rf dist .svelte-kit",
66
+ "dev": "tsup --watch",
67
+ "typecheck": "tsc --noEmit && svelte-check --tsconfig ./tsconfig.json",
68
+ "lint": "eslint .",
69
+ "format": "prettier --write .",
70
+ "format:check": "prettier --check .",
71
+ "prepublishOnly": "pnpm run build"
58
72
  },
59
73
  "peerDependencies": {
60
74
  "react": ">=17",
@@ -63,31 +77,32 @@
63
77
  "vue": ">=3.3"
64
78
  },
65
79
  "peerDependenciesMeta": {
66
- "react": {
67
- "optional": true
68
- },
69
- "react-dom": {
70
- "optional": true
71
- },
72
- "vue": {
73
- "optional": true
74
- },
75
- "svelte": {
76
- "optional": true
77
- }
80
+ "react": { "optional": true },
81
+ "react-dom": { "optional": true },
82
+ "vue": { "optional": true },
83
+ "svelte": { "optional": true }
78
84
  },
79
85
  "devDependencies": {
86
+ "@eslint/js": "^9.28.0",
80
87
  "@sveltejs/package": "^2.3.12",
81
88
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
89
+ "@types/node": "^22.15.29",
82
90
  "@types/react": "^19.1.6",
83
91
  "@types/react-dom": "^19.1.5",
84
92
  "@vitejs/plugin-vue": "^5.2.4",
93
+ "eslint": "^9.28.0",
94
+ "eslint-config-prettier": "^10.1.5",
95
+ "eslint-plugin-svelte": "^3.9.0",
96
+ "globals": "^16.2.0",
97
+ "prettier": "^3.5.3",
98
+ "prettier-plugin-svelte": "^3.4.0",
85
99
  "react": "^19.1.0",
86
100
  "react-dom": "^19.1.0",
87
101
  "svelte": "^5.33.14",
88
102
  "svelte-check": "^4.2.1",
89
103
  "tsup": "^8.5.0",
90
104
  "typescript": "^5.8.3",
105
+ "typescript-eslint": "^8.33.1",
91
106
  "vite": "^6.3.5",
92
107
  "vite-plugin-dts": "^4.5.4",
93
108
  "vue": "^3.5.16"
@@ -103,12 +118,8 @@
103
118
  "css"
104
119
  ],
105
120
  "license": "MIT",
106
- "scripts": {
107
- "build": "pnpm run build:js && pnpm run build:vue && pnpm run build:svelte",
108
- "build:js": "tsup",
109
- "build:vue": "vite build --config vite.vue.config.ts",
110
- "build:svelte": "svelte-package --input src/svelte --output dist/svelte",
111
- "typecheck": "tsc --noEmit && svelte-check --tsconfig ./tsconfig.json",
112
- "dev": "tsup --watch"
121
+ "packageManager": "pnpm@9.15.9",
122
+ "engines": {
123
+ "node": ">=18"
113
124
  }
114
- }
125
+ }