diff --git a/src/scripts/costumeEditor.js b/src/scripts/costumeEditor.js index da44669..3aa183b 100644 --- a/src/scripts/costumeEditor.js +++ b/src/scripts/costumeEditor.js @@ -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,41 +375,63 @@ 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) { - 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); +// 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 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 { // Save initial empty state saveHistory(); } - document.querySelector('.save-btn').addEventListener('click', () => { - const dataURL = canvas.toDataURL({ - format: 'png', - quality: 1, - multiplier: 1 - }); - if (onSave) onSave(dataURL); +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 + }); + } 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(); diff --git a/src/scripts/editor.js b/src/scripts/editor.js index 4c00de0..dd97dcb 100644 --- a/src/scripts/editor.js +++ b/src/scripts/editor.js @@ -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;