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) => {
|
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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user