1
0
forked from noxious/server

Better sprite-sheet generating

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

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