From 43fe6ab33e419b4e420b34a843865ffd7e2d8e4a Mon Sep 17 00:00:00 2001
From: Dennis Postma <dennis@directonline.io>
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<Buffer> {
+      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()