From 9d7cee23343593145e88e9c59ece189bca0b6120 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Tue, 28 Jan 2025 15:34:21 +0100 Subject: [PATCH] Combining sprites to generate a spritesheet works again. --- .../gameMaster/assetManager/sprite/update.ts | 120 +++++++++++++++--- 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index bdd036e..09f00e9 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -3,6 +3,7 @@ import { UUID } from '#application/types' import SpriteRepository from '#repositories/spriteRepository' import { SpriteAction } from '#entities/spriteAction' import sharp from 'sharp' +import fs from 'fs' type Payload = { id: UUID @@ -52,11 +53,14 @@ export default class SpriteUpdateEvent extends BaseEvent { .setSprites(actionData.sprites) .setOriginX(actionData.originX) .setOriginY(actionData.originY) - .setFrameWidth(await this.calculateWidth(actionData.sprites[0])) - .setFrameHeight(await this.calculateHeight(actionData.sprites[0])) + .setFrameWidth(await this.calculateMaxWidth(actionData.sprites)) + .setFrameHeight(await this.calculateMaxHeight(actionData.sprites)) .setFrameRate(actionData.frameRate) await spriteRepository.getEntityManager().persistAndFlush(spriteAction) + + // Generate sprite sheet for this action + await this.generateSpriteSheet(actionData.sprites, sprite.getId(), actionData.action) } return callback(true) @@ -66,27 +70,103 @@ export default class SpriteUpdateEvent extends BaseEvent { } } - private async calculateWidth(base64: string): Promise { - const uri = base64.split(';base64,').pop() - if (!uri) return 0 - - const imgBuffer = Buffer.from(uri, 'base64') - const image = await sharp(imgBuffer).metadata() - return image.width ?? 0 + private async calculateMaxWidth(sprites: string[]): Promise { + if (!sprites.length) return 0 + + const widths = await Promise.all(sprites.map(async (base64) => { + const uri = base64.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 calculateHeight(base64: string): Promise { - const uri = base64.split(';base64,').pop() - if (!uri) return 0 - - const imgBuffer = Buffer.from(uri, 'base64') - const image = await sharp(imgBuffer).metadata() - return image.height ?? 0 + private async calculateMaxHeight(sprites: string[]): Promise { + if (!sprites.length) return 0 + + const heights = await Promise.all(sprites.map(async (base64) => { + const uri = base64.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 generateSpriteSheet(sprites: string[]) { - // In here comes a function that generates a sprite sheet from the given sprites using Sharp. - // This function takes an array of base64 encoded sprites and generates a single png sprite sheet. - // Then proceeds to save ths sprite sheet to the public/sprites/{spriteId}/ directory. The file name is {action}.png. + private async generateSpriteSheet(sprites: string[], spriteId: string, action: string): Promise { + try { + // Skip if no sprites + if (!sprites.length) return + + // Process all base64 images to buffers and get their dimensions + const imageData = await Promise.all( + sprites.map(async (base64) => { + const uri = base64.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 } + }) + ) + + // Find the largest dimensions + const maxWidth = Math.max(...imageData.map(data => data.width)) + const maxHeight = Math.max(...imageData.map(data => data.height)) + + // Extend all images to match the largest dimensions without resizing + const resizedBuffers = await Promise.all( + imageData.map(async ({ buffer, width, height }) => { + // Calculate padding to center the sprite + const leftPadding = Math.floor((maxWidth - width) / 2) + const topPadding = Math.floor((maxHeight - height) / 2) + + return await sharp(buffer) + .extend({ + top: topPadding, + bottom: Math.ceil((maxHeight - height) / 2), + left: leftPadding, + right: Math.ceil((maxWidth - width) / 2), + background: { r: 0, g: 0, b: 0, alpha: 0 } + }) + .toBuffer() + }) + ) + + // Create sprite sheet by combining images horizontally + const spriteSheet = await sharp({ + create: { + width: maxWidth * sprites.length, + height: maxHeight, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 0 } + } + }) + .composite( + resizedBuffers.map((buffer, index) => ({ + input: buffer, + left: index * maxWidth, + top: 0 + })) + ) + .png() + .toBuffer() + + // Ensure directory exists + const dir = `public/sprites/${spriteId}` + await fs.promises.mkdir(dir, { recursive: true }) + + // Save the sprite sheet + await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet) + } catch (error) { + console.error('Error generating sprite sheet:', error) + throw error + } } }