import { Canvas, PencilBrush, Circle, Rect, Line, IText, FabricImage, Path } from 'fabric'; let canvas = null; let editorContainer = null; let currentTool = 'draw'; let currentColor = '#000000'; let currentSize = 5; let fillEnabled = true; export function openCostumeEditor(existingCostume = null, onSave) { closeCostumeEditor(); editorContainer = document.createElement('div'); editorContainer.className = 'costume-editor-overlay'; editorContainer.innerHTML = `

Costume Editor

`; document.body.appendChild(editorContainer); // Initialize Fabric.js canvas const canvasEl = document.getElementById('fabric-canvas'); canvas = new Canvas(canvasEl, { width: 720, height: 480, backgroundColor: null, isDrawingMode: false }); // Load existing costume if provided setupControls(onSave, existingCostume); } function setupControls(onSave, existingCostume) { const colorPicker = document.getElementById('color-picker'); const brushSize = document.getElementById('brush-size'); const sizeDisplay = document.getElementById('size-display'); const fillEnabledCheckbox = document.getElementById('fill-enabled'); const toolButtons = document.querySelectorAll('.tool-btn'); let isDrawing = false; let startPoint = null; let currentShape = null; let eraserPaths = []; let outlineColor = '#000000'; let outlineWidth = 2; // History setup const history = []; let historyStep = -1; function saveHistory() { if (historyStep < history.length - 1) { history.splice(historyStep + 1); } const json = canvas.toJSON(); history.push(JSON.stringify(json)); historyStep++; if (history.length > 50) { history.shift(); historyStep--; } updateUndoRedoButtons(); } function updateUndoRedoButtons() { document.getElementById('undo-btn').disabled = historyStep <= 0; document.getElementById('redo-btn').disabled = historyStep >= history.length - 1; } // Tool selection toolButtons.forEach(btn => { btn.addEventListener('click', () => { toolButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentTool = btn.dataset.tool; // Clean up existing listeners canvas.off('mouse:down'); canvas.off('mouse:move'); canvas.off('mouse:up'); canvas.isDrawingMode = false; canvas.selection = false; if (currentTool === 'draw') { canvas.isDrawingMode = true; canvas.freeDrawingBrush = new PencilBrush(canvas); canvas.freeDrawingBrush.color = currentColor; canvas.freeDrawingBrush.width = currentSize; } else if (currentTool === 'erase') { canvas.isDrawingMode = true; const eraserBrush = new PencilBrush(canvas); eraserBrush.width = currentSize * 2; eraserBrush.color = '#FFFFFF'; eraserBrush.inverted = true; canvas.freeDrawingBrush = eraserBrush; // Use destination-out for actual erasing canvas.on('before:path:created', (e) => { e.path.globalCompositeOperation = 'destination-out'; }); } else if (currentTool === 'select') { canvas.selection = true; } else if (currentTool === 'text') { canvas.on('mouse:down', (options) => { if (options.target) return; // Don't create new text if clicking existing object const pointer = canvas.getScenePoint(options.e); const text = new IText('Text', { left: pointer.x, top: pointer.y, fill: currentColor, fontSize: Math.max(20, currentSize * 4), fontFamily: 'Arial' }); canvas.add(text); canvas.setActiveObject(text); text.enterEditing(); text.selectAll(); setTimeout(() => saveHistory(), 100); }); } else if (currentTool === 'line' || currentTool === 'rect' || currentTool === 'circle') { canvas.on('mouse:down', (options) => { if (options.target) return; // Don't draw if clicking on existing object isDrawing = true; const pointer = canvas.getScenePoint(options.e); startPoint = { x: pointer.x, y: pointer.y }; if (currentTool === 'line') { currentShape = new Line([startPoint.x, startPoint.y, startPoint.x, startPoint.y], { stroke: outlineColor, strokeWidth: outlineWidth, selectable: false }); } else if (currentTool === 'rect') { currentShape = new Rect({ left: startPoint.x, top: startPoint.y, width: 1, height: 1, fill: fillEnabled ? currentColor : 'transparent', stroke: outlineColor, strokeWidth: outlineWidth, selectable: false }); } else if (currentTool === 'circle') { currentShape = new Circle({ left: startPoint.x, top: startPoint.y, radius: 1, fill: fillEnabled ? currentColor : 'transparent', stroke: outlineColor, strokeWidth: outlineWidth, selectable: false, originX: 'center', originY: 'center' }); } if (currentShape) { canvas.add(currentShape); } }); canvas.on('mouse:move', (options) => { if (!isDrawing || !currentShape) return; const pointer = canvas.getScenePoint(options.e); if (currentTool === 'line') { currentShape.set({ x2: pointer.x, y2: pointer.y }); } else if (currentTool === 'rect') { const width = pointer.x - startPoint.x; const height = pointer.y - startPoint.y; currentShape.set({ width: Math.abs(width), height: Math.abs(height), left: width > 0 ? startPoint.x : pointer.x, top: height > 0 ? startPoint.y : pointer.y }); } else if (currentTool === 'circle') { const radius = Math.sqrt( Math.pow(pointer.x - startPoint.x, 2) + Math.pow(pointer.y - startPoint.y, 2) ); currentShape.set({ radius: Math.max(1, radius) }); } canvas.renderAll(); }); canvas.on('mouse:up', () => { if (isDrawing && currentShape) { currentShape.setCoords(); currentShape.set({ selectable: true }); saveHistory(); } isDrawing = false; currentShape = null; }); } else if (currentTool === 'bucket') { canvas.on('mouse:down', (options) => { if (!options.target) return; const target = options.target; if (target.type === 'rect' || target.type === 'circle' || target.type === 'triangle' || target.type === 'polygon') { target.set({ fill: currentColor, stroke: outlineColor, strokeWidth: outlineWidth }); canvas.renderAll(); saveHistory(); } else if (target.type === 'line' || target.type === 'path') { target.set({ stroke: currentColor, strokeWidth: outlineWidth }); canvas.renderAll(); saveHistory(); } else if (target.type === 'i-text' || target.type === 'text') { target.set('fill', currentColor); canvas.renderAll(); saveHistory(); } }); } }); }); colorPicker.addEventListener('input', (e) => { currentColor = e.target.value; if (canvas.freeDrawingBrush && currentTool === 'draw') { canvas.freeDrawingBrush.color = currentColor; } }); const outlineColorPicker = document.getElementById('outline-color-picker'); const outlineWidthInput = document.getElementById('outline-width'); outlineColorPicker.addEventListener('input', (e) => { outlineColor = e.target.value; }); outlineWidthInput.addEventListener('input', (e) => { outlineWidth = parseInt(e.target.value); }); brushSize.addEventListener('input', (e) => { currentSize = parseInt(e.target.value); sizeDisplay.textContent = `${currentSize}px`; if (canvas.freeDrawingBrush) { canvas.freeDrawingBrush.width = currentSize; } }); fillEnabledCheckbox.addEventListener('change', (e) => { fillEnabled = e.target.checked; }); document.getElementById('clear-canvas').addEventListener('click', () => { if (confirm('Clear entire canvas?')) { canvas.clear(); canvas.backgroundColor = null; canvas.renderAll(); saveHistory(); } }); document.getElementById('delete-selected').addEventListener('click', () => { const activeObjects = canvas.getActiveObjects(); if (activeObjects.length > 0) { activeObjects.forEach(obj => canvas.remove(obj)); canvas.discardActiveObject(); canvas.renderAll(); saveHistory(); } }); canvas.on('path:created', saveHistory); canvas.on('object:added', (e) => { if (e.target && e.target.type !== 'path') { saveHistory(); } }); canvas.on('object:modified', saveHistory); // Snap to center functionality const SNAP_DISTANCE = 15; const centerX = 240; const centerY = 180; canvas.on('object:moving', (e) => { const obj = e.target; // Snap to horizontal center if (Math.abs(obj.left - centerX) < SNAP_DISTANCE) { obj.set({ left: centerX }); } // Snap to vertical center if (Math.abs(obj.top - centerY) < SNAP_DISTANCE) { obj.set({ top: centerY }); } canvas.renderAll(); }); document.getElementById('undo-btn').addEventListener('click', () => { if (historyStep > 0) { historyStep--; canvas.loadFromJSON(history[historyStep]).then(() => { canvas.renderAll(); updateUndoRedoButtons(); }); } }); document.getElementById('redo-btn').addEventListener('click', () => { if (historyStep < history.length - 1) { historyStep++; canvas.loadFromJSON(history[historyStep]).then(() => { canvas.renderAll(); updateUndoRedoButtons(); }); } }); // Trigger draw mode by default // 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); canvas.renderAll(); saveHistory(); }).catch(err => { console.error('Failed to load costume:', err); }); } } 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); closeCostumeEditor(); }); document.querySelector('.cancel-btn').addEventListener('click', closeCostumeEditor); document.querySelector('.close-editor-btn').addEventListener('click', closeCostumeEditor); } export function closeCostumeEditor() { if (editorContainer) { editorContainer.remove(); editorContainer = null; } if (canvas) { canvas.dispose(); canvas = null; } } // CSS (same as before) const style = document.createElement('style'); style.textContent = ` .costume-editor-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; } .costume-editor-modal { background: #2b2b2b; border-radius: 8px; width: 90%; max-width: 800px; max-height: 90vh; display: flex; flex-direction: column; color: white; } .costume-editor-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid #444; } .costume-editor-header h2 { margin: 0; font-size: 20px; } .close-editor-btn { background: none; border: none; color: white; font-size: 30px; cursor: pointer; } .close-editor-btn:hover { color: #ff4444; } .costume-editor-content { display: flex; flex-direction: column; flex: 1; overflow: hidden; } .costume-editor-toolbar { display: flex; gap: 15px; padding: 15px 20px; border-bottom: 1px solid #444; flex-wrap: wrap; background: #333; align-items: center; } .tool-group { display: flex; gap: 8px; align-items: center; } .tool-btn, .action-btn { background: #444; border: 2px solid transparent; color: white; padding: 8px 12px; border-radius: 4px; cursor: pointer; transition: all 0.2s; font-size: 16px; } .tool-btn:hover, .action-btn:hover { background: #555; } .tool-btn.active { background: #0066ff; border-color: #0044cc; } .costume-editor-toolbar label { display: flex; align-items: center; gap: 8px; font-size: 14px; } #color-picker { width: 40px; height: 30px; border: none; cursor: pointer; } #brush-size { width: 100px; } #size-display { font-size: 12px; color: #aaa; min-width: 35px; } .costume-editor-canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a1a; overflow: auto; padding: 20px; } .canvas-container { border: 2px solid #444; border-radius: 4px; background-image: linear-gradient(45deg, #666 25%, transparent 25%), linear-gradient(-45deg, #666 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #666 75%), linear-gradient(-45deg, transparent 75%, #666 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; } .costume-editor-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 15px 20px; border-top: 1px solid #444; } .costume-editor-footer button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: all 0.2s; } .cancel-btn { background: #555; color: white; } .cancel-btn:hover { background: #666; } .save-btn { background: #0066ff; color: white; } .save-btn:hover { background: #0055dd; } .primary { font-weight: bold; } .action-btn:disabled { opacity: 0.5; cursor: not-allowed; } `; document.head.appendChild(style);