feat: add Sprite component
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
643e712c92
commit
6e22ec9d76
59
src/sprite.test.tsx
Normal file
59
src/sprite.test.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { Sprite } from "./sprite"
|
||||
|
||||
test("Sprite renders div with correct dimensions", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} />).toString()
|
||||
expect(html).toContain('width:32px')
|
||||
expect(html).toContain('height:32px')
|
||||
})
|
||||
|
||||
test("Sprite sets background-image", () => {
|
||||
const html = (<Sprite src="/warrior.png" width={32} height={32} frames={4} frameDuration={100} />).toString()
|
||||
expect(html).toContain("background-image:url('/warrior.png')")
|
||||
})
|
||||
|
||||
test("Sprite calculates background-size for horizontal strip", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} />).toString()
|
||||
expect(html).toContain('background-size:128px 32px')
|
||||
})
|
||||
|
||||
test("Sprite generates keyframes animation", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} />).toString()
|
||||
expect(html).toContain('@keyframes sprite-')
|
||||
expect(html).toContain('steps(4)')
|
||||
expect(html).toContain('400ms')
|
||||
})
|
||||
|
||||
test("Sprite keyframes animate background-position", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} />).toString()
|
||||
expect(html).toContain('from{background-position:0 0}')
|
||||
expect(html).toContain('to{background-position:-128px 0}')
|
||||
})
|
||||
|
||||
test("Sprite with columns calculates grid background-size", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={8} frameDuration={100} columns={4} />).toString()
|
||||
expect(html).toContain('background-size:128px 64px')
|
||||
})
|
||||
|
||||
test("Sprite with columns generates grid keyframes", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} columns={2} />).toString()
|
||||
expect(html).toContain('0%{background-position:0px 0px}')
|
||||
expect(html).toContain('25%{background-position:-32px 0px}')
|
||||
expect(html).toContain('50%{background-position:0px -32px}')
|
||||
expect(html).toContain('75%{background-position:-32px -32px}')
|
||||
})
|
||||
|
||||
test("Sprite passes through class", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} class="my-sprite" />).toString()
|
||||
expect(html).toContain('class="my-sprite"')
|
||||
})
|
||||
|
||||
test("Sprite passes through style", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} style="opacity:0.5" />).toString()
|
||||
expect(html).toContain('opacity:0.5')
|
||||
})
|
||||
|
||||
test("Sprite pauses when playing is false", () => {
|
||||
const html = (<Sprite src="/test.png" width={32} height={32} frames={4} frameDuration={100} playing={false} />).toString()
|
||||
expect(html).toContain('animation-play-state:paused')
|
||||
})
|
||||
70
src/sprite.tsx
Normal file
70
src/sprite.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
type SpriteProps = {
|
||||
src: string
|
||||
width: number
|
||||
height: number
|
||||
frames: number
|
||||
frameDuration: number
|
||||
columns?: number
|
||||
class?: string
|
||||
style?: string
|
||||
playing?: boolean
|
||||
}
|
||||
|
||||
export const Sprite = ({
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
frameDuration,
|
||||
columns,
|
||||
class: className,
|
||||
style,
|
||||
playing = true,
|
||||
}: SpriteProps) => {
|
||||
const isGrid = columns !== undefined
|
||||
const rows = isGrid ? Math.ceil(frames / columns!) : 1
|
||||
const cols = isGrid ? columns! : frames
|
||||
|
||||
const sheetWidth = cols * width
|
||||
const sheetHeight = rows * height
|
||||
const totalDuration = frames * frameDuration
|
||||
|
||||
const keyframeId = `sprite-${Bun.hash(`${src}-${width}-${height}-${frames}-${frameDuration}-${columns}`).toString(36)}`
|
||||
|
||||
const keyframes = isGrid
|
||||
? generateGridKeyframes(keyframeId, width, height, frames, columns!)
|
||||
: generateStripKeyframes(keyframeId, sheetWidth)
|
||||
|
||||
const divStyle = [
|
||||
`width:${width}px`,
|
||||
`height:${height}px`,
|
||||
`background-image:url('${src}')`,
|
||||
`background-size:${sheetWidth}px ${sheetHeight}px`,
|
||||
`animation:${keyframeId} ${totalDuration}ms steps(${frames}) infinite`,
|
||||
playing ? '' : 'animation-play-state:paused',
|
||||
style,
|
||||
].filter(Boolean).join(';')
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{keyframes}</style>
|
||||
<div class={className} style={divStyle} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const generateStripKeyframes = (id: string, sheetWidth: number): string =>
|
||||
`@keyframes ${id}{from{background-position:0 0}to{background-position:-${sheetWidth}px 0}}`
|
||||
|
||||
const generateGridKeyframes = (id: string, width: number, height: number, frames: number, columns: number): string => {
|
||||
const steps: string[] = []
|
||||
for (let i = 0; i < frames; i++) {
|
||||
const col = i % columns
|
||||
const row = Math.floor(i / columns)
|
||||
const x = -col * width
|
||||
const y = -row * height
|
||||
const percent = (i / frames) * 100
|
||||
steps.push(`${percent}%{background-position:${x}px ${y}px}`)
|
||||
}
|
||||
return `@keyframes ${id}{${steps.join('')}}`
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user