Finalised spritesheet generator, updated init command for correct offset values

This commit is contained in:
2025-01-31 02:17:55 +01:00
parent c400e868af
commit 60753cb2db
2 changed files with 98 additions and 99 deletions

View File

@ -13,6 +13,20 @@ interface SpriteImage {
}
}
interface ImageDimensions {
width: number
height: number
offsetX: number
offsetY: number
}
interface EffectiveDimensions {
width: number
height: number
top: number
bottom: number
}
type Payload = {
id: UUID
name: string
@ -59,6 +73,16 @@ export default class SpriteUpdateEvent extends BaseEvent {
// Create new actions
for (const actionData of data.spriteActions) {
// Process images and calculate dimensions
const imageData = await Promise.all(actionData.sprites.map(sprite => this.processImage(sprite)))
const effectiveDimensions = imageData.map(dimensions => this.calculateEffectiveDimensions(dimensions))
// Calculate total height needed for the sprite sheet
const maxHeight = Math.max(...effectiveDimensions.map(d => d.height))
const maxTop = Math.max(...effectiveDimensions.map(d => d.top))
const maxBottom = Math.max(...effectiveDimensions.map(d => d.bottom))
const totalHeight = maxHeight + maxTop + maxBottom
const spriteAction = new SpriteAction()
spriteAction.setSprite(sprite)
sprite.getSpriteActions().add(spriteAction)
@ -69,7 +93,7 @@ export default class SpriteUpdateEvent extends BaseEvent {
.setOriginX(actionData.originX)
.setOriginY(actionData.originY)
.setFrameWidth(await this.calculateMaxWidth(actionData.sprites))
.setFrameHeight(await this.calculateMaxHeight(actionData.sprites))
.setFrameHeight(totalHeight)
.setFrameRate(actionData.frameRate)
await spriteRepository.getEntityManager().persistAndFlush(spriteAction)
@ -82,87 +106,35 @@ export default class SpriteUpdateEvent extends BaseEvent {
}
}
private async calculateMaxWidth(sprites: SpriteImage[]): Promise<number> {
if (!sprites.length) return 0
const widths = await Promise.all(
sprites.map(async (base64) => {
const uri = base64.url.split(';base64,').pop()
if (!uri) return 0
const imgBuffer = Buffer.from(uri, 'base64')
const image = await sharp(imgBuffer).metadata()
return image.width ?? 0
})
)
return Math.max(...widths)
}
private async calculateMaxHeight(sprites: SpriteImage[]): Promise<number> {
if (!sprites.length) return 0
const heights = await Promise.all(
sprites.map(async (base64) => {
const uri = base64.url.split(';base64,').pop()
if (!uri) return 0
const imgBuffer = Buffer.from(uri, 'base64')
const image = await sharp(imgBuffer).metadata()
return image.height ?? 0
})
)
return Math.max(...heights)
}
private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise<boolean> {
try {
// Skip if no sprites
if (!sprites.length) return true
// Process all base64 images to buffers and get their dimensions
const imageData = await Promise.all(
sprites.map(async (sprite) => {
const uri = sprite.url.split(';base64,').pop()
if (!uri) throw new Error('Invalid base64 image')
const buffer = Buffer.from(uri, 'base64')
const metadata = await sharp(buffer).metadata()
return {
buffer,
width: metadata.width ?? 0,
height: metadata.height ?? 0,
offsetX: sprite.offset?.x ?? 0,
offsetY: sprite.offset?.y ?? 0
}
})
)
// Process all images and get their dimensions
const imageData = await Promise.all(sprites.map(sprite => this.processImage(sprite)))
const effectiveDimensions = imageData.map(dimensions => this.calculateEffectiveDimensions(dimensions))
// Find the largest dimensions including offsets
const effectiveDimensions = imageData.map(({ width, height, offsetX, offsetY }) => ({
width: width + Math.abs(offsetX),
height: height + Math.abs(offsetY),
top: offsetY >= 0 ? offsetY : 0,
bottom: offsetY < 0 ? Math.abs(offsetY) : 0
}))
// Calculate maximum dimensions
const maxWidth = Math.max(...effectiveDimensions.map(d => d.width))
const maxHeight = Math.max(...effectiveDimensions.map(d => d.height))
const maxTop = Math.max(...effectiveDimensions.map(d => d.top))
const maxBottom = Math.max(...effectiveDimensions.map(d => d.bottom))
// Calculate total height needed to accommodate all sprites with their offsets
// Calculate total height needed
const totalHeight = maxHeight + maxTop + maxBottom
// Extend all images to match the largest dimensions without resizing
const resizedBuffers = await Promise.all(
imageData.map(async ({ buffer, width, height, offsetX, offsetY }) => {
// Calculate the position of the image within the frame
const left = offsetX >= 0 ? offsetX : 0
// Position from bottom edge while respecting offsetY
const verticalOffset = totalHeight - height - (offsetY >= 0 ? offsetY : 0)
// Process images and create sprite sheet
const processedImages = await Promise.all(
sprites.map(async (sprite, index) => {
const { width, height, offsetX, offsetY } = await this.processImage(sprite)
const uri = sprite.url.split(';base64,').pop()
if (!uri) throw new Error('Invalid base64 image')
const buffer = Buffer.from(uri, 'base64')
// Create a blank canvas of the maximum size
return await sharp({
// Create individual frame
const left = offsetX >= 0 ? offsetX : 0
const verticalOffset = totalHeight - height - (offsetY >= 0 ? offsetY : 0)
return sharp({
create: {
width: maxWidth,
height: totalHeight,
@ -170,19 +142,13 @@ export default class SpriteUpdateEvent extends BaseEvent {
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite([
{
input: buffer,
left: left,
top: verticalOffset
}
])
.composite([{ input: buffer, left, top: verticalOffset }])
.png()
.toBuffer()
})
)
// Create sprite sheet by combining images horizontally
// Combine frames into sprite sheet
const spriteSheet = await sharp({
create: {
width: maxWidth * sprites.length,
@ -191,15 +157,15 @@ export default class SpriteUpdateEvent extends BaseEvent {
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite(
resizedBuffers.map((buffer, index) => ({
input: buffer,
left: index * maxWidth,
top: 0
}))
)
.png()
.toBuffer()
.composite(
processedImages.map((buffer, index) => ({
input: buffer,
left: index * maxWidth,
top: 0
}))
)
.png()
.toBuffer()
// Ensure directory exists
const dir = `public/sprites/${spriteId}`
@ -214,4 +180,37 @@ export default class SpriteUpdateEvent extends BaseEvent {
return false
}
}
private async processImage(sprite: SpriteImage): Promise<ImageDimensions> {
const uri = sprite.url.split(';base64,').pop()
if (!uri) throw new Error('Invalid base64 image')
const buffer = Buffer.from(uri, 'base64')
const metadata = await sharp(buffer).metadata()
return {
width: metadata.width ?? 0,
height: metadata.height ?? 0,
offsetX: sprite.offset?.x ?? 0,
offsetY: sprite.offset?.y ?? 0
}
}
private calculateEffectiveDimensions(imageDimensions: ImageDimensions): EffectiveDimensions {
return {
width: imageDimensions.width + Math.abs(imageDimensions.offsetX),
height: imageDimensions.height + Math.abs(imageDimensions.offsetY),
top: imageDimensions.offsetY >= 0 ? imageDimensions.offsetY : 0,
bottom: imageDimensions.offsetY < 0 ? Math.abs(imageDimensions.offsetY) : 0
}
}
private async calculateMaxWidth(sprites: SpriteImage[]): Promise<number> {
if (!sprites.length) return 0
// Process all images and get their dimensions
const imageData = await Promise.all(sprites.map(sprite => this.processImage(sprite)))
const effectiveDimensions = imageData.map(dimensions => this.calculateEffectiveDimensions(dimensions))
// Calculate maximum width needed
return Math.max(...effectiveDimensions.map(d => d.width))
}
}