Horror code
This commit is contained in:
parent
2ac9416fe6
commit
2be49c010f
@ -70,56 +70,395 @@ export default class SpriteUpdateEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function preprocessSprite(buffer: Buffer): Promise<Buffer> {
|
||||||
|
// Force the sprite to maintain its exact dimensions
|
||||||
|
return sharp(buffer)
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findContentBounds(buffer: Buffer) {
|
||||||
|
const { data, info } = await sharp(buffer)
|
||||||
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const width = info.width;
|
||||||
|
const height = info.height;
|
||||||
|
|
||||||
|
// Track pixels per column to find the true center of mass
|
||||||
|
const columnWeights = new Array(width).fill(0);
|
||||||
|
let firstContentY = height; // Track highest content point
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
const alpha = data[idx + 3];
|
||||||
|
if (alpha > 0) {
|
||||||
|
columnWeights[x]++;
|
||||||
|
firstContentY = Math.min(firstContentY, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the weighted center
|
||||||
|
let totalWeight = 0;
|
||||||
|
let weightedSum = 0;
|
||||||
|
columnWeights.forEach((weight, x) => {
|
||||||
|
if (weight > 0) {
|
||||||
|
totalWeight += weight;
|
||||||
|
weightedSum += x * weight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerOfMass = Math.round(weightedSum / totalWeight);
|
||||||
|
|
||||||
|
return {
|
||||||
|
centerX: centerOfMass,
|
||||||
|
topY: firstContentY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzePixelDistribution(buffer: Buffer) {
|
||||||
|
const { data, info } = await sharp(buffer)
|
||||||
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const width = info.width;
|
||||||
|
const height = info.height;
|
||||||
|
|
||||||
|
// Track solid pixels in columns and rows
|
||||||
|
const columns = new Array(width).fill(0);
|
||||||
|
const solidPixelsPerRow = new Array(height).fill(0);
|
||||||
|
let firstSolidPixelY = height;
|
||||||
|
|
||||||
|
// Find the most dense vertical line of pixels
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
const alpha = data[idx + 3];
|
||||||
|
if (alpha > 0) {
|
||||||
|
columns[x]++;
|
||||||
|
solidPixelsPerRow[y]++;
|
||||||
|
firstSolidPixelY = Math.min(firstSolidPixelY, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the strongest vertical line (most likely the center of the character)
|
||||||
|
let maxDensity = 0;
|
||||||
|
let centralLine = Math.floor(width / 2);
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
if (columns[x] > maxDensity) {
|
||||||
|
maxDensity = columns[x];
|
||||||
|
centralLine = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
centralLine,
|
||||||
|
firstContentY: firstSolidPixelY,
|
||||||
|
densityMap: columns
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function processSprites(spriteActions: SpriteActionInput[]): Promise<ProcessedSpriteAction[]> {
|
async function processSprites(spriteActions: SpriteActionInput[]): Promise<ProcessedSpriteAction[]> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
spriteActions.map(async (spriteAction) => {
|
spriteActions.map(async (spriteAction) => {
|
||||||
const { action, sprites } = spriteAction
|
const { action, sprites } = spriteAction;
|
||||||
|
|
||||||
if (!Array.isArray(sprites) || sprites.length === 0) {
|
// First get all dimensions
|
||||||
gameMasterLogger.error(`Invalid sprites array for action: ${action}`)
|
const spriteBuffers = await Promise.all(
|
||||||
}
|
|
||||||
|
|
||||||
const buffersWithDimensions = await Promise.all(
|
|
||||||
sprites.map(async (sprite: string) => {
|
sprites.map(async (sprite: string) => {
|
||||||
const buffer = Buffer.from(sprite.split(',')[1], 'base64')
|
const buffer = Buffer.from(sprite.split(',')[1], 'base64');
|
||||||
const normalizedBuffer = await normalizeSprite(buffer)
|
const metadata = await sharp(buffer).metadata();
|
||||||
const { width, height } = await sharp(normalizedBuffer).metadata()
|
return { buffer, width: metadata.width!, height: metadata.height! };
|
||||||
return { buffer: normalizedBuffer, width, height }
|
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
const frameWidth = Math.max(...buffersWithDimensions.map((b) => b.width || 0))
|
// Calculate frame size
|
||||||
const frameHeight = Math.max(...buffersWithDimensions.map((b) => b.height || 0))
|
const frameWidth = Math.ceil(Math.max(...spriteBuffers.map(s => s.width)) / 2) * 2;
|
||||||
|
const frameHeight = Math.ceil(Math.max(...spriteBuffers.map(s => s.height)) / 2) * 2;
|
||||||
|
|
||||||
|
// Use first frame as reference and center all frames exactly the same way
|
||||||
|
const centerOffset = Math.floor(frameWidth / 2);
|
||||||
|
|
||||||
|
// Process all sprites with exact same centering
|
||||||
|
const buffersWithDimensions = await Promise.all(
|
||||||
|
spriteBuffers.map(async ({ buffer }) => {
|
||||||
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
const leftPadding = centerOffset - Math.floor(metadata.width! / 2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: await sharp({
|
||||||
|
create: {
|
||||||
|
width: frameWidth,
|
||||||
|
height: frameHeight,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.composite([
|
||||||
|
{
|
||||||
|
input: buffer,
|
||||||
|
left: leftPadding,
|
||||||
|
top: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.png()
|
||||||
|
.toBuffer(),
|
||||||
|
width: frameWidth,
|
||||||
|
height: frameHeight
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...spriteAction,
|
...spriteAction,
|
||||||
frameWidth,
|
frameWidth,
|
||||||
frameHeight,
|
frameHeight,
|
||||||
buffersWithDimensions
|
buffersWithDimensions
|
||||||
}
|
};
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function normalizeSprite(buffer: Buffer): Promise<Buffer> {
|
// Add these utility functions at the top
|
||||||
const image = sharp(buffer)
|
interface BaselineResult {
|
||||||
|
baselineY: number;
|
||||||
|
contentHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any transparent edges
|
async function detectBaseline(buffer: Buffer): Promise<BaselineResult> {
|
||||||
const trimmed = await image
|
const image = sharp(buffer);
|
||||||
.trim()
|
const metadata = await image.metadata();
|
||||||
.toBuffer()
|
const { data, info } = await image
|
||||||
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
// Optional: Ensure dimensions are even numbers
|
const height = metadata.height!;
|
||||||
const metadata = await sharp(trimmed).metadata()
|
const width = metadata.width!;
|
||||||
const width = Math.ceil(metadata.width! / 2) * 2
|
|
||||||
const height = Math.ceil(metadata.height! / 2) * 2
|
|
||||||
|
|
||||||
return sharp(trimmed)
|
// Scan from bottom to top to find the lowest non-transparent pixel
|
||||||
.resize(width, height, {
|
let baselineY = 0;
|
||||||
fit: 'contain',
|
let topY = height;
|
||||||
|
|
||||||
|
for (let y = height - 1; y >= 0; y--) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
const alpha = data[idx + 3];
|
||||||
|
if (alpha > 0) {
|
||||||
|
baselineY = Math.max(baselineY, y);
|
||||||
|
topY = Math.min(topY, y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baselineY,
|
||||||
|
contentHeight: baselineY - topY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the calculateOptimalFrameSize function
|
||||||
|
async function calculateOptimalFrameSize(buffers: Array<{ buffer: Buffer }>) {
|
||||||
|
const roundToEven = (n: number) => Math.ceil(n / 2) * 2;
|
||||||
|
|
||||||
|
// Analyze all frames
|
||||||
|
const analyses = await Promise.all(
|
||||||
|
buffers.map(async ({ buffer }) => {
|
||||||
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
const baseline = await detectBaseline(buffer);
|
||||||
|
return {
|
||||||
|
width: metadata.width!,
|
||||||
|
height: metadata.height!,
|
||||||
|
baseline: baseline.baselineY,
|
||||||
|
contentHeight: baseline.contentHeight
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate optimal dimensions
|
||||||
|
const maxWidth = roundToEven(Math.max(...analyses.map(a => a.width)));
|
||||||
|
const maxHeight = roundToEven(Math.max(...analyses.map(a => a.height)));
|
||||||
|
|
||||||
|
// Calculate consistent baseline
|
||||||
|
const maxBaseline = Math.max(...analyses.map(a => a.baseline));
|
||||||
|
const maxContentHeight = Math.max(...analyses.map(a => a.contentHeight));
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
baselineY: maxBaseline,
|
||||||
|
contentHeight: maxContentHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findSpriteCenter(buffer: Buffer): Promise<number> {
|
||||||
|
const { data, info } = await sharp(buffer)
|
||||||
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const width = info.width;
|
||||||
|
const height = info.height;
|
||||||
|
|
||||||
|
// For isometric sprites, focus on the upper body area (30-60%)
|
||||||
|
// This helps with more consistent centering especially for downward poses
|
||||||
|
const startY = Math.floor(height * 0.3);
|
||||||
|
const endY = Math.floor(height * 0.6);
|
||||||
|
|
||||||
|
let leftMost = width;
|
||||||
|
let rightMost = 0;
|
||||||
|
let pixelCount = 0;
|
||||||
|
let weightedSum = 0;
|
||||||
|
|
||||||
|
// Scan the critical area for solid pixels
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
const alpha = data[idx + 3];
|
||||||
|
if (alpha > 0) {
|
||||||
|
leftMost = Math.min(leftMost, x);
|
||||||
|
rightMost = Math.max(rightMost, x);
|
||||||
|
pixelCount++;
|
||||||
|
weightedSum += x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a combination of mass center and geometric center
|
||||||
|
const massCenterX = Math.round(weightedSum / pixelCount);
|
||||||
|
const geometricCenterX = Math.round((leftMost + rightMost) / 2);
|
||||||
|
|
||||||
|
// Weighted average favoring the mass center
|
||||||
|
return Math.round((massCenterX * 0.7 + geometricCenterX * 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findIsometricCenter(buffer: Buffer): Promise<{ centerX: number; verticalCenterLine: number }> {
|
||||||
|
const { data, info } = await sharp(buffer)
|
||||||
|
.raw()
|
||||||
|
.ensureAlpha()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const width = info.width;
|
||||||
|
const height = info.height;
|
||||||
|
|
||||||
|
// Track solid pixels in vertical slices
|
||||||
|
const verticalSlices = new Array(width).fill(0);
|
||||||
|
|
||||||
|
// For isometric chars, focus more on upper body (25-65% of height)
|
||||||
|
// This helps with more stable centering especially for downward-facing poses
|
||||||
|
const startY = Math.floor(height * 0.25);
|
||||||
|
const endY = Math.floor(height * 0.65);
|
||||||
|
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
const alpha = data[idx + 3];
|
||||||
|
if (alpha > 0) {
|
||||||
|
verticalSlices[x]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most dense vertical line (likely character's center mass)
|
||||||
|
let maxDensity = 0;
|
||||||
|
let verticalCenterLine = Math.floor(width / 2);
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
if (verticalSlices[x] > maxDensity) {
|
||||||
|
maxDensity = verticalSlices[x];
|
||||||
|
verticalCenterLine = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the geometric center of the actual content
|
||||||
|
let leftMost = width;
|
||||||
|
let rightMost = 0;
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
if (verticalSlices[x] > 0) {
|
||||||
|
leftMost = Math.min(leftMost, x);
|
||||||
|
rightMost = Math.max(rightMost, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a weighted combination of density center and geometric center
|
||||||
|
const geometricCenter = Math.round((leftMost + rightMost) / 2);
|
||||||
|
const centerX = Math.round((verticalCenterLine * 0.7 + geometricCenter * 0.3));
|
||||||
|
|
||||||
|
return { centerX, verticalCenterLine };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function normalizeIsometricSprite(
|
||||||
|
buffer: Buffer,
|
||||||
|
frameWidth: number,
|
||||||
|
frameHeight: number,
|
||||||
|
targetCenterX: number,
|
||||||
|
isDownwardFacing: boolean = false
|
||||||
|
): Promise<Buffer> {
|
||||||
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
const { centerX, verticalCenterLine } = await findIsometricCenter(buffer);
|
||||||
|
|
||||||
|
// Calculate offset with isometric correction
|
||||||
|
let offset = targetCenterX - centerX;
|
||||||
|
if (isDownwardFacing) {
|
||||||
|
offset = Math.round(offset + (centerX - verticalCenterLine) * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we don't exceed frame dimensions
|
||||||
|
const leftPadding = Math.max(0, offset);
|
||||||
|
const rightPadding = Math.max(0, frameWidth - metadata.width! - offset);
|
||||||
|
|
||||||
|
// First ensure the image fits within frame dimensions
|
||||||
|
const resizedBuffer = await sharp(buffer)
|
||||||
|
.resize(Math.min(metadata.width!, frameWidth), Math.min(metadata.height!, frameHeight), {
|
||||||
|
fit: 'inside',
|
||||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
})
|
})
|
||||||
.toBuffer()
|
.toBuffer();
|
||||||
|
|
||||||
|
// Then apply padding
|
||||||
|
return sharp(resizedBuffer)
|
||||||
|
.extend({
|
||||||
|
top: 0,
|
||||||
|
bottom: frameHeight - (await sharp(resizedBuffer).metadata()).height!,
|
||||||
|
left: leftPadding,
|
||||||
|
right: rightPadding,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified normalizeSprite function
|
||||||
|
async function normalizeSprite(buffer: Buffer, targetWidth: number, targetHeight: number): Promise<Buffer> {
|
||||||
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
const currentWidth = metadata.width!;
|
||||||
|
const currentHeight = metadata.height!;
|
||||||
|
|
||||||
|
// Calculate padding to perfectly center the sprite
|
||||||
|
const leftPadding = Math.floor((targetWidth - currentWidth) / 2);
|
||||||
|
|
||||||
|
return sharp(buffer)
|
||||||
|
.resize(currentWidth, currentHeight, {
|
||||||
|
fit: 'fill', // Force exact size without any automatic scaling
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
top: 0,
|
||||||
|
bottom: targetHeight - currentHeight,
|
||||||
|
left: leftPadding,
|
||||||
|
right: targetWidth - currentWidth - leftPadding,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) {
|
async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) {
|
||||||
@ -128,43 +467,17 @@ export default class SpriteUpdateEvent {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => {
|
processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => {
|
||||||
// First pass: analyze all frames to determine the consistent dimensions
|
|
||||||
const frames = await Promise.all(
|
const frames = await Promise.all(
|
||||||
buffersWithDimensions.map(async ({ buffer }) => {
|
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()
|
const metadata = await sharp(buffer).metadata()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buffer,
|
buffer,
|
||||||
width: metadata.width!,
|
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({
|
const combinedImage = await sharp({
|
||||||
create: {
|
create: {
|
||||||
width: frameWidth * frames.length,
|
width: frameWidth * frames.length,
|
||||||
@ -174,27 +487,14 @@ export default class SpriteUpdateEvent {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.composite(
|
.composite(
|
||||||
frames.map(({ buffer, width, height }, index) => {
|
frames.map(({ buffer }, index) => ({
|
||||||
// Calculate offset to maintain consistent center point
|
input: buffer,
|
||||||
const frameCenterX = Math.floor(width / 2)
|
left: index * frameWidth, // Remove centering calc since sprites are already centered
|
||||||
const frameCenterY = Math.floor(height / 2)
|
top: 0
|
||||||
|
}))
|
||||||
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 = 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
input: buffer,
|
|
||||||
left,
|
|
||||||
top
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
.png()
|
.png()
|
||||||
.toBuffer()
|
.toBuffer();
|
||||||
|
|
||||||
const filename = getPublicPath('sprites', id, `${action}.png`)
|
const filename = getPublicPath('sprites', id, `${action}.png`)
|
||||||
await writeFile(filename, combinedImage)
|
await writeFile(filename, combinedImage)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user