make the sprites only resize to what they need to be
This commit is contained in:
@@ -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) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -374,8 +375,20 @@ const outlineColorPicker = document.getElementById('outline-color-picker');
|
||||
// Trigger draw mode by default
|
||||
document.querySelector('[data-tool="draw"]').click();
|
||||
|
||||
// Load existing costume if provided
|
||||
if (existingCostume && existingCostume.texture) {
|
||||
// Load existing costume if provided
|
||||
if (existingCostume) {
|
||||
// If we have editor data, restore the full canvas state
|
||||
if (existingCostume.editorData) {
|
||||
canvas.loadFromJSON(existingCostume.editorData).then(() => {
|
||||
canvas.renderAll();
|
||||
saveHistory();
|
||||
}).catch(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) {
|
||||
@@ -395,20 +408,30 @@ const outlineColorPicker = document.getElementById('outline-color-picker');
|
||||
saveHistory();
|
||||
}).catch(err => {
|
||||
console.error('Failed to load costume:', err);
|
||||
saveHistory();
|
||||
});
|
||||
} else {
|
||||
saveHistory();
|
||||
}
|
||||
} else {
|
||||
saveHistory();
|
||||
}
|
||||
} else {
|
||||
// Save initial empty state
|
||||
saveHistory();
|
||||
}
|
||||
|
||||
document.querySelector('.save-btn').addEventListener('click', () => {
|
||||
const dataURL = canvas.toDataURL({
|
||||
format: 'png',
|
||||
quality: 1,
|
||||
multiplier: 1
|
||||
document.querySelector('.save-btn').addEventListener('click', async () => {
|
||||
// Save both cropped version (for display) and full canvas data (for re-editing)
|
||||
const croppedDataURL = await autoCropCanvas(canvas);
|
||||
const fullCanvasJSON = canvas.toJSON();
|
||||
|
||||
if (onSave) {
|
||||
onSave({
|
||||
dataURL: croppedDataURL,
|
||||
editorData: fullCanvasJSON
|
||||
});
|
||||
if (onSave) onSave(dataURL);
|
||||
}
|
||||
closeCostumeEditor();
|
||||
});
|
||||
|
||||
@@ -416,6 +439,80 @@ const outlineColorPicker = document.getElementById('outline-color-picker');
|
||||
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() {
|
||||
if (editorContainer) {
|
||||
editorContainer.remove();
|
||||
|
||||
@@ -595,12 +595,14 @@ function renderCostumesList() {
|
||||
editBtn.draggable = false;
|
||||
editBtn.title = "Edit costume";
|
||||
editBtn.onclick = () => {
|
||||
openCostumeEditor(costume, async (dataURL) => {
|
||||
if (!dataURL) return;
|
||||
openCostumeEditor(costume, async (costumeData) => {
|
||||
if (!costumeData) return;
|
||||
|
||||
// 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.editorData = costumeData.editorData;
|
||||
|
||||
// Update sprite if this is the current costume
|
||||
if (activeSprite.pixiSprite.texture === costume.texture) {
|
||||
@@ -613,11 +615,11 @@ function renderCostumesList() {
|
||||
if (currentSocket && currentRoom) {
|
||||
currentSocket.emit("projectUpdate", {
|
||||
roomId: currentRoom,
|
||||
type: "updateCostume",
|
||||
type: "addCostume",
|
||||
data: {
|
||||
spriteId: activeSprite.id,
|
||||
name: costume.name,
|
||||
texture: dataURL,
|
||||
name: uniqueName,
|
||||
texture: costumeData.dataURL,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1825,6 +1827,7 @@ loadButton.addEventListener("click", () => {
|
||||
});
|
||||
loadInput.addEventListener("change", loadProject);
|
||||
|
||||
// Create new costume with editor
|
||||
// Create new costume with editor
|
||||
document.getElementById('create-costume-button')?.addEventListener('click', () => {
|
||||
if (!activeSprite) {
|
||||
@@ -1832,10 +1835,13 @@ document.getElementById('create-costume-button')?.addEventListener('click', () =
|
||||
return;
|
||||
}
|
||||
|
||||
openCostumeEditor(null, async (dataURL) => {
|
||||
if (!dataURL || !activeSprite) return;
|
||||
openCostumeEditor(null, async (costumeData) => {
|
||||
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 counter = 1;
|
||||
|
||||
Reference in New Issue
Block a user