1
0
forked from noxious/server

Better sprite-sheet generating

This commit is contained in:
Dennis Postma 2024-12-20 01:28:36 +01:00
parent 1cbf116ad4
commit 43fe6ab33e
2 changed files with 75 additions and 17 deletions

12
package-lock.json generated
View File

@ -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"

View File

@ -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()