From 43fe6ab33e419b4e420b34a843865ffd7e2d8e4a Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Fri, 20 Dec 2024 01:28:36 +0100 Subject: [PATCH] Better sprite-sheet generating --- package-lock.json | 12 +-- .../gameMaster/assetManager/sprite/update.ts | 80 ++++++++++++++++--- 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4bed3f5..7696131 100644 --- a/package-lock.json +++ b/package-lock.json @@ -949,9 +949,9 @@ "license": "BSD-3-Clause" }, "node_modules/bullmq": { - "version": "5.34.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.2.tgz", - "integrity": "sha512-eUzeCswrKbQDE1WY8h4ZTBtynOzCU5qx9felFdYOmIJrsy0warDahHKUiCZ6dUCs6ZxYMGtcaciIMcAf1L54yw==", + "version": "5.34.3", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.3.tgz", + "integrity": "sha512-S8/V11w7p6jYAGvv+00skLza/4inTOupWPe0uCD8mZSUiYKzvmW4/YEB+KVjZI2CC2oD3KJ3t7/KkUd31MxMig==", "license": "MIT", "dependencies": { "cron-parser": "^4.6.0", @@ -1843,9 +1843,9 @@ "license": "ISC" }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/src/socketEvents/gameMaster/assetManager/sprite/update.ts b/src/socketEvents/gameMaster/assetManager/sprite/update.ts index a45a85e..c8d1b7a 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/update.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/update.ts @@ -82,8 +82,9 @@ export default class SpriteUpdateEvent { const buffersWithDimensions = await Promise.all( sprites.map(async (sprite: string) => { const buffer = Buffer.from(sprite.split(',')[1], 'base64') - const { width, height } = await sharp(buffer).metadata() - return { buffer, width, height } + const normalizedBuffer = await normalizeSprite(buffer) + const { width, height } = await sharp(normalizedBuffer).metadata() + return { buffer: normalizedBuffer, width, height } }) ) @@ -100,24 +101,70 @@ export default class SpriteUpdateEvent { ) } + async function normalizeSprite(buffer: Buffer): Promise { + const image = sharp(buffer) + + // Remove any transparent edges + const trimmed = await image + .trim() + .toBuffer() + + // Optional: Ensure dimensions are even numbers + const metadata = await sharp(trimmed).metadata() + const width = Math.ceil(metadata.width! / 2) * 2 + const height = Math.ceil(metadata.height! / 2) * 2 + + return sharp(trimmed) + .resize(width, height, { + fit: 'contain', + background: { r: 0, g: 0, b: 0, alpha: 0 } + }) + .toBuffer() + } + async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) { const publicFolder = getPublicPath('sprites', id) await mkdir(publicFolder, { recursive: true }) await Promise.all( processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => { - // Get and validate all frame dimensions first + // First pass: analyze all frames to determine the consistent dimensions const frames = await Promise.all( buffersWithDimensions.map(async ({ buffer }) => { + const image = sharp(buffer) + + // Get trim boundaries to find actual sprite content + const { info: trimData } = await image + .trim() + .toBuffer({ resolveWithObject: true }) + + // Get original metadata const metadata = await sharp(buffer).metadata() + return { buffer, width: metadata.width!, - height: metadata.height! + height: metadata.height!, + trimmed: { + width: trimData.width, + height: trimData.height, + left: metadata.width! - trimData.width, + top: metadata.height! - trimData.height + } } }) ) + // Calculate the average center point of all frames + const centerPoints = frames.map(frame => ({ + x: Math.floor(frame.trimmed.left + frame.trimmed.width / 2), + y: Math.floor(frame.trimmed.top + frame.trimmed.height / 2) + })) + + const avgCenterX = Math.round(centerPoints.reduce((sum, p) => sum + p.x, 0) / frames.length) + const avgCenterY = Math.round(centerPoints.reduce((sum, p) => sum + p.y, 0) / frames.length) + + // Create the combined image with precise alignment const combinedImage = await sharp({ create: { width: frameWidth * frames.length, @@ -127,13 +174,24 @@ export default class SpriteUpdateEvent { } }) .composite( - frames.map(({ buffer, width, height }, index) => ({ - input: buffer, - // Center horizontally based on the exact middle of each frame - left: index * frameWidth + ((frameWidth - width) >> 1), - // Top position is always 0 - top: 0 - })) + frames.map(({ buffer, width, height }, index) => { + // Calculate offset to maintain consistent center point + const frameCenterX = Math.floor(width / 2) + const frameCenterY = Math.floor(height / 2) + + const adjustedLeft = index * frameWidth + (frameWidth / 2) - frameCenterX + const adjustedTop = (frameHeight / 2) - frameCenterY + + // Round to nearest even number to prevent sub-pixel rendering + const left = Math.round(adjustedLeft / 2) * 2 + const top = Math.round(adjustedTop / 2) * 2 + + return { + input: buffer, + left, + top + } + }) ) .png() .toBuffer()