make the sprites only resize to what they need to be

This commit is contained in:
2026-01-20 21:51:45 -06:00
parent 6f59e34761
commit 95d821c376
2 changed files with 139 additions and 36 deletions

View File

@@ -321,9 +321,10 @@ const outlineColorPicker = document.getElementById('outline-color-picker');
} }
}); });
canvas.on('path:created', saveHistory); canvas.on('path:created', saveHistory);
canvas.on('object:added', (e) => { canvas.on('object:added', (e) => {
if (e.target && e.target.type !== 'path') { // Only save history for valid fabric objects
if (e.target && typeof e.target.getBounds === 'function' && e.target.type !== 'path') {
saveHistory(); saveHistory();
} }
}); });
@@ -374,41 +375,63 @@ const outlineColorPicker = document.getElementById('outline-color-picker');
// Trigger draw mode by default // Trigger draw mode by default
document.querySelector('[data-tool="draw"]').click(); document.querySelector('[data-tool="draw"]').click();
// Load existing costume if provided // Load existing costume if provided
if (existingCostume && existingCostume.texture) { if (existingCostume) {
const url = existingCostume.texture.baseTexture?.resource?.url || existingCostume.texture.baseTexture?.cacheId; // If we have editor data, restore the full canvas state
if (existingCostume.editorData) {
if (url) { canvas.loadFromJSON(existingCostume.editorData).then(() => {
FabricImage.fromURL(url).then((img) => {
img.set({
left: 240,
top: 180,
originX: 'center',
originY: 'center'
});
const scale = Math.min(460 / img.width, 340 / img.height, 1);
img.scale(scale);
canvas.add(img);
canvas.renderAll(); canvas.renderAll();
saveHistory(); saveHistory();
}).catch(err => { }).catch(err => {
console.error('Failed to load costume:', err); console.error('Failed to load editor data:', err);
saveHistory();
}); });
} }
// Otherwise, load from texture image (legacy/first edit)
else if (existingCostume.texture) {
const url = existingCostume.texture.baseTexture?.resource?.url || existingCostume.texture.baseTexture?.cacheId;
if (url) {
FabricImage.fromURL(url).then((img) => {
img.set({
left: 240,
top: 180,
originX: 'center',
originY: 'center'
});
const scale = Math.min(460 / img.width, 340 / img.height, 1);
img.scale(scale);
canvas.add(img);
canvas.renderAll();
saveHistory();
}).catch(err => {
console.error('Failed to load costume:', err);
saveHistory();
});
} else {
saveHistory();
}
} else {
saveHistory();
}
} else { } else {
// Save initial empty state // Save initial empty state
saveHistory(); saveHistory();
} }
document.querySelector('.save-btn').addEventListener('click', () => { document.querySelector('.save-btn').addEventListener('click', async () => {
const dataURL = canvas.toDataURL({ // Save both cropped version (for display) and full canvas data (for re-editing)
format: 'png', const croppedDataURL = await autoCropCanvas(canvas);
quality: 1, const fullCanvasJSON = canvas.toJSON();
multiplier: 1
}); if (onSave) {
if (onSave) onSave(dataURL); onSave({
dataURL: croppedDataURL,
editorData: fullCanvasJSON
});
}
closeCostumeEditor(); closeCostumeEditor();
}); });
@@ -416,6 +439,80 @@ const outlineColorPicker = document.getElementById('outline-color-picker');
document.querySelector('.close-editor-btn').addEventListener('click', closeCostumeEditor); document.querySelector('.close-editor-btn').addEventListener('click', closeCostumeEditor);
} }
function autoCropCanvas(canvas) {
return new Promise((resolve) => {
// First, get the full canvas as a data URL
const fullDataURL = canvas.toDataURL({ format: 'png', quality: 1 });
// Create an image from it
const img = new Image();
img.onload = () => {
// Create a temporary canvas to analyze pixels
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const ctx = tempCanvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
let minX = canvas.width, minY = canvas.height, maxX = 0, maxY = 0;
let hasContent = false;
// Scan for non-transparent pixels
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const alpha = pixels[(y * canvas.width + x) * 4 + 3];
if (alpha > 0) {
hasContent = true;
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
}
// If no content, return small empty canvas
if (!hasContent) {
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 100;
emptyCanvas.height = 100;
resolve(emptyCanvas.toDataURL('image/png'));
return;
}
// Add padding
const padding = 10;
minX = Math.max(0, minX - padding);
minY = Math.max(0, minY - padding);
maxX = Math.min(canvas.width - 1, maxX + padding);
maxY = Math.min(canvas.height - 1, maxY + padding);
const cropWidth = maxX - minX + 1;
const cropHeight = maxY - minY + 1;
// Create final cropped canvas
const croppedCanvas = document.createElement('canvas');
croppedCanvas.width = cropWidth;
croppedCanvas.height = cropHeight;
const croppedCtx = croppedCanvas.getContext('2d');
// Draw the cropped portion
croppedCtx.drawImage(
img,
minX, minY, cropWidth, cropHeight,
0, 0, cropWidth, cropHeight
);
resolve(croppedCanvas.toDataURL('image/png'));
};
img.src = fullDataURL;
});
}
export function closeCostumeEditor() { export function closeCostumeEditor() {
if (editorContainer) { if (editorContainer) {
editorContainer.remove(); editorContainer.remove();

View File

@@ -595,12 +595,14 @@ function renderCostumesList() {
editBtn.draggable = false; editBtn.draggable = false;
editBtn.title = "Edit costume"; editBtn.title = "Edit costume";
editBtn.onclick = () => { editBtn.onclick = () => {
openCostumeEditor(costume, async (dataURL) => { openCostumeEditor(costume, async (costumeData) => {
if (!dataURL) return; if (!costumeData) return;
// Update the existing costume // Update the existing costume
const newTexture = PIXI.Texture.from(dataURL); const newTexture = PIXI.Texture.from(costumeData.dataURL);
newTexture.editorData = costumeData.editorData;
costume.texture = newTexture; costume.texture = newTexture;
costume.editorData = costumeData.editorData;
// Update sprite if this is the current costume // Update sprite if this is the current costume
if (activeSprite.pixiSprite.texture === costume.texture) { if (activeSprite.pixiSprite.texture === costume.texture) {
@@ -613,11 +615,11 @@ function renderCostumesList() {
if (currentSocket && currentRoom) { if (currentSocket && currentRoom) {
currentSocket.emit("projectUpdate", { currentSocket.emit("projectUpdate", {
roomId: currentRoom, roomId: currentRoom,
type: "updateCostume", type: "addCostume",
data: { data: {
spriteId: activeSprite.id, spriteId: activeSprite.id,
name: costume.name, name: uniqueName,
texture: dataURL, texture: costumeData.dataURL,
}, },
}); });
} }
@@ -1825,6 +1827,7 @@ loadButton.addEventListener("click", () => {
}); });
loadInput.addEventListener("change", loadProject); loadInput.addEventListener("change", loadProject);
// Create new costume with editor
// Create new costume with editor // Create new costume with editor
document.getElementById('create-costume-button')?.addEventListener('click', () => { document.getElementById('create-costume-button')?.addEventListener('click', () => {
if (!activeSprite) { if (!activeSprite) {
@@ -1832,10 +1835,13 @@ document.getElementById('create-costume-button')?.addEventListener('click', () =
return; return;
} }
openCostumeEditor(null, async (dataURL) => { openCostumeEditor(null, async (costumeData) => {
if (!dataURL || !activeSprite) return; if (!costumeData || !activeSprite) return;
const texture = PIXI.Texture.from(dataURL); const texture = PIXI.Texture.from(costumeData.dataURL);
// Store the editorData along with the texture for future editing
texture.editorData = costumeData.editorData;
let uniqueName = 'costume'; let uniqueName = 'costume';
let counter = 1; let counter = 1;