From cdc65a17ea2361b2722151821f351839bdeaf4d0 Mon Sep 17 00:00:00 2001 From: intrexx mervore <146873484+numbpill3d@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:10:58 -0400 Subject: [PATCH 1/9] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3c3629e..37d7e73 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.env From 05192cd19af7431522840267bd2c30e0433a4983 Mon Sep 17 00:00:00 2001 From: numbpill3d Date: Thu, 5 Jun 2025 02:53:55 -0400 Subject: [PATCH 2/9] feat: Implement main application logic and UI management - Added main application script for Conjuration, initializing core components such as UI Manager, Theme Manager, and various tools (Brush Engine, Palette Tool, etc.). - Set up event listeners for window controls, menu management, and tool interactions. - Created a centralized MenuManager class to handle menu interactions and state management. - Implemented canvas size selection dialog with visual previews and resizing functionality. - Added project management features including new, open, and save project functionalities. - Integrated GIF and PNG export capabilities. --- .history/src/scripts/app_20250605021544.js | 667 +++++++++++++++++ .../scripts/ui/MenuManager_20250605015633.js | 0 .../scripts/ui/MenuManager_20250605015720.js | 106 +++ src/scripts/app.js | 692 +++++++----------- src/scripts/ui/MenuManager.js | 106 +++ 5 files changed, 1147 insertions(+), 424 deletions(-) create mode 100644 .history/src/scripts/app_20250605021544.js create mode 100644 .history/src/scripts/ui/MenuManager_20250605015633.js create mode 100644 .history/src/scripts/ui/MenuManager_20250605015720.js create mode 100644 src/scripts/ui/MenuManager.js diff --git a/.history/src/scripts/app_20250605021544.js b/.history/src/scripts/app_20250605021544.js new file mode 100644 index 0000000..ec1b229 --- /dev/null +++ b/.history/src/scripts/app_20250605021544.js @@ -0,0 +1,667 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. + */ + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Glitch Tool + const glitchTool = new GlitchTool(pixelCanvas); + + // Initialize Timeline + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }); + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); + + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast('Failed to generate GIF: ' + error.message, 'error'); + }); + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + scanLines: document.getElementById('effect-scanLines').checked, + vignette: document.getElementById('effect-vignette').checked, + noise: document.getElementById('effect-noise').checked, + pixelate: document.getElementById('effect-pixelate').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 350, height: 350, name: '350×350', description: 'Medium square' }, + { width: 800, height: 500, name: '800×500', description: 'Large landscape' }, + { width: 900, height: 900, name: '900×900', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // We don't need preview width/height anymore since we're using fixed size preview boxes + + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add some CSS for the size selection dialog + const style = document.createElement('style'); + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + + document.head.appendChild(style); + } +}); diff --git a/.history/src/scripts/ui/MenuManager_20250605015633.js b/.history/src/scripts/ui/MenuManager_20250605015633.js new file mode 100644 index 0000000..e69de29 diff --git a/.history/src/scripts/ui/MenuManager_20250605015720.js b/.history/src/scripts/ui/MenuManager_20250605015720.js new file mode 100644 index 0000000..fdc243a --- /dev/null +++ b/.history/src/scripts/ui/MenuManager_20250605015720.js @@ -0,0 +1,106 @@ +/** + * MenuManager - Centralized menu management system + * Handles all menu interactions and state management + */ +class MenuManager { + constructor() { + this.menus = new Map(); + this.activeMenu = null; + + // Register all menus + this.registerMenu('file', 'file-menu-button', 'file-menu'); + this.registerMenu('edit', 'edit-menu-button', 'edit-menu'); + this.registerMenu('view', 'view-menu-button', 'view-menu'); + this.registerMenu('export', 'export-menu-button', 'export-menu'); + this.registerMenu('lore', 'lore-menu-button', 'lore-menu'); + + // Close menus when clicking outside + document.addEventListener('click', () => this.closeAllMenus()); + } + + /** + * Register a new menu + * @param {string} name - Menu identifier + * @param {string} buttonId - Button element ID + * @param {string} menuId - Menu element ID + */ + registerMenu(name, buttonId, menuId) { + const button = document.getElementById(buttonId); + const menu = document.getElementById(menuId); + + if (!button || !menu) { + console.warn(`Menu elements not found for: ${name}`); + return; + } + + this.menus.set(name, { button, menu }); + + button.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleMenu(name); + }); + } + + /** + * Toggle menu visibility + * @param {string} name - Menu identifier + */ + toggleMenu(name) { + const menuData = this.menus.get(name); + if (!menuData) return; + + if (this.activeMenu === name) { + this.closeMenu(name); + } else { + this.closeAllMenus(); + this.openMenu(name); + } + } + + /** + * Open a specific menu + * @param {string} name - Menu identifier + */ + openMenu(name) { + const menuData = this.menus.get(name); + if (!menuData) return; + + const { button, menu } = menuData; + const buttonRect = button.getBoundingClientRect(); + + menu.style.left = `${buttonRect.left}px`; + menu.style.top = `${buttonRect.bottom}px`; + menu.style.zIndex = '2000'; + menu.style.display = 'flex'; + button.classList.add('active'); + + this.activeMenu = name; + } + + /** + * Close a specific menu + * @param {string} name - Menu identifier + */ + closeMenu(name) { + const menuData = this.menus.get(name); + if (!menuData) return; + + menuData.menu.style.display = 'none'; + menuData.button.classList.remove('active'); + + if (this.activeMenu === name) { + this.activeMenu = null; + } + } + + /** + * Close all menus + */ + closeAllMenus() { + this.menus.forEach((menuData, name) => { + this.closeMenu(name); + }); + } +} + +export default MenuManager; \ No newline at end of file diff --git a/src/scripts/app.js b/src/scripts/app.js index a2cbc36..ec1b229 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -65,359 +65,63 @@ document.addEventListener('DOMContentLoaded', () => { * Set up all event listeners for the application */ function setupEventListeners() { - // Window control buttons - document.getElementById('minimize-button').addEventListener('click', () => { - voidAPI.minimizeWindow(); - }); + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + document.getElementById('maximize-button').addEventListener('click', () => { voidAPI.maximizeWindow().then(result => { - // Update button text based on window state - document.getElementById('maximize-button').textContent = - result.isMaximized ? '□' : '[]'; - }); - }); - - document.getElementById('close-button').addEventListener('click', () => { - voidAPI.closeWindow(); - }); - - // Menu buttons - document.getElementById('file-menu-button').addEventListener('click', (e) => { - e.stopPropagation(); - const menu = document.getElementById('file-menu'); - if (menu.style.display === 'flex') { - menu.style.display = 'none'; - document.getElementById('file-menu-button').classList.remove('active'); - } else { - // Close all menus first - document.querySelectorAll('.menu-dropdown').forEach(m => { - m.style.display = 'none'; - }); - document.querySelectorAll('.menu-button').forEach(b => { - b.classList.remove('active'); - }); - - // Position and show this menu - const buttonRect = e.target.getBoundingClientRect(); - menu.style.left = `${buttonRect.left}px`; - menu.style.top = `${buttonRect.bottom}px`; - menu.style.zIndex = '2000'; - menu.style.display = 'flex'; - document.getElementById('file-menu-button').classList.add('active'); - } - }); - - document.getElementById('edit-menu-button').addEventListener('click', (e) => { - e.stopPropagation(); - const menu = document.getElementById('edit-menu'); - if (menu.style.display === 'flex') { - menu.style.display = 'none'; - document.getElementById('edit-menu-button').classList.remove('active'); - } else { - // Close all menus first - document.querySelectorAll('.menu-dropdown').forEach(m => { - m.style.display = 'none'; - }); - document.querySelectorAll('.menu-button').forEach(b => { - b.classList.remove('active'); - }); - - // Position and show this menu - const buttonRect = e.target.getBoundingClientRect(); - menu.style.left = `${buttonRect.left}px`; - menu.style.top = `${buttonRect.bottom}px`; - menu.style.zIndex = '2000'; - menu.style.display = 'flex'; - document.getElementById('edit-menu-button').classList.add('active'); - } - }); - - document.getElementById('view-menu-button').addEventListener('click', (e) => { - e.stopPropagation(); - const menu = document.getElementById('view-menu'); - if (menu.style.display === 'flex') { - menu.style.display = 'none'; - document.getElementById('view-menu-button').classList.remove('active'); - } else { - // Close all menus first - document.querySelectorAll('.menu-dropdown').forEach(m => { - m.style.display = 'none'; - }); - document.querySelectorAll('.menu-button').forEach(b => { - b.classList.remove('active'); - }); - - // Position and show this menu - const buttonRect = e.target.getBoundingClientRect(); - menu.style.left = `${buttonRect.left}px`; - menu.style.top = `${buttonRect.bottom}px`; - menu.style.zIndex = '2000'; - menu.style.display = 'flex'; - document.getElementById('view-menu-button').classList.add('active'); - } - }); - - document.getElementById('export-menu-button').addEventListener('click', (e) => { - e.stopPropagation(); - const menu = document.getElementById('export-menu'); - if (menu.style.display === 'flex') { - menu.style.display = 'none'; - document.getElementById('export-menu-button').classList.remove('active'); - } else { - // Close all menus first - document.querySelectorAll('.menu-dropdown').forEach(m => { - m.style.display = 'none'; - }); - document.querySelectorAll('.menu-button').forEach(b => { - b.classList.remove('active'); - }); - - // Position and show this menu - const buttonRect = e.target.getBoundingClientRect(); - menu.style.left = `${buttonRect.left}px`; - menu.style.top = `${buttonRect.bottom}px`; - menu.style.zIndex = '2000'; - menu.style.display = 'flex'; - document.getElementById('export-menu-button').classList.add('active'); - } - }); - - document.getElementById('lore-menu-button').addEventListener('click', (e) => { - e.stopPropagation(); - const menu = document.getElementById('lore-menu'); - if (menu.style.display === 'flex') { - menu.style.display = 'none'; - document.getElementById('lore-menu-button').classList.remove('active'); - } else { - // Close all menus first - document.querySelectorAll('.menu-dropdown').forEach(m => { - m.style.display = 'none'; - }); - document.querySelectorAll('.menu-button').forEach(b => { - b.classList.remove('active'); - }); - - // Position and show this menu - const buttonRect = e.target.getBoundingClientRect(); - menu.style.left = `${buttonRect.left}px`; - menu.style.top = `${buttonRect.bottom}px`; - menu.style.zIndex = '2000'; - menu.style.display = 'flex'; - document.getElementById('lore-menu-button').classList.add('active'); - } - }); - - // Close menus when clicking outside - document.addEventListener('click', () => { - document.querySelectorAll('.menu-dropdown').forEach(m => { - m.style.display = 'none'; - }); - document.querySelectorAll('.menu-button').forEach(b => { - b.classList.remove('active'); - }); - }); - - // File menu items - document.getElementById('new-project').addEventListener('click', () => { - uiManager.showConfirmDialog( - 'Create New Project', - 'This will clear your current project. Are you sure?', - () => { - pixelCanvas.clear(); - timeline.clear(); - timeline.addFrame(); - menuSystem.closeAllMenus(); - uiManager.showToast('New project created', 'success'); - } - ); - }); - - document.getElementById('open-project').addEventListener('click', () => { - voidAPI.openProject().then(result => { - if (result.success) { - try { - const projectData = result.data; - pixelCanvas.setDimensions(projectData.width, projectData.height); - timeline.loadFromData(projectData.frames); - menuSystem.closeAllMenus(); - uiManager.showToast('Project loaded successfully', 'success'); - } catch (error) { - uiManager.showToast('Failed to load project: ' + error.message, 'error'); - } - } - }); - }); - - document.getElementById('save-project').addEventListener('click', () => { - const projectData = { - width: pixelCanvas.width, - height: pixelCanvas.height, - frames: timeline.getFramesData(), - palette: paletteTool.getCurrentPalette(), - effects: { - grain: document.getElementById('effect-grain').checked, - static: document.getElementById('effect-static').checked, - glitch: document.getElementById('effect-glitch').checked, - crt: document.getElementById('effect-crt').checked, - intensity: document.getElementById('effect-intensity').value - } - }; - - voidAPI.saveProject(projectData).then(result => { - if (result.success) { - menuSystem.closeAllMenus(); - uiManager.showToast('Project saved successfully', 'success'); - } else { - uiManager.showToast('Failed to save project', 'error'); - } + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; }); }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } - // Edit menu items - document.getElementById('undo').addEventListener('click', () => { - if (pixelCanvas.undo()) { - uiManager.showToast('Undo successful', 'info'); - } else { - uiManager.showToast('Nothing to undo', 'info'); - } - menuSystem.closeAllMenus(); - }); - - document.getElementById('redo').addEventListener('click', () => { - if (pixelCanvas.redo()) { - uiManager.showToast('Redo successful', 'info'); - } else { - uiManager.showToast('Nothing to redo', 'info'); - } - menuSystem.closeAllMenus(); - }); - - document.getElementById('toggle-grid').addEventListener('click', () => { - pixelCanvas.toggleGrid(); - menuSystem.closeAllMenus(); - uiManager.showToast('Grid toggled', 'info'); - }); - - document.getElementById('resize-canvas').addEventListener('click', () => { - // Show canvas resize dialog - const content = ` -
- -
- - - - - - - -
-
-
- -
- - × - -
-
-
- -
- `; - - uiManager.showModal('Resize Canvas', content, () => { - menuSystem.closeAllMenus(); - }); - - // Add event listeners to preset buttons - document.querySelectorAll('.preset-size-button').forEach(button => { - button.addEventListener('click', () => { - const width = parseInt(button.dataset.width); - const height = parseInt(button.dataset.height); - document.getElementById('canvas-width').value = width; - document.getElementById('canvas-height').value = height; - }); - }); - - // Add resize button to modal footer - const modalFooter = document.createElement('div'); - modalFooter.className = 'modal-footer'; - - const cancelButton = document.createElement('button'); - cancelButton.className = 'modal-button'; - cancelButton.textContent = 'Cancel'; - cancelButton.addEventListener('click', () => { - uiManager.hideModal(); - }); - - const resizeButton = document.createElement('button'); - resizeButton.className = 'modal-button primary'; - resizeButton.textContent = 'Resize'; - resizeButton.addEventListener('click', () => { - const width = parseInt(document.getElementById('canvas-width').value); - const height = parseInt(document.getElementById('canvas-height').value); - const preserveContent = document.getElementById('preserve-content').checked; - - if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { - pixelCanvas.resize(width, height, preserveContent); - updateCanvasSizeDisplay(); - uiManager.hideModal(); - uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); - } else { - uiManager.showToast('Invalid dimensions', 'error'); - } - }); - - modalFooter.appendChild(cancelButton); - modalFooter.appendChild(resizeButton); - - document.querySelector('.modal-dialog').appendChild(modalFooter); + function setupMenuManager() { + // Already handled by MenuManager + } - menuSystem.closeAllMenus(); - }); + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } - // Export menu items - document.getElementById('export-png').addEventListener('click', () => { - const pngDataUrl = pixelCanvas.exportToPNG(); - voidAPI.exportPng(pngDataUrl).then(result => { - if (result.success) { - menuSystem.closeAllMenus(); - uiManager.showToast('PNG exported successfully', 'success'); - } else { - uiManager.showToast('Failed to export PNG', 'error'); - } - }); - }); + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } - document.getElementById('export-gif').addEventListener('click', () => { - uiManager.showLoadingDialog('Generating GIF...'); - - // Get frame delay from input - const frameDelay = parseInt(document.getElementById('frame-delay').value); - - // Generate GIF - gifExporter.generateGif(frameDelay).then(gifData => { - voidAPI.exportGif(gifData).then(result => { - uiManager.hideLoadingDialog(); - if (result.success) { - menuSystem.closeAllMenus(); - uiManager.showToast('GIF exported successfully', 'success'); - } else { - uiManager.showToast('Failed to export GIF', 'error'); - } - }); - }).catch(error => { - uiManager.hideLoadingDialog(); - uiManager.showToast('Failed to generate GIF: ' + error.message, 'error'); - }); - }); + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } - // Theme selection + function setupThemeMenu() { document.getElementById('theme-lain-dive').addEventListener('click', () => { themeManager.setTheme('lain-dive'); menuSystem.closeAllMenus(); @@ -432,21 +136,9 @@ document.addEventListener('DOMContentLoaded', () => { themeManager.setTheme('monolith'); menuSystem.closeAllMenus(); }); + } - // Toggle grid - document.getElementById('toggle-grid').addEventListener('click', () => { - pixelCanvas.toggleGrid(); - menuSystem.closeAllMenus(); - }); - - // Toggle rulers (placeholder functionality) - document.getElementById('toggle-rulers').addEventListener('click', () => { - // Placeholder for ruler functionality - uiManager.showToast('Rulers feature coming soon', 'info'); - menuSystem.closeAllMenus(); - }); - - // Lore menu items + function setupLoreMenu() { document.getElementById('lore-option1').addEventListener('click', () => { themeManager.setTheme('lain-dive'); menuSystem.closeAllMenus(); @@ -464,20 +156,19 @@ document.addEventListener('DOMContentLoaded', () => { menuSystem.closeAllMenus(); uiManager.showToast('Lore: Monolith activated', 'success'); }); + } - // Tool buttons + function setupToolButtons() { document.querySelectorAll('.tool-button').forEach(button => { button.addEventListener('click', () => { const toolId = button.id; - // Handle brush tools if (toolId.startsWith('brush-')) { const brushType = toolId.replace('brush-', ''); brushEngine.setActiveBrush(brushType); uiManager.setActiveTool(toolId); } - // Handle symmetry tools if (toolId.startsWith('symmetry-')) { const symmetryType = toolId.replace('symmetry-', ''); symmetryTools.setSymmetryMode(symmetryType); @@ -485,8 +176,9 @@ document.addEventListener('DOMContentLoaded', () => { } }); }); + } - // Palette options + function setupPaletteOptions() { document.querySelectorAll('.palette-option').forEach(option => { option.addEventListener('click', () => { const paletteId = option.id; @@ -495,64 +187,46 @@ document.addEventListener('DOMContentLoaded', () => { uiManager.setActivePalette(paletteId); }); }); + } - // Effect checkboxes + function setupEffectControls() { document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { - checkbox.addEventListener('change', () => { - updateEffects(); - }); + checkbox.addEventListener('change', updateEffects); }); - // Effect intensity slider - document.getElementById('effect-intensity').addEventListener('input', () => { - updateEffects(); - }); + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } - // Brush size slider + function setupBrushControls() { document.getElementById('brush-size').addEventListener('input', (e) => { const size = parseInt(e.target.value); brushEngine.setBrushSize(size); document.getElementById('brush-size-value').textContent = size; }); + } - // Timeline controls - document.getElementById('add-frame').addEventListener('click', () => { - timeline.addFrame(); - }); - - document.getElementById('duplicate-frame').addEventListener('click', () => { - timeline.duplicateCurrentFrame(); - }); - - document.getElementById('delete-frame').addEventListener('click', () => { - if (timeline.getFrameCount() > 1) { - timeline.deleteCurrentFrame(); - } else { - uiManager.showToast('Cannot delete the only frame', 'error'); - } - }); - - // Animation controls - document.getElementById('play-animation').addEventListener('click', () => { - timeline.playAnimation(); - }); - - document.getElementById('stop-animation').addEventListener('click', () => { - timeline.stopAnimation(); - }); + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + document.getElementById('loop-animation').addEventListener('click', (e) => { const loopButton = e.currentTarget; loopButton.classList.toggle('active'); timeline.setLooping(loopButton.classList.contains('active')); }); - - // Onion skin toggle + document.getElementById('onion-skin').addEventListener('change', (e) => { timeline.setOnionSkinning(e.target.checked); }); + } - // Zoom controls + function setupZoomControls() { document.getElementById('zoom-in').addEventListener('click', () => { pixelCanvas.zoomIn(); updateZoomLevel(); @@ -563,7 +237,6 @@ document.addEventListener('DOMContentLoaded', () => { updateZoomLevel(); }); - // Zoom menu items document.getElementById('zoom-in-menu').addEventListener('click', () => { pixelCanvas.zoomIn(); updateZoomLevel(); @@ -582,44 +255,215 @@ document.addEventListener('DOMContentLoaded', () => { menuSystem.closeAllMenus(); }); - // Add mouse wheel zoom document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { - // Prevent default scrolling e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; - // Zoom in or out based on wheel direction - if (e.deltaY < 0) { - pixelCanvas.zoomIn(); + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); } else { - pixelCanvas.zoomOut(); + uiManager.showToast('Failed to save project', 'error'); } + }); + } - updateZoomLevel(); - }, { passive: false }); + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } - // Canvas size display - updateCanvasSizeDisplay(); + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } - // Initialize with default tool - uiManager.setActiveTool('brush-pencil'); - uiManager.setActiveSymmetry('symmetry-none'); - uiManager.setActivePalette('palette-monochrome'); + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } - // Menu item event listeners for each menu item - document.querySelectorAll('.menu-item').forEach(item => { - item.addEventListener('click', (e) => { - e.stopPropagation(); - // Close all menus when a menu item is clicked - document.querySelectorAll('.menu-dropdown').forEach(m => { - m.style.display = 'none'; - }); - document.querySelectorAll('.menu-button').forEach(b => { - b.classList.remove('active'); - }); + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }); + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); + + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast('Failed to generate GIF: ' + error.message, 'error'); }); } + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + /** * Update all active effects */ diff --git a/src/scripts/ui/MenuManager.js b/src/scripts/ui/MenuManager.js new file mode 100644 index 0000000..fdc243a --- /dev/null +++ b/src/scripts/ui/MenuManager.js @@ -0,0 +1,106 @@ +/** + * MenuManager - Centralized menu management system + * Handles all menu interactions and state management + */ +class MenuManager { + constructor() { + this.menus = new Map(); + this.activeMenu = null; + + // Register all menus + this.registerMenu('file', 'file-menu-button', 'file-menu'); + this.registerMenu('edit', 'edit-menu-button', 'edit-menu'); + this.registerMenu('view', 'view-menu-button', 'view-menu'); + this.registerMenu('export', 'export-menu-button', 'export-menu'); + this.registerMenu('lore', 'lore-menu-button', 'lore-menu'); + + // Close menus when clicking outside + document.addEventListener('click', () => this.closeAllMenus()); + } + + /** + * Register a new menu + * @param {string} name - Menu identifier + * @param {string} buttonId - Button element ID + * @param {string} menuId - Menu element ID + */ + registerMenu(name, buttonId, menuId) { + const button = document.getElementById(buttonId); + const menu = document.getElementById(menuId); + + if (!button || !menu) { + console.warn(`Menu elements not found for: ${name}`); + return; + } + + this.menus.set(name, { button, menu }); + + button.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleMenu(name); + }); + } + + /** + * Toggle menu visibility + * @param {string} name - Menu identifier + */ + toggleMenu(name) { + const menuData = this.menus.get(name); + if (!menuData) return; + + if (this.activeMenu === name) { + this.closeMenu(name); + } else { + this.closeAllMenus(); + this.openMenu(name); + } + } + + /** + * Open a specific menu + * @param {string} name - Menu identifier + */ + openMenu(name) { + const menuData = this.menus.get(name); + if (!menuData) return; + + const { button, menu } = menuData; + const buttonRect = button.getBoundingClientRect(); + + menu.style.left = `${buttonRect.left}px`; + menu.style.top = `${buttonRect.bottom}px`; + menu.style.zIndex = '2000'; + menu.style.display = 'flex'; + button.classList.add('active'); + + this.activeMenu = name; + } + + /** + * Close a specific menu + * @param {string} name - Menu identifier + */ + closeMenu(name) { + const menuData = this.menus.get(name); + if (!menuData) return; + + menuData.menu.style.display = 'none'; + menuData.button.classList.remove('active'); + + if (this.activeMenu === name) { + this.activeMenu = null; + } + } + + /** + * Close all menus + */ + closeAllMenus() { + this.menus.forEach((menuData, name) => { + this.closeMenu(name); + }); + } +} + +export default MenuManager; \ No newline at end of file From 253c15ba5eb851f2af4f24905ff2c9a44e4b2d43 Mon Sep 17 00:00:00 2001 From: numbpill3d Date: Sat, 7 Jun 2025 13:12:49 -0400 Subject: [PATCH 3/9] feat: Implement PixelCanvas class for pixel manipulation and rendering - Added PixelCanvas class to handle drawing on a canvas with pixel manipulation. - Implemented methods for drawing pixels, lines, rectangles, ellipses, and flood fill. - Introduced undo/redo functionality with history management. - Added support for various visual effects (grain, static, glitch, CRT, scan lines, vignette, noise, pixelate). - Implemented zooming and grid display features. - Included methods for exporting canvas as PNG and managing pixel data. - Set up event listeners for mouse interactions and cursor position updates. --- .history/src/scripts/app_20250605030646.js | 666 ++++++++++++ .../canvas/PixelCanvas_20250605030852.js | 996 +++++++++++++++++ .../canvas/PixelCanvas_20250605031003.js | 999 ++++++++++++++++++ src/scripts/app.js | 157 ++- src/scripts/canvas/PixelCanvas.js | 83 +- 5 files changed, 2779 insertions(+), 122 deletions(-) create mode 100644 .history/src/scripts/app_20250605030646.js create mode 100644 .history/src/scripts/canvas/PixelCanvas_20250605030852.js create mode 100644 .history/src/scripts/canvas/PixelCanvas_20250605031003.js diff --git a/.history/src/scripts/app_20250605030646.js b/.history/src/scripts/app_20250605030646.js new file mode 100644 index 0000000..dd777c4 --- /dev/null +++ b/.history/src/scripts/app_20250605030646.js @@ -0,0 +1,666 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. + */ + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Glitch Tool + const glitchTool = new GlitchTool(pixelCanvas); + + // Initialize Timeline + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }); + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); + + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast('Failed to generate GIF: ' + error.message, 'error'); + }); + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + scanLines: document.getElementById('effect-scanLines').checked, + vignette: document.getElementById('effect-vignette').checked, + noise: document.getElementById('effect-noise').checked, + pixelate: document.getElementById('effect-pixelate').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/canvas/PixelCanvas_20250605030852.js b/.history/src/scripts/canvas/PixelCanvas_20250605030852.js new file mode 100644 index 0000000..f34352d --- /dev/null +++ b/.history/src/scripts/canvas/PixelCanvas_20250605030852.js @@ -0,0 +1,996 @@ +/** + * PixelCanvas Class + * + * Handles the main drawing canvas, pixel manipulation, and rendering. + */ +class PixelCanvas { + /** + * Create a new PixelCanvas + * @param {Object} options - Configuration options + * @param {string} options.canvasId - ID of the main canvas element + * @param {string} options.effectsCanvasId - ID of the effects overlay canvas + * @param {string} options.uiCanvasId - ID of the UI overlay canvas + * @param {number} options.width - Width in pixels + * @param {number} options.height - Height in pixels + * @param {number} options.pixelSize - Size of each pixel in screen pixels + */ + constructor(options) { + // Canvas elements + this.canvas = document.getElementById(options.canvasId); + this.effectsCanvas = document.getElementById(options.effectsCanvasId); + this.uiCanvas = document.getElementById(options.uiCanvasId); + + // Canvas contexts + this.ctx = this.canvas.getContext('2d'); + this.effectsCtx = this.effectsCanvas.getContext('2d'); + this.uiCtx = this.uiCanvas.getContext('2d'); + + // Canvas dimensions + this.width = options.width || 64; + this.height = options.height || 64; + this.pixelSize = options.pixelSize || 8; + + // Zoom level + this.zoom = 1; + + // Pixel data + this.pixels = new Array(this.width * this.height).fill('#000000'); + + // Undo/Redo history + this.history = []; + this.historyIndex = -1; + this.maxHistorySize = 50; // Maximum number of states to store + + // Save initial state + this.saveToHistory(); + + // Effects settings + this.effects = { + grain: false, + static: false, + glitch: false, + crt: false, + scanLines: false, + vignette: false, + noise: false, + pixelate: false, + intensity: 0.5 + }; + + // Animation frame for effects + this.effectsAnimationFrame = null; + + // Mouse state + this.isDrawing = false; + this.lastX = 0; + this.lastY = 0; + + // Grid display + this.showGrid = true; + + // Initialize the canvas + this.initCanvas(); + + // Set up event listeners + this.setupEventListeners(); + + // Start the effects animation loop + this.animateEffects(); + } + + /** + * Initialize the canvas with the correct dimensions + */ + initCanvas() { + // Set canvas dimensions + this.canvas.width = this.width * this.pixelSize * this.zoom; + this.canvas.height = this.height * this.pixelSize * this.zoom; + + // Set effects canvas dimensions + this.effectsCanvas.width = this.canvas.width; + this.effectsCanvas.height = this.canvas.height; + + // Set UI canvas dimensions + this.uiCanvas.width = this.canvas.width; + this.uiCanvas.height = this.canvas.height; + + // Set rendering options for pixel art + this.ctx.imageSmoothingEnabled = false; + this.effectsCtx.imageSmoothingEnabled = false; + this.uiCtx.imageSmoothingEnabled = false; + + // Clear the canvas + this.clear(); + + // Draw the initial grid + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Set up event listeners for mouse/touch interaction + */ + setupEventListeners() { + // Bind event handlers to this instance + this._boundHandleMouseDown = this.handleMouseDown.bind(this); + this._boundHandleMouseMove = this.handleMouseMove.bind(this); + this._boundHandleMouseUp = this.handleMouseUp.bind(this); + this._boundUpdateCursorPosition = this.updateCursorPosition.bind(this); + this._boundHandleContextMenu = (e) => { e.preventDefault(); }; + this._boundHandleMouseLeave = () => { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + }; + + // Mouse events + this.canvas.addEventListener('mousedown', this._boundHandleMouseDown); + document.addEventListener('mousemove', this._boundHandleMouseMove); + document.addEventListener('mouseup', this._boundHandleMouseUp); + + // Prevent context menu on right-click + this.canvas.addEventListener('contextmenu', this._boundHandleContextMenu); + + // Update cursor position display + this.canvas.addEventListener('mousemove', this._boundUpdateCursorPosition); + + // Mouse leave + this.canvas.addEventListener('mouseleave', this._boundHandleMouseLeave); + } + + /** + * Handle mouse down event + * @param {MouseEvent} e - Mouse event + */ + handleMouseDown(e) { + this.isDrawing = true; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Store last position + this.lastX = x; + this.lastY = y; + + // Save the current state before drawing + this.saveToHistory(); + + // Draw a single pixel + this.drawPixel(x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Render the canvas + this.render(); + } + + /** + * Handle mouse move event + * @param {MouseEvent} e - Mouse event + */ + handleMouseMove(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Only draw if the position has changed + if (x !== this.lastX || y !== this.lastY) { + // Draw a line from last position to current position + this.drawLine(this.lastX, this.lastY, x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Update last position + this.lastX = x; + this.lastY = y; + + // Render the canvas + this.render(); + } + } + + /** + * Handle mouse up event + */ + handleMouseUp() { + this.isDrawing = false; + } + + /** + * Update the cursor position display + * @param {MouseEvent} e - Mouse event + */ + updateCursorPosition(e) { + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + document.getElementById('cursor-position').textContent = `X: ${x} Y: ${y}`; + } else { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + } + } + + /** + * Draw a single pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawPixel(x, y, color) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + this.pixels[index] = color; + } + } + + /** + * Draw a line using Bresenham's algorithm + * @param {number} x0 - Starting X coordinate + * @param {number} y0 - Starting Y coordinate + * @param {number} x1 - Ending X coordinate + * @param {number} y1 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawLine(x0, y0, x1, y1, color) { + const dx = Math.abs(x1 - x0); + const dy = Math.abs(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dx - dy; + + while (true) { + this.drawPixel(x0, y0, color); + + if (x0 === x1 && y0 === y1) break; + + const e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + /** + * Draw a rectangle + * @param {number} x - X coordinate of top-left corner + * @param {number} y - Y coordinate of top-left corner + * @param {number} width - Width of rectangle + * @param {number} height - Height of rectangle + * @param {string} color - Color in hex format + * @param {boolean} fill - Whether to fill the rectangle + */ + drawRect(x, y, width, height, color, fill = false) { + if (fill) { + for (let i = x; i < x + width; i++) { + for (let j = y; j < y + height; j++) { + this.drawPixel(i, j, color); + } + } + } else { + // Draw horizontal lines + for (let i = x; i < x + width; i++) { + this.drawPixel(i, y, color); + this.drawPixel(i, y + height - 1, color); + } + + // Draw vertical lines + for (let j = y; j < y + height; j++) { + this.drawPixel(x, j, color); + this.drawPixel(x + width - 1, j, color); + } + } + } + + /** + * Draw an ellipse + * @param {number} xc - X coordinate of center + * @param {number} yc - Y coordinate of center + * @param {number} a - Semi-major axis + * @param {number} b - Semi-minor axis + * @param {string} color - Color in hex format + */ + drawEllipse(xc, yc, a, b, color) { + let x = 0; + let y = b; + let a2 = a * a; + let b2 = b * b; + let d = b2 - a2 * b + a2 / 4; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + + while (a2 * (y - 0.5) > b2 * (x + 1)) { + if (d < 0) { + d += b2 * (2 * x + 3); + } else { + d += b2 * (2 * x + 3) + a2 * (-2 * y + 2); + y--; + } + x++; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + + d = b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2; + + while (y > 0) { + if (d < 0) { + d += b2 * (2 * x + 2) + a2 * (-2 * y + 3); + x++; + } else { + d += a2 * (-2 * y + 3); + } + y--; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + } + + /** + * Fill an area with a color (flood fill) + * @param {number} x - Starting X coordinate + * @param {number} y - Starting Y coordinate + * @param {string} fillColor - Color to fill with + */ + floodFill(x, y, fillColor) { + const targetColor = this.getPixel(x, y); + + // Don't fill if the target color is the same as the fill color + if (targetColor === fillColor) return; + + // Use a more efficient stack-based approach instead of queue + // This avoids the overhead of shift() operations + const stack = [{x, y}]; + + // Create a visited map to avoid checking the same pixel multiple times + const visited = new Set(); + const getKey = (x, y) => `${x},${y}`; + + while (stack.length > 0) { + const {x, y} = stack.pop(); + const key = getKey(x, y); + + // Skip if already visited + if (visited.has(key)) continue; + + // Mark as visited + visited.add(key); + + // Check if this pixel is the target color + if (this.getPixel(x, y) === targetColor) { + // Set the pixel to the fill color + this.drawPixel(x, y, fillColor); + + // Add adjacent pixels to the stack + // Check bounds before adding to avoid unnecessary getPixel calls + if (x > 0) stack.push({x: x - 1, y}); + if (x < this.width - 1) stack.push({x: x + 1, y}); + if (y > 0) stack.push({x, y: y - 1}); + if (y < this.height - 1) stack.push({x, y: y + 1}); + } + } + } + + /** + * Get the color of a pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @returns {string} Color in hex format + */ + getPixel(x, y) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + return this.pixels[index]; + } + return null; + } + + /** + * Clear the canvas + */ + clear() { + // Clear pixel data + this.pixels.fill('#000000'); + + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Clear UI canvas + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + } + + /** + * Render the canvas + */ + render() { + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Always use ImageData for rendering for consistent performance + const imageData = this.ctx.createImageData(this.width, this.height); + const data = imageData.data; + + for (let i = 0; i < this.pixels.length; i++) { + const color = this.pixels[i]; + // Parse hex color to RGB + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + + // Set RGBA values (4 bytes per pixel) + const pixelIndex = i * 4; + data[pixelIndex] = r; + data[pixelIndex + 1] = g; + data[pixelIndex + 2] = b; + data[pixelIndex + 3] = 255; // Alpha (fully opaque) + } + + // Create temporary canvas for efficient scaling + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.putImageData(imageData, 0, 0); + + // Draw scaled image + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage( + tempCanvas, + 0, 0, this.width, this.height, + 0, 0, this.canvas.width, this.canvas.height + ); + + // Draw grid if enabled + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Draw the grid + */ + drawGrid() { + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + + if (this.zoom >= 4) { + this.uiCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; + this.uiCtx.lineWidth = 1; + + // Use a single path for all grid lines for better performance + this.uiCtx.beginPath(); + + // Draw vertical lines + for (let x = 0; x <= this.width; x++) { + const xPos = x * this.pixelSize * this.zoom; + this.uiCtx.moveTo(xPos, 0); + this.uiCtx.lineTo(xPos, this.canvas.height); + } + + // Draw horizontal lines + for (let y = 0; y <= this.height; y++) { + const yPos = y * this.pixelSize * this.zoom; + this.uiCtx.moveTo(0, yPos); + this.uiCtx.lineTo(this.canvas.width, yPos); + } + + // Draw all lines at once + this.uiCtx.stroke(); + } + } + + /** + * Set the effects settings + * @param {Object} effects - Effects settings + */ + setEffects(effects) { + this.effects = {...this.effects, ...effects}; + } + + /** + * Animate the effects + */ + animateEffects() { + // Use a bound function to avoid creating a new function on each frame + if (!this._boundAnimateEffects) { + this._boundAnimateEffects = this.animateEffects.bind(this); + } + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Check if any effects are enabled + const hasEffects = + this.effects.grain || + this.effects.static || + this.effects.glitch || + this.effects.crt || + this.effects.scanLines || + this.effects.vignette || + this.effects.noise || + this.effects.pixelate; + + // Only apply effects if at least one is enabled + if (hasEffects) { + // Apply grain effect + if (this.effects.grain) { + this.applyGrainEffect(); + } + + // Apply static effect + if (this.effects.static) { + this.applyStaticEffect(); + } + + // Apply glitch effect + if (this.effects.glitch) { + this.applyGlitchEffect(); + } + + // Apply CRT effect + if (this.effects.crt) { + this.applyCRTEffect(); + } + + // Apply scan lines effect + if (this.effects.scanLines) { + this.applyScanLines(); + } + + // Apply vignette effect + if (this.effects.vignette) { + this.applyVignette(); + } + + // Apply noise effect + if (this.effects.noise) { + this.applyNoiseEffect(); + } + + // Apply pixelate effect + if (this.effects.pixelate) { + this.applyPixelateEffect(); + } + } + + // Request next frame + this.effectsAnimationFrame = requestAnimationFrame(this._boundAnimateEffects); + } + + /** + * Apply grain effect + */ + applyGrainEffect() { + const intensity = this.effects.intensity * 0.1; + + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + if (Math.random() < intensity) { + this.effectsCtx.fillRect( + x * this.pixelSize * this.zoom, + y * this.pixelSize * this.zoom, + this.pixelSize * this.zoom, + this.pixelSize * this.zoom + ); + } + } + } + } + + /** + * Apply static effect + */ + applyStaticEffect() { + const intensity = this.effects.intensity * 0.05; + + // Use a single path for better performance + this.effectsCtx.fillStyle = 'rgba(255, 255, 255, 0.1)'; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + // Only process every other line for the scan line effect + for (let y = 0; y < this.canvas.height; y += 2) { + if (Math.random() < intensity) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + } + + // Draw all static lines at once + this.effectsCtx.fill(); + } + + /** + * Apply glitch effect + */ + applyGlitchEffect() { + const intensity = this.effects.intensity; + + // Only apply glitch occasionally + if (Math.random() < intensity * 0.1) { + // Random offset for a few rows + const numRows = Math.floor(Math.random() * 5) + 1; + + for (let i = 0; i < numRows; i++) { + const y = Math.floor(Math.random() * this.height); + const offset = (Math.random() - 0.5) * 10 * intensity; + + // Create a temporary canvas to hold the row + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.pixelSize * this.zoom; + const tempCtx = tempCanvas.getContext('2d'); + + // Copy the row from the main canvas + tempCtx.drawImage( + this.canvas, + 0, y * this.pixelSize * this.zoom, + this.canvas.width, this.pixelSize * this.zoom, + 0, 0, + this.canvas.width, this.pixelSize * this.zoom + ); + + // Draw the row with offset + this.effectsCtx.drawImage( + tempCanvas, + offset * this.pixelSize * this.zoom, y * this.pixelSize * this.zoom + ); + } + } + } + + /** + * Apply CRT effect + */ + applyCRTEffect() { + const intensity = this.effects.intensity; + + // Scan lines + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.fillRect(0, y, this.canvas.width, 1); + } + + // Vignette + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity * 0.7})`); + + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply scan lines effect + */ + applyScanLines() { + const intensity = this.effects.intensity * 0.1; + + // Draw horizontal scan lines + this.effectsCtx.fillStyle = `rgba(0, 0, 0, ${intensity})`; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + + // Draw all scan lines at once + this.effectsCtx.fill(); + } + + /** + * Apply vignette effect + */ + applyVignette() { + const intensity = this.effects.intensity * 0.7; + + // Create radial gradient + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, Math.max(this.canvas.width, this.canvas.height) / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity})`); + + // Apply gradient + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply noise effect + */ + applyNoiseEffect() { + const intensity = this.effects.intensity * 0.05; + + // Create an ImageData object for better performance + const imageData = this.effectsCtx.createImageData(this.canvas.width, this.canvas.height); + const data = imageData.data; + + // Fill with noise + for (let i = 0; i < data.length; i += 4) { + if (Math.random() < intensity) { + // White noise pixel with 50% opacity + data[i] = 255; // R + data[i + 1] = 255; // G + data[i + 2] = 255; // B + data[i + 3] = 128; // A (50% opacity) + } + } + + // Put the image data back to the canvas + this.effectsCtx.putImageData(imageData, 0, 0); + } + + /** + * Apply pixelate effect + */ + applyPixelateEffect() { + const intensity = Math.max(1, Math.floor(this.effects.intensity * 10)); + + // Create a temporary canvas + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.canvas.height; + const tempCtx = tempCanvas.getContext('2d'); + + // Draw the main canvas to the temporary canvas + tempCtx.drawImage(this.canvas, 0, 0); + + // Clear the effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Draw pixelated version + for (let y = 0; y < this.canvas.height; y += intensity) { + for (let x = 0; x < this.canvas.width; x += intensity) { + // Get the color of the pixel at (x, y) + const pixelData = tempCtx.getImageData(x, y, 1, 1).data; + + // Set the fill style to the pixel color + this.effectsCtx.fillStyle = `rgba(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]}, 0.5)`; + + // Draw a rectangle with the size of the pixel block + this.effectsCtx.fillRect(x, y, intensity, intensity); + } + } + } + + /** + * Set canvas dimensions + * @param {number} width - Width in pixels + * @param {number} height - Height in pixels + */ + setDimensions(width, height) { + this.width = width; + this.height = height; + this.pixels = new Array(this.width * this.height).fill('#000000'); + this.initCanvas(); + } + + /** + * Zoom in + */ + zoomIn() { + if (this.zoom < 16) { + this.zoom *= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Zoom out + */ + zoomOut() { + if (this.zoom > 0.5) { + this.zoom /= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Get the current zoom level + * @returns {number} Zoom level + */ + getZoom() { + return this.zoom; + } + + /** + * Reset zoom to 100% + */ + resetZoom() { + this.zoom = 1; + this.initCanvas(); + this.render(); + } + + /** + * Toggle grid visibility + */ + toggleGrid() { + this.showGrid = !this.showGrid; + this.render(); + } + + /** + * Export the canvas as a PNG data URL + * @returns {string} PNG data URL + */ + exportToPNG() { + // Create a temporary canvas for export + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = this.width; + exportCanvas.height = this.height; + const exportCtx = exportCanvas.getContext('2d'); + + // Draw pixels at 1:1 scale + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + const index = y * this.width + x; + const color = this.pixels[index]; + + exportCtx.fillStyle = color; + exportCtx.fillRect(x, y, 1, 1); + } + } + + // Return data URL + return exportCanvas.toDataURL('image/png'); + } + + /** + * Get the pixel data as an array + * @returns {Array} Pixel data + */ + getPixelData() { + return [...this.pixels]; + } + + /** + * Set the pixel data from an array + * @param {Array} pixelData - Pixel data + * @param {boolean} saveHistory - Whether to save this change to history + */ + setPixelData(pixelData, saveHistory = true) { + if (pixelData.length === this.width * this.height) { + this.pixels = [...pixelData]; + if (saveHistory) { + this.saveToHistory(); + } + this.render(); + } + } + + /** + * Save the current state to history + */ + saveToHistory() { + // If we're not at the end of the history, remove future states + if (this.historyIndex < this.history.length - 1) { + this.history = this.history.slice(0, this.historyIndex + 1); + } + + // Add current state to history + this.history.push(this.getPixelData()); + this.historyIndex = this.history.length - 1; + + // Limit history size + if (this.history.length > this.maxHistorySize) { + this.history.shift(); + this.historyIndex--; + } + } + + /** + * Undo the last action + * @returns {boolean} Whether the undo was successful + */ + undo() { + if (this.historyIndex > 0) { + this.historyIndex--; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Redo the last undone action + * @returns {boolean} Whether the redo was successful + */ + redo() { + if (this.historyIndex < this.history.length - 1) { + this.historyIndex++; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Resize the canvas + * @param {number} width - New width in pixels + * @param {number} height - New height in pixels + * @param {boolean} preserveContent - Whether to preserve the current content + */ + resize(width, height, preserveContent = true) { + // Create a new pixel array + const newPixels = new Array(width * height).fill('#000000'); + + // Copy existing pixels if preserving content + if (preserveContent) { + const minWidth = Math.min(width, this.width); + const minHeight = Math.min(height, this.height); + + for (let y = 0; y < minHeight; y++) { + for (let x = 0; x < minWidth; x++) { + const oldIndex = y * this.width + x; + const newIndex = y * width + x; + newPixels[newIndex] = this.pixels[oldIndex]; + } + } + } + + // Update dimensions + this.width = width; + this.height = height; + this.pixels = newPixels; + + // Reinitialize the canvas + this.initCanvas(); + + // Save to history + this.saveToHistory(); + + // Render the canvas + this.render(); + } + + /** + * Clean up resources + * This should be called when the canvas is no longer needed + */ + cleanup() { + // Cancel animation frame if it exists + if (this.effectsAnimationFrame) { + cancelAnimationFrame(this.effectsAnimationFrame); + this.effectsAnimationFrame = null; + } + + // Remove event listeners using the bound handlers + this.canvas.removeEventListener('mousedown', this._boundHandleMouseDown); + document.removeEventListener('mousemove', this._boundHandleMouseMove); + document.removeEventListener('mouseup', this._boundHandleMouseUp); + this.canvas.removeEventListener('contextmenu', this._boundHandleContextMenu); + this.canvas.removeEventListener('mousemove', this._boundUpdateCursorPosition); + this.canvas.removeEventListener('mouseleave', this._boundHandleMouseLeave); + } +} diff --git a/.history/src/scripts/canvas/PixelCanvas_20250605031003.js b/.history/src/scripts/canvas/PixelCanvas_20250605031003.js new file mode 100644 index 0000000..2a45920 --- /dev/null +++ b/.history/src/scripts/canvas/PixelCanvas_20250605031003.js @@ -0,0 +1,999 @@ +/** + * PixelCanvas Class + * + * Handles the main drawing canvas, pixel manipulation, and rendering. + */ +class PixelCanvas { + /** + * Create a new PixelCanvas + * @param {Object} options - Configuration options + * @param {string} options.canvasId - ID of the main canvas element + * @param {string} options.effectsCanvasId - ID of the effects overlay canvas + * @param {string} options.uiCanvasId - ID of the UI overlay canvas + * @param {number} options.width - Width in pixels + * @param {number} options.height - Height in pixels + * @param {number} options.pixelSize - Size of each pixel in screen pixels + */ + constructor(options) { + // Canvas elements + this.canvas = document.getElementById(options.canvasId); + this.effectsCanvas = document.getElementById(options.effectsCanvasId); + this.uiCanvas = document.getElementById(options.uiCanvasId); + + // Canvas contexts + this.ctx = this.canvas.getContext('2d'); + this.effectsCtx = this.effectsCanvas.getContext('2d'); + this.uiCtx = this.uiCanvas.getContext('2d'); + + // Canvas dimensions + this.width = options.width || 64; + this.height = options.height || 64; + this.pixelSize = options.pixelSize || 8; + + // Zoom level + this.zoom = 1; + + // Pixel data + this.pixels = new Array(this.width * this.height).fill('#000000'); + + // Undo/Redo history + this.history = []; + this.historyIndex = -1; + this.maxHistorySize = 50; // Maximum number of states to store + + // Save initial state + this.saveToHistory(); + + // Effects settings + this.effects = { + grain: false, + static: false, + glitch: false, + crt: false, + scanLines: false, + vignette: false, + noise: false, + pixelate: false, + intensity: 0.5 + }; + + // Animation frame for effects + this.effectsAnimationFrame = null; + + // Mouse state + this.isDrawing = false; + this.lastX = 0; + this.lastY = 0; + + // Grid display + this.showGrid = true; + + // Initialize the canvas + this.initCanvas(); + + // Set up event listeners + this.setupEventListeners(); + + // Start the effects animation loop + this.animateEffects(); + } + + /** + * Initialize the canvas with the correct dimensions + */ + initCanvas() { + // Set canvas dimensions + this.canvas.width = this.width * this.pixelSize * this.zoom; + this.canvas.height = this.height * this.pixelSize * this.zoom; + + // Set effects canvas dimensions + this.effectsCanvas.width = this.canvas.width; + this.effectsCanvas.height = this.canvas.height; + + // Set UI canvas dimensions + this.uiCanvas.width = this.canvas.width; + this.uiCanvas.height = this.canvas.height; + + // Set rendering options for pixel art + this.ctx.imageSmoothingEnabled = false; + this.effectsCtx.imageSmoothingEnabled = false; + this.uiCtx.imageSmoothingEnabled = false; + + // Clear the canvas + this.clear(); + + // Draw the initial grid + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Set up event listeners for mouse/touch interaction + */ + setupEventListeners() { + // Bind event handlers to this instance + this._boundHandleMouseDown = this.handleMouseDown.bind(this); + this._boundHandleMouseMove = this.handleMouseMove.bind(this); + this._boundHandleMouseUp = this.handleMouseUp.bind(this); + this._boundUpdateCursorPosition = this.updateCursorPosition.bind(this); + this._boundHandleContextMenu = (e) => { e.preventDefault(); }; + this._boundHandleMouseLeave = () => { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + }; + + // Mouse events + this.canvas.addEventListener('mousedown', this._boundHandleMouseDown); + document.addEventListener('mousemove', this._boundHandleMouseMove); + document.addEventListener('mouseup', this._boundHandleMouseUp); + + // Prevent context menu on right-click + this.canvas.addEventListener('contextmenu', this._boundHandleContextMenu); + + // Update cursor position display + this.canvas.addEventListener('mousemove', this._boundUpdateCursorPosition); + + // Mouse leave + this.canvas.addEventListener('mouseleave', this._boundHandleMouseLeave); + } + + /** + * Handle mouse down event + * @param {MouseEvent} e - Mouse event + */ + handleMouseDown(e) { + this.isDrawing = true; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Store last position + this.lastX = x; + this.lastY = y; + + // Save the current state before drawing + this.saveToHistory(); + + // Draw a single pixel + this.drawPixel(x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Render the canvas + this.render(); + } + + /** + * Handle mouse move event + * @param {MouseEvent} e - Mouse event + */ + handleMouseMove(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Only draw if the position has changed + if (x !== this.lastX || y !== this.lastY) { + // Draw a line from last position to current position + this.drawLine(this.lastX, this.lastY, x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Update last position + this.lastX = x; + this.lastY = y; + + // Render the canvas + this.render(); + } + } + + /** + * Handle mouse up event + */ + handleMouseUp() { + this.isDrawing = false; + } + + /** + * Update the cursor position display + * @param {MouseEvent} e - Mouse event + */ + updateCursorPosition(e) { + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + document.getElementById('cursor-position').textContent = `X: ${x} Y: ${y}`; + } else { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + } + } + + /** + * Draw a single pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawPixel(x, y, color) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + this.pixels[index] = color; + } + } + + /** + * Draw a line using Bresenham's algorithm + * @param {number} x0 - Starting X coordinate + * @param {number} y0 - Starting Y coordinate + * @param {number} x1 - Ending X coordinate + * @param {number} y1 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawLine(x0, y0, x1, y1, color) { + const dx = Math.abs(x1 - x0); + const dy = Math.abs(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dx - dy; + + while (true) { + this.drawPixel(x0, y0, color); + + if (x0 === x1 && y0 === y1) break; + + const e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + /** + * Draw a rectangle + * @param {number} x - X coordinate of top-left corner + * @param {number} y - Y coordinate of top-left corner + * @param {number} width - Width of rectangle + * @param {number} height - Height of rectangle + * @param {string} color - Color in hex format + * @param {boolean} fill - Whether to fill the rectangle + */ + drawRect(x, y, width, height, color, fill = false) { + if (fill) { + for (let i = x; i < x + width; i++) { + for (let j = y; j < y + height; j++) { + this.drawPixel(i, j, color); + } + } + } else { + // Draw horizontal lines + for (let i = x; i < x + width; i++) { + this.drawPixel(i, y, color); + this.drawPixel(i, y + height - 1, color); + } + + // Draw vertical lines + for (let j = y; j < y + height; j++) { + this.drawPixel(x, j, color); + this.drawPixel(x + width - 1, j, color); + } + } + } + + /** + * Draw an ellipse + * @param {number} xc - X coordinate of center + * @param {number} yc - Y coordinate of center + * @param {number} a - Semi-major axis + * @param {number} b - Semi-minor axis + * @param {string} color - Color in hex format + */ + drawEllipse(xc, yc, a, b, color) { + let x = 0; + let y = b; + let a2 = a * a; + let b2 = b * b; + let d = b2 - a2 * b + a2 / 4; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + + while (a2 * (y - 0.5) > b2 * (x + 1)) { + if (d < 0) { + d += b2 * (2 * x + 3); + } else { + d += b2 * (2 * x + 3) + a2 * (-2 * y + 2); + y--; + } + x++; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + + d = b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2; + + while (y > 0) { + if (d < 0) { + d += b2 * (2 * x + 2) + a2 * (-2 * y + 3); + x++; + } else { + d += a2 * (-2 * y + 3); + } + y--; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + } + + /** + * Fill an area with a color (flood fill) + * @param {number} x - Starting X coordinate + * @param {number} y - Starting Y coordinate + * @param {string} fillColor - Color to fill with + */ + floodFill(x, y, fillColor) { + const targetColor = this.getPixel(x, y); + + // Don't fill if the target color is the same as the fill color + if (targetColor === fillColor) return; + + // Use a more efficient stack-based approach instead of queue + // This avoids the overhead of shift() operations + const stack = [{x, y}]; + + // Create a visited array for efficient tracking + const visited = new Uint8Array(this.width * this.height); + const index = y * this.width + x; + + // Check starting pixel + if (index < 0 || index >= visited.length) return; + + while (stack.length > 0) { + const {x, y} = stack.pop(); + const index = y * this.width + x; + + // Skip if already visited + if (visited[index]) continue; + + // Mark as visited + visited[index] = 1; + + // Check if this pixel is the target color + if (this.getPixel(x, y) === targetColor) { + // Set the pixel to the fill color + this.drawPixel(x, y, fillColor); + + // Add adjacent pixels to the stack + // Check bounds before adding to avoid unnecessary getPixel calls + if (x > 0) stack.push({x: x - 1, y}); + if (x < this.width - 1) stack.push({x: x + 1, y}); + if (y > 0) stack.push({x, y: y - 1}); + if (y < this.height - 1) stack.push({x, y: y + 1}); + } + } + } + + /** + * Get the color of a pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @returns {string} Color in hex format + */ + getPixel(x, y) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + return this.pixels[index]; + } + return null; + } + + /** + * Clear the canvas + */ + clear() { + // Clear pixel data + this.pixels.fill('#000000'); + + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Clear UI canvas + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + } + + /** + * Render the canvas + */ + render() { + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Always use ImageData for rendering for consistent performance + const imageData = this.ctx.createImageData(this.width, this.height); + const data = imageData.data; + + for (let i = 0; i < this.pixels.length; i++) { + const color = this.pixels[i]; + // Parse hex color to RGB + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + + // Set RGBA values (4 bytes per pixel) + const pixelIndex = i * 4; + data[pixelIndex] = r; + data[pixelIndex + 1] = g; + data[pixelIndex + 2] = b; + data[pixelIndex + 3] = 255; // Alpha (fully opaque) + } + + // Create temporary canvas for efficient scaling + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.putImageData(imageData, 0, 0); + + // Draw scaled image + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage( + tempCanvas, + 0, 0, this.width, this.height, + 0, 0, this.canvas.width, this.canvas.height + ); + + // Draw grid if enabled + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Draw the grid + */ + drawGrid() { + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + + if (this.zoom >= 4) { + this.uiCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; + this.uiCtx.lineWidth = 1; + + // Use a single path for all grid lines for better performance + this.uiCtx.beginPath(); + + // Draw vertical lines + for (let x = 0; x <= this.width; x++) { + const xPos = x * this.pixelSize * this.zoom; + this.uiCtx.moveTo(xPos, 0); + this.uiCtx.lineTo(xPos, this.canvas.height); + } + + // Draw horizontal lines + for (let y = 0; y <= this.height; y++) { + const yPos = y * this.pixelSize * this.zoom; + this.uiCtx.moveTo(0, yPos); + this.uiCtx.lineTo(this.canvas.width, yPos); + } + + // Draw all lines at once + this.uiCtx.stroke(); + } + } + + /** + * Set the effects settings + * @param {Object} effects - Effects settings + */ + setEffects(effects) { + this.effects = {...this.effects, ...effects}; + } + + /** + * Animate the effects + */ + animateEffects() { + // Use a bound function to avoid creating a new function on each frame + if (!this._boundAnimateEffects) { + this._boundAnimateEffects = this.animateEffects.bind(this); + } + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Check if any effects are enabled + const hasEffects = + this.effects.grain || + this.effects.static || + this.effects.glitch || + this.effects.crt || + this.effects.scanLines || + this.effects.vignette || + this.effects.noise || + this.effects.pixelate; + + // Only apply effects if at least one is enabled + if (hasEffects) { + // Apply grain effect + if (this.effects.grain) { + this.applyGrainEffect(); + } + + // Apply static effect + if (this.effects.static) { + this.applyStaticEffect(); + } + + // Apply glitch effect + if (this.effects.glitch) { + this.applyGlitchEffect(); + } + + // Apply CRT effect + if (this.effects.crt) { + this.applyCRTEffect(); + } + + // Apply scan lines effect + if (this.effects.scanLines) { + this.applyScanLines(); + } + + // Apply vignette effect + if (this.effects.vignette) { + this.applyVignette(); + } + + // Apply noise effect + if (this.effects.noise) { + this.applyNoiseEffect(); + } + + // Apply pixelate effect + if (this.effects.pixelate) { + this.applyPixelateEffect(); + } + } + + // Request next frame + this.effectsAnimationFrame = requestAnimationFrame(this._boundAnimateEffects); + } + + /** + * Apply grain effect + */ + applyGrainEffect() { + const intensity = this.effects.intensity * 0.1; + + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + if (Math.random() < intensity) { + this.effectsCtx.fillRect( + x * this.pixelSize * this.zoom, + y * this.pixelSize * this.zoom, + this.pixelSize * this.zoom, + this.pixelSize * this.zoom + ); + } + } + } + } + + /** + * Apply static effect + */ + applyStaticEffect() { + const intensity = this.effects.intensity * 0.05; + + // Use a single path for better performance + this.effectsCtx.fillStyle = 'rgba(255, 255, 255, 0.1)'; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + // Only process every other line for the scan line effect + for (let y = 0; y < this.canvas.height; y += 2) { + if (Math.random() < intensity) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + } + + // Draw all static lines at once + this.effectsCtx.fill(); + } + + /** + * Apply glitch effect + */ + applyGlitchEffect() { + const intensity = this.effects.intensity; + + // Only apply glitch occasionally + if (Math.random() < intensity * 0.1) { + // Random offset for a few rows + const numRows = Math.floor(Math.random() * 5) + 1; + + for (let i = 0; i < numRows; i++) { + const y = Math.floor(Math.random() * this.height); + const offset = (Math.random() - 0.5) * 10 * intensity; + + // Create a temporary canvas to hold the row + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.pixelSize * this.zoom; + const tempCtx = tempCanvas.getContext('2d'); + + // Copy the row from the main canvas + tempCtx.drawImage( + this.canvas, + 0, y * this.pixelSize * this.zoom, + this.canvas.width, this.pixelSize * this.zoom, + 0, 0, + this.canvas.width, this.pixelSize * this.zoom + ); + + // Draw the row with offset + this.effectsCtx.drawImage( + tempCanvas, + offset * this.pixelSize * this.zoom, y * this.pixelSize * this.zoom + ); + } + } + } + + /** + * Apply CRT effect + */ + applyCRTEffect() { + const intensity = this.effects.intensity; + + // Scan lines + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.fillRect(0, y, this.canvas.width, 1); + } + + // Vignette + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity * 0.7})`); + + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply scan lines effect + */ + applyScanLines() { + const intensity = this.effects.intensity * 0.1; + + // Draw horizontal scan lines + this.effectsCtx.fillStyle = `rgba(0, 0, 0, ${intensity})`; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + + // Draw all scan lines at once + this.effectsCtx.fill(); + } + + /** + * Apply vignette effect + */ + applyVignette() { + const intensity = this.effects.intensity * 0.7; + + // Create radial gradient + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, Math.max(this.canvas.width, this.canvas.height) / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity})`); + + // Apply gradient + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply noise effect + */ + applyNoiseEffect() { + const intensity = this.effects.intensity * 0.05; + + // Create an ImageData object for better performance + const imageData = this.effectsCtx.createImageData(this.canvas.width, this.canvas.height); + const data = imageData.data; + + // Fill with noise + for (let i = 0; i < data.length; i += 4) { + if (Math.random() < intensity) { + // White noise pixel with 50% opacity + data[i] = 255; // R + data[i + 1] = 255; // G + data[i + 2] = 255; // B + data[i + 3] = 128; // A (50% opacity) + } + } + + // Put the image data back to the canvas + this.effectsCtx.putImageData(imageData, 0, 0); + } + + /** + * Apply pixelate effect + */ + applyPixelateEffect() { + const intensity = Math.max(1, Math.floor(this.effects.intensity * 10)); + + // Create a temporary canvas + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.canvas.height; + const tempCtx = tempCanvas.getContext('2d'); + + // Draw the main canvas to the temporary canvas + tempCtx.drawImage(this.canvas, 0, 0); + + // Clear the effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Draw pixelated version + for (let y = 0; y < this.canvas.height; y += intensity) { + for (let x = 0; x < this.canvas.width; x += intensity) { + // Get the color of the pixel at (x, y) + const pixelData = tempCtx.getImageData(x, y, 1, 1).data; + + // Set the fill style to the pixel color + this.effectsCtx.fillStyle = `rgba(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]}, 0.5)`; + + // Draw a rectangle with the size of the pixel block + this.effectsCtx.fillRect(x, y, intensity, intensity); + } + } + } + + /** + * Set canvas dimensions + * @param {number} width - Width in pixels + * @param {number} height - Height in pixels + */ + setDimensions(width, height) { + this.width = width; + this.height = height; + this.pixels = new Array(this.width * this.height).fill('#000000'); + this.initCanvas(); + } + + /** + * Zoom in + */ + zoomIn() { + if (this.zoom < 16) { + this.zoom *= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Zoom out + */ + zoomOut() { + if (this.zoom > 0.5) { + this.zoom /= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Get the current zoom level + * @returns {number} Zoom level + */ + getZoom() { + return this.zoom; + } + + /** + * Reset zoom to 100% + */ + resetZoom() { + this.zoom = 1; + this.initCanvas(); + this.render(); + } + + /** + * Toggle grid visibility + */ + toggleGrid() { + this.showGrid = !this.showGrid; + this.render(); + } + + /** + * Export the canvas as a PNG data URL + * @returns {string} PNG data URL + */ + exportToPNG() { + // Create a temporary canvas for export + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = this.width; + exportCanvas.height = this.height; + const exportCtx = exportCanvas.getContext('2d'); + + // Draw pixels at 1:1 scale + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + const index = y * this.width + x; + const color = this.pixels[index]; + + exportCtx.fillStyle = color; + exportCtx.fillRect(x, y, 1, 1); + } + } + + // Return data URL + return exportCanvas.toDataURL('image/png'); + } + + /** + * Get the pixel data as an array + * @returns {Array} Pixel data + */ + getPixelData() { + return [...this.pixels]; + } + + /** + * Set the pixel data from an array + * @param {Array} pixelData - Pixel data + * @param {boolean} saveHistory - Whether to save this change to history + */ + setPixelData(pixelData, saveHistory = true) { + if (pixelData.length === this.width * this.height) { + this.pixels = [...pixelData]; + if (saveHistory) { + this.saveToHistory(); + } + this.render(); + } + } + + /** + * Save the current state to history + */ + saveToHistory() { + // If we're not at the end of the history, remove future states + if (this.historyIndex < this.history.length - 1) { + this.history = this.history.slice(0, this.historyIndex + 1); + } + + // Add current state to history + this.history.push(this.getPixelData()); + this.historyIndex = this.history.length - 1; + + // Limit history size + if (this.history.length > this.maxHistorySize) { + this.history.shift(); + this.historyIndex--; + } + } + + /** + * Undo the last action + * @returns {boolean} Whether the undo was successful + */ + undo() { + if (this.historyIndex > 0) { + this.historyIndex--; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Redo the last undone action + * @returns {boolean} Whether the redo was successful + */ + redo() { + if (this.historyIndex < this.history.length - 1) { + this.historyIndex++; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Resize the canvas + * @param {number} width - New width in pixels + * @param {number} height - New height in pixels + * @param {boolean} preserveContent - Whether to preserve the current content + */ + resize(width, height, preserveContent = true) { + // Create a new pixel array + const newPixels = new Array(width * height).fill('#000000'); + + // Copy existing pixels if preserving content + if (preserveContent) { + const minWidth = Math.min(width, this.width); + const minHeight = Math.min(height, this.height); + + for (let y = 0; y < minHeight; y++) { + for (let x = 0; x < minWidth; x++) { + const oldIndex = y * this.width + x; + const newIndex = y * width + x; + newPixels[newIndex] = this.pixels[oldIndex]; + } + } + } + + // Update dimensions + this.width = width; + this.height = height; + this.pixels = newPixels; + + // Reinitialize the canvas + this.initCanvas(); + + // Save to history + this.saveToHistory(); + + // Render the canvas + this.render(); + } + + /** + * Clean up resources + * This should be called when the canvas is no longer needed + */ + cleanup() { + // Cancel animation frame if it exists + if (this.effectsAnimationFrame) { + cancelAnimationFrame(this.effectsAnimationFrame); + this.effectsAnimationFrame = null; + } + + // Remove event listeners using the bound handlers + this.canvas.removeEventListener('mousedown', this._boundHandleMouseDown); + document.removeEventListener('mousemove', this._boundHandleMouseMove); + document.removeEventListener('mouseup', this._boundHandleMouseUp); + this.canvas.removeEventListener('contextmenu', this._boundHandleContextMenu); + this.canvas.removeEventListener('mousemove', this._boundUpdateCursorPosition); + this.canvas.removeEventListener('mouseleave', this._boundHandleMouseLeave); + } +} diff --git a/src/scripts/app.js b/src/scripts/app.js index ec1b229..dd777c4 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -511,17 +511,14 @@ document.addEventListener('DOMContentLoaded', () => { { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, { width: 120, height: 60, name: '120×60', description: 'Small banner' }, { width: 120, height: 80, name: '120×80', description: 'Small animation' }, - { width: 350, height: 350, name: '350×350', description: 'Medium square' }, - { width: 800, height: 500, name: '800×500', description: 'Large landscape' }, - { width: 900, height: 900, name: '900×900', description: 'Large square' } + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } ]; // Create HTML for size options with silhouettes let sizesHTML = '
'; canvasSizes.forEach(size => { - // We don't need preview width/height anymore since we're using fixed size preview boxes - // Calculate silhouette dimensions to match aspect ratio let silhouetteWidth, silhouetteHeight; @@ -579,89 +576,91 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // Add some CSS for the size selection dialog - const style = document.createElement('style'); - style.textContent = ` - .modal-dialog { - width: 600px !important; - height: 500px !important; - max-width: 80% !important; - max-height: 80% !important; - } - - .modal-body { - max-height: 400px; - overflow-y: auto; - padding-right: 10px; - } + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } - .canvas-size-options { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 15px; - margin-bottom: 20px; - } + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } - .canvas-size-option { - display: flex; - flex-direction: column; - align-items: center; - padding: 15px; - border: 1px solid var(--panel-border); - background-color: var(--button-bg); - cursor: pointer; - transition: all 0.2s ease; - } + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } - .canvas-size-option:hover { - background-color: var(--button-hover); - transform: translateY(-2px); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - } + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } - .canvas-size-preview { - position: relative; - background-color: #000; - margin-bottom: 10px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--panel-border); - width: 120px; - height: 120px; - } + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } - .canvas-size-silhouette { - position: absolute; - background-color: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.3); - } + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } - .canvas-size-info { - text-align: center; - width: 100%; - } + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } - .canvas-size-name { - font-weight: bold; - margin-bottom: 5px; - color: var(--highlight-color); - text-shadow: var(--text-glow); - } + .canvas-size-info { + text-align: center; + width: 100%; + } - .canvas-size-description { - font-size: 12px; - color: var(--secondary-color); - } + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } - /* Make the modal dialog more square/rectangular */ - #modal-container { - display: flex; - justify-content: center; - align-items: center; - } - `; + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } - document.head.appendChild(style); + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } } }); diff --git a/src/scripts/canvas/PixelCanvas.js b/src/scripts/canvas/PixelCanvas.js index 3fac938..2a45920 100644 --- a/src/scripts/canvas/PixelCanvas.js +++ b/src/scripts/canvas/PixelCanvas.js @@ -357,19 +357,22 @@ class PixelCanvas { // This avoids the overhead of shift() operations const stack = [{x, y}]; - // Create a visited map to avoid checking the same pixel multiple times - const visited = new Set(); - const getKey = (x, y) => `${x},${y}`; + // Create a visited array for efficient tracking + const visited = new Uint8Array(this.width * this.height); + const index = y * this.width + x; + + // Check starting pixel + if (index < 0 || index >= visited.length) return; while (stack.length > 0) { const {x, y} = stack.pop(); - const key = getKey(x, y); + const index = y * this.width + x; // Skip if already visited - if (visited.has(key)) continue; + if (visited[index]) continue; // Mark as visited - visited.add(key); + visited[index] = 1; // Check if this pixel is the target color if (this.getPixel(x, y) === targetColor) { @@ -426,46 +429,40 @@ class PixelCanvas { this.ctx.fillStyle = '#000000'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - // Use a more efficient rendering approach for larger canvases - if (this.pixelSize * this.zoom === 1) { - // For 1:1 pixel rendering, use ImageData for better performance - const imageData = this.ctx.createImageData(this.width, this.height); - const data = imageData.data; - - for (let i = 0; i < this.pixels.length; i++) { - const color = this.pixels[i]; - // Parse hex color to RGB - const r = parseInt(color.slice(1, 3), 16); - const g = parseInt(color.slice(3, 5), 16); - const b = parseInt(color.slice(5, 7), 16); - - // Set RGBA values (4 bytes per pixel) - const pixelIndex = i * 4; - data[pixelIndex] = r; - data[pixelIndex + 1] = g; - data[pixelIndex + 2] = b; - data[pixelIndex + 3] = 255; // Alpha (fully opaque) - } + // Always use ImageData for rendering for consistent performance + const imageData = this.ctx.createImageData(this.width, this.height); + const data = imageData.data; - this.ctx.putImageData(imageData, 0, 0); - } else { - // For scaled rendering, use the existing approach - for (let y = 0; y < this.height; y++) { - for (let x = 0; x < this.width; x++) { - const index = y * this.width + x; - const color = this.pixels[index]; - - this.ctx.fillStyle = color; - this.ctx.fillRect( - x * this.pixelSize * this.zoom, - y * this.pixelSize * this.zoom, - this.pixelSize * this.zoom, - this.pixelSize * this.zoom - ); - } - } + for (let i = 0; i < this.pixels.length; i++) { + const color = this.pixels[i]; + // Parse hex color to RGB + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + + // Set RGBA values (4 bytes per pixel) + const pixelIndex = i * 4; + data[pixelIndex] = r; + data[pixelIndex + 1] = g; + data[pixelIndex + 2] = b; + data[pixelIndex + 3] = 255; // Alpha (fully opaque) } + // Create temporary canvas for efficient scaling + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.putImageData(imageData, 0, 0); + + // Draw scaled image + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage( + tempCanvas, + 0, 0, this.width, this.height, + 0, 0, this.canvas.width, this.canvas.height + ); + // Draw grid if enabled if (this.showGrid) { this.drawGrid(); From 33f9651eba3a3c91917eef2f127e8a0e6b6a3748 Mon Sep 17 00:00:00 2001 From: "deepsource-io[bot]" <42547082+deepsource-io[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 08:02:23 +0000 Subject: [PATCH 4/9] ci: add .deepsource.toml --- .deepsource.toml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..b89c880 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,31 @@ +version = 1 + +[[analyzers]] +name = "javascript" + + [analyzers.meta] + plugins = [ + "react", + "vue", + "ember", + "meteor", + "angularjs", + "angular" + ] + environment = [ + "nodejs", + "browser", + "jest", + "mocha", + "jasmine", + "vitest", + "cypress", + "mongo", + "jquery" + ] + +[[transformers]] +name = "prettier" + +[[transformers]] +name = "standardjs" \ No newline at end of file From 814a7ee58d8a60f346a84ab1eeb70b1ce26f5e0a Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 08:17:21 +0000 Subject: [PATCH 5/9] refactor: convert logical operator to optional chainining The [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator can be used to perform null checks before accessing a property, or calling a function. --- .history/src/scripts/ui/MenuSystem_20250519200450.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519200503.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519200552.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519200620.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519200704.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519222623.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519222755.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519222812.js | 2 +- .history/src/scripts/ui/MenuSystem_20250519222827.js | 2 +- src/scripts/ui/MenuSystem.js | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.history/src/scripts/ui/MenuSystem_20250519200450.js b/.history/src/scripts/ui/MenuSystem_20250519200450.js index b5219fe..26945bb 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200450.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200450.js @@ -336,7 +336,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200503.js b/.history/src/scripts/ui/MenuSystem_20250519200503.js index b5219fe..26945bb 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200503.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200503.js @@ -336,7 +336,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200552.js b/.history/src/scripts/ui/MenuSystem_20250519200552.js index e4e2801..32a2273 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200552.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200552.js @@ -341,7 +341,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200620.js b/.history/src/scripts/ui/MenuSystem_20250519200620.js index cd48cfa..58b2cbc 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200620.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200620.js @@ -343,7 +343,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200704.js b/.history/src/scripts/ui/MenuSystem_20250519200704.js index aa64f6c..feeeacc 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200704.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200704.js @@ -344,7 +344,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222623.js b/.history/src/scripts/ui/MenuSystem_20250519222623.js index 55710c6..bcb4a4c 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222623.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222623.js @@ -347,7 +347,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222755.js b/.history/src/scripts/ui/MenuSystem_20250519222755.js index 0e394c7..74cffa7 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222755.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222755.js @@ -351,7 +351,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222812.js b/.history/src/scripts/ui/MenuSystem_20250519222812.js index db0bc6a..ae3fc49 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222812.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222812.js @@ -355,7 +355,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222827.js b/.history/src/scripts/ui/MenuSystem_20250519222827.js index 7e33967..8c1bbe7 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222827.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222827.js @@ -359,7 +359,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } diff --git a/src/scripts/ui/MenuSystem.js b/src/scripts/ui/MenuSystem.js index 7e33967..8c1bbe7 100644 --- a/src/scripts/ui/MenuSystem.js +++ b/src/scripts/ui/MenuSystem.js @@ -359,7 +359,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } From ee74d0becacbdc9286f02234d605119862bc07c3 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 08:17:39 +0000 Subject: [PATCH 6/9] style: format code with Prettier and StandardJS This commit fixes the style issues introduced in 814a7ee according to the output from Prettier and StandardJS. Details: https://github.com/numbpill3d/conjuration/pull/2 --- .../scripts/ui/MenuSystem_20250519200450.js | 146 ++++----- .../scripts/ui/MenuSystem_20250519200503.js | 146 ++++----- .../scripts/ui/MenuSystem_20250519200552.js | 150 ++++----- .../scripts/ui/MenuSystem_20250519200620.js | 150 ++++----- .../scripts/ui/MenuSystem_20250519200704.js | 152 ++++----- .../scripts/ui/MenuSystem_20250519222623.js | 154 ++++----- .../scripts/ui/MenuSystem_20250519222755.js | 156 +++++----- .../scripts/ui/MenuSystem_20250519222812.js | 158 +++++----- .../scripts/ui/MenuSystem_20250519222827.js | 162 +++++----- src/scripts/ui/MenuSystem.js | 292 +++++++++--------- 10 files changed, 863 insertions(+), 803 deletions(-) diff --git a/.history/src/scripts/ui/MenuSystem_20250519200450.js b/.history/src/scripts/ui/MenuSystem_20250519200450.js index 26945bb..a63473c 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200450.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200450.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,10 +162,10 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; + menu.style.display = "flex"; // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -176,10 +182,10 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; + menu.style.display = "none"; // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -189,12 +195,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -226,34 +232,34 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -268,11 +274,11 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this.hideContextMenu); + document.addEventListener("click", this.hideContextMenu); }, 0); } @@ -280,14 +286,14 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener - document.removeEventListener('click', this.hideContextMenu); + document.removeEventListener("click", this.hideContextMenu); } /** @@ -302,21 +308,21 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -351,9 +357,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200503.js b/.history/src/scripts/ui/MenuSystem_20250519200503.js index 26945bb..a63473c 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200503.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200503.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,10 +162,10 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; + menu.style.display = "flex"; // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -176,10 +182,10 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; + menu.style.display = "none"; // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -189,12 +195,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -226,34 +232,34 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -268,11 +274,11 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this.hideContextMenu); + document.addEventListener("click", this.hideContextMenu); }, 0); } @@ -280,14 +286,14 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener - document.removeEventListener('click', this.hideContextMenu); + document.removeEventListener("click", this.hideContextMenu); } /** @@ -302,21 +308,21 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -351,9 +357,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200552.js b/.history/src/scripts/ui/MenuSystem_20250519200552.js index 32a2273..5300bc6 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200552.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200552.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,10 +162,10 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; + menu.style.display = "flex"; // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -176,10 +182,10 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; + menu.style.display = "none"; // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -189,12 +195,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -226,36 +232,36 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.className = "menu-dropdown"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -270,14 +276,14 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Bind the hideContextMenu method to this instance this._boundHideContextMenu = this.hideContextMenu.bind(this); // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); + document.addEventListener("click", this._boundHideContextMenu); }, 0); } @@ -285,14 +291,14 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener - document.removeEventListener('click', this.hideContextMenu); + document.removeEventListener("click", this.hideContextMenu); } /** @@ -307,21 +313,21 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -356,9 +362,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200620.js b/.history/src/scripts/ui/MenuSystem_20250519200620.js index 58b2cbc..fad9272 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200620.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200620.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,10 +162,10 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; + menu.style.display = "flex"; // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -176,10 +182,10 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; + menu.style.display = "none"; // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -189,12 +195,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -226,36 +232,36 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.className = "menu-dropdown"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -270,14 +276,14 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Bind the hideContextMenu method to this instance this._boundHideContextMenu = this.hideContextMenu.bind(this); // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); + document.addEventListener("click", this._boundHideContextMenu); }, 0); } @@ -285,15 +291,15 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener using the bound method if (this._boundHideContextMenu) { - document.removeEventListener('click', this._boundHideContextMenu); + document.removeEventListener("click", this._boundHideContextMenu); } } @@ -309,21 +315,21 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -358,9 +364,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519200704.js b/.history/src/scripts/ui/MenuSystem_20250519200704.js index feeeacc..fd72d7d 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519200704.js +++ b/.history/src/scripts/ui/MenuSystem_20250519200704.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,10 +162,10 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; + menu.style.display = "flex"; // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -176,10 +182,10 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; + menu.style.display = "none"; // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -189,12 +195,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -226,36 +232,36 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.className = "menu-dropdown"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -270,14 +276,14 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Bind the hideContextMenu method to this instance this._boundHideContextMenu = this.hideContextMenu.bind(this); // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); + document.addEventListener("click", this._boundHideContextMenu); }, 0); } @@ -285,15 +291,15 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener using the bound method if (this._boundHideContextMenu) { - document.removeEventListener('click', this._boundHideContextMenu); + document.removeEventListener("click", this._boundHideContextMenu); } } @@ -309,22 +315,22 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; - menuItem.type = 'button'; // Add type attribute + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -359,9 +365,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222623.js b/.history/src/scripts/ui/MenuSystem_20250519222623.js index bcb4a4c..a47ae80 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222623.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222623.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,10 +162,10 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; + menu.style.display = "flex"; // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -176,10 +182,10 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; + menu.style.display = "none"; // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -189,12 +195,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -213,7 +219,7 @@ class MenuSystem { menu.style.top = `${buttonRect.bottom}px`; // Ensure the menu is visible by setting a high z-index - menu.style.zIndex = '2000'; + menu.style.zIndex = "2000"; } /** @@ -229,36 +235,36 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.className = "menu-dropdown"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -273,14 +279,14 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Bind the hideContextMenu method to this instance this._boundHideContextMenu = this.hideContextMenu.bind(this); // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); + document.addEventListener("click", this._boundHideContextMenu); }, 0); } @@ -288,15 +294,15 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener using the bound method if (this._boundHideContextMenu) { - document.removeEventListener('click', this._boundHideContextMenu); + document.removeEventListener("click", this._boundHideContextMenu); } } @@ -312,22 +318,22 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; - menuItem.type = 'button'; // Add type attribute + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -362,9 +368,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222755.js b/.history/src/scripts/ui/MenuSystem_20250519222755.js index 74cffa7..e6a70cc 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222755.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222755.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,11 +162,11 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; - menu.classList.add('visible'); + menu.style.display = "flex"; + menu.classList.add("visible"); // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -180,10 +186,10 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; + menu.style.display = "none"; // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -193,12 +199,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -217,7 +223,7 @@ class MenuSystem { menu.style.top = `${buttonRect.bottom}px`; // Ensure the menu is visible by setting a high z-index - menu.style.zIndex = '2000'; + menu.style.zIndex = "2000"; } /** @@ -233,36 +239,36 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.className = "menu-dropdown"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -277,14 +283,14 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Bind the hideContextMenu method to this instance this._boundHideContextMenu = this.hideContextMenu.bind(this); // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); + document.addEventListener("click", this._boundHideContextMenu); }, 0); } @@ -292,15 +298,15 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener using the bound method if (this._boundHideContextMenu) { - document.removeEventListener('click', this._boundHideContextMenu); + document.removeEventListener("click", this._boundHideContextMenu); } } @@ -316,22 +322,22 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; - menuItem.type = 'button'; // Add type attribute + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -366,9 +372,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222812.js b/.history/src/scripts/ui/MenuSystem_20250519222812.js index ae3fc49..0f9a38d 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222812.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222812.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,11 +162,11 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; - menu.classList.add('visible'); + menu.style.display = "flex"; + menu.classList.add("visible"); // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -180,11 +186,11 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; - menu.classList.remove('visible'); + menu.style.display = "none"; + menu.classList.remove("visible"); // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -197,12 +203,12 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; @@ -221,7 +227,7 @@ class MenuSystem { menu.style.top = `${buttonRect.bottom}px`; // Ensure the menu is visible by setting a high z-index - menu.style.zIndex = '2000'; + menu.style.zIndex = "2000"; } /** @@ -237,36 +243,36 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.className = "menu-dropdown"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -281,14 +287,14 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Bind the hideContextMenu method to this instance this._boundHideContextMenu = this.hideContextMenu.bind(this); // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); + document.addEventListener("click", this._boundHideContextMenu); }, 0); } @@ -296,15 +302,15 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener using the bound method if (this._boundHideContextMenu) { - document.removeEventListener('click', this._boundHideContextMenu); + document.removeEventListener("click", this._boundHideContextMenu); } } @@ -320,22 +326,22 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; - menuItem.type = 'button'; // Add type attribute + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -370,9 +376,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/.history/src/scripts/ui/MenuSystem_20250519222827.js b/.history/src/scripts/ui/MenuSystem_20250519222827.js index 8c1bbe7..b3d89d0 100644 --- a/.history/src/scripts/ui/MenuSystem_20250519222827.js +++ b/.history/src/scripts/ui/MenuSystem_20250519222827.js @@ -9,8 +9,8 @@ class MenuSystem { */ constructor() { this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + this.menuButtons = document.querySelectorAll(".menu-button"); + this.menuDropdowns = document.querySelectorAll(".menu-dropdown"); // Set up event listeners this.setupEventListeners(); @@ -21,15 +21,18 @@ class MenuSystem { */ setupEventListeners() { // Close menus when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { + document.addEventListener("click", (e) => { + if ( + !e.target.closest(".menu-button") && + !e.target.closest(".menu-dropdown") + ) { this.closeAllMenus(); } }); // Close menus when pressing Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { this.closeAllMenus(); } }); @@ -42,79 +45,82 @@ class MenuSystem { * Set up keyboard shortcuts */ setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Only handle shortcuts when not in an input field - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { return; } // Ctrl+N: New Project - if (e.ctrlKey && e.key === 'n') { + if (e.ctrlKey && e.key === "n") { e.preventDefault(); - document.getElementById('new-project').click(); + document.getElementById("new-project").click(); } // Ctrl+O: Open Project - if (e.ctrlKey && e.key === 'o') { + if (e.ctrlKey && e.key === "o") { e.preventDefault(); - document.getElementById('open-project').click(); + document.getElementById("open-project").click(); } // Ctrl+S: Save Project - if (e.ctrlKey && e.key === 's' && !e.shiftKey) { + if (e.ctrlKey && e.key === "s" && !e.shiftKey) { e.preventDefault(); - document.getElementById('save-project').click(); + document.getElementById("save-project").click(); } // Ctrl+Shift+S: Save Project As - if (e.ctrlKey && e.shiftKey && e.key === 'S') { + if (e.ctrlKey && e.shiftKey && e.key === "S") { e.preventDefault(); - document.getElementById('save-project-as').click(); + document.getElementById("save-project-as").click(); } // Ctrl+Z: Undo - if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { + if (e.ctrlKey && e.key === "z" && !e.shiftKey) { e.preventDefault(); - document.getElementById('undo').click(); + document.getElementById("undo").click(); } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { + if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { e.preventDefault(); - document.getElementById('redo').click(); + document.getElementById("redo").click(); } // Ctrl+G: Toggle Grid - if (e.ctrlKey && e.key === 'g') { + if (e.ctrlKey && e.key === "g") { e.preventDefault(); - document.getElementById('toggle-grid').click(); + document.getElementById("toggle-grid").click(); } // Tool shortcuts switch (e.key.toLowerCase()) { - case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); + case "b": // Pencil Tool + document.getElementById("brush-pencil").click(); break; - case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); + case "e": // Eraser Tool + document.getElementById("brush-eraser").click(); break; - case 'f': // Fill Tool - document.getElementById('brush-fill').click(); + case "f": // Fill Tool + document.getElementById("brush-fill").click(); break; - case 'l': // Line Tool - document.getElementById('brush-line').click(); + case "l": // Line Tool + document.getElementById("brush-line").click(); break; - case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); + case "r": // Rectangle Tool + document.getElementById("brush-rect").click(); break; - case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); + case "o": // Ellipse Tool + document.getElementById("brush-ellipse").click(); break; - case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); + case "g": // Glitch Tool + document.getElementById("brush-glitch").click(); break; - case 's': // Static Tool - document.getElementById('brush-static').click(); + case "s": // Static Tool + document.getElementById("brush-static").click(); break; } }); @@ -156,11 +162,11 @@ class MenuSystem { this.positionMenu(menu, button); // Show the menu - menu.style.display = 'flex'; - menu.classList.add('visible'); + menu.style.display = "flex"; + menu.classList.add("visible"); // Add active class to the button - button.classList.add('active'); + button.classList.add("active"); // Set as active menu this.activeMenu = menuId; @@ -180,11 +186,11 @@ class MenuSystem { if (!menu || !button) return; // Hide the menu - menu.style.display = 'none'; - menu.classList.remove('visible'); + menu.style.display = "none"; + menu.classList.remove("visible"); // Remove active class from the button - button.classList.remove('active'); + button.classList.remove("active"); // Clear active menu this.activeMenu = null; @@ -197,19 +203,19 @@ class MenuSystem { * Close all menus */ closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; - menu.classList.remove('visible'); + this.menuDropdowns.forEach((menu) => { + menu.style.display = "none"; + menu.classList.remove("visible"); }); - this.menuButtons.forEach(button => { - button.classList.remove('active'); + this.menuButtons.forEach((button) => { + button.classList.remove("active"); }); this.activeMenu = null; // Log for debugging - console.log('Closing all menus'); + console.log("Closing all menus"); } /** @@ -225,7 +231,7 @@ class MenuSystem { menu.style.top = `${buttonRect.bottom}px`; // Ensure the menu is visible by setting a high z-index - menu.style.zIndex = '2000'; + menu.style.zIndex = "2000"; } /** @@ -241,36 +247,36 @@ class MenuSystem { this.closeAllMenus(); // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById("context-menu"); if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; + contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.className = "menu-dropdown"; document.body.appendChild(contextMenu); } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = ""; // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; + const separator = document.createElement("div"); + separator.className = "menu-separator"; contextMenu.appendChild(separator); } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', () => { + menuItem.addEventListener("click", () => { this.hideContextMenu(); if (item.action) item.action(); }); @@ -285,14 +291,14 @@ class MenuSystem { contextMenu.style.top = `${e.clientY}px`; // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = "flex"; // Bind the hideContextMenu method to this instance this._boundHideContextMenu = this.hideContextMenu.bind(this); // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); + document.addEventListener("click", this._boundHideContextMenu); }, 0); } @@ -300,15 +306,15 @@ class MenuSystem { * Hide the context menu */ hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + const contextMenu = document.getElementById("context-menu"); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } // Remove the event listener using the bound method if (this._boundHideContextMenu) { - document.removeEventListener('click', this._boundHideContextMenu); + document.removeEventListener("click", this._boundHideContextMenu); } } @@ -324,22 +330,22 @@ class MenuSystem { if (!menu) return; // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; + const menuItem = document.createElement("button"); + menuItem.className = "menu-item"; menuItem.id = item.id; - menuItem.type = 'button'; // Add type attribute + menuItem.type = "button"; // Add type attribute menuItem.textContent = item.label; if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener("click", item.action); } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; + const shortcutSpan = document.createElement("span"); + shortcutSpan.className = "menu-shortcut"; shortcutSpan.textContent = item.shortcut; menuItem.appendChild(shortcutSpan); } @@ -374,9 +380,9 @@ class MenuSystem { if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove("disabled"); } else { - menuItem.classList.add('disabled'); + menuItem.classList.add("disabled"); } } } diff --git a/src/scripts/ui/MenuSystem.js b/src/scripts/ui/MenuSystem.js index 8c1bbe7..26701f9 100644 --- a/src/scripts/ui/MenuSystem.js +++ b/src/scripts/ui/MenuSystem.js @@ -7,209 +7,215 @@ class MenuSystem { /** * Create a new MenuSystem */ - constructor() { - this.activeMenu = null; - this.menuButtons = document.querySelectorAll('.menu-button'); - this.menuDropdowns = document.querySelectorAll('.menu-dropdown'); + constructor () { + this.activeMenu = null + this.menuButtons = document.querySelectorAll('.menu-button') + this.menuDropdowns = document.querySelectorAll('.menu-dropdown') // Set up event listeners - this.setupEventListeners(); + this.setupEventListeners() } /** * Set up event listeners */ - setupEventListeners() { + setupEventListeners () { // Close menus when clicking outside document.addEventListener('click', (e) => { - if (!e.target.closest('.menu-button') && !e.target.closest('.menu-dropdown')) { - this.closeAllMenus(); + if ( + !e.target.closest('.menu-button') && + !e.target.closest('.menu-dropdown') + ) { + this.closeAllMenus() } - }); + }) // Close menus when pressing Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { - this.closeAllMenus(); + this.closeAllMenus() } - }); + }) // Set up keyboard shortcuts - this.setupKeyboardShortcuts(); + this.setupKeyboardShortcuts() } /** * Set up keyboard shortcuts */ - setupKeyboardShortcuts() { + setupKeyboardShortcuts () { document.addEventListener('keydown', (e) => { // Only handle shortcuts when not in an input field if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { - return; + return } // Ctrl+N: New Project if (e.ctrlKey && e.key === 'n') { - e.preventDefault(); - document.getElementById('new-project').click(); + e.preventDefault() + document.getElementById('new-project').click() } // Ctrl+O: Open Project if (e.ctrlKey && e.key === 'o') { - e.preventDefault(); - document.getElementById('open-project').click(); + e.preventDefault() + document.getElementById('open-project').click() } // Ctrl+S: Save Project if (e.ctrlKey && e.key === 's' && !e.shiftKey) { - e.preventDefault(); - document.getElementById('save-project').click(); + e.preventDefault() + document.getElementById('save-project').click() } // Ctrl+Shift+S: Save Project As if (e.ctrlKey && e.shiftKey && e.key === 'S') { - e.preventDefault(); - document.getElementById('save-project-as').click(); + e.preventDefault() + document.getElementById('save-project-as').click() } // Ctrl+Z: Undo if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { - e.preventDefault(); - document.getElementById('undo').click(); + e.preventDefault() + document.getElementById('undo').click() } // Ctrl+Y or Ctrl+Shift+Z: Redo - if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'Z')) { - e.preventDefault(); - document.getElementById('redo').click(); + if ( + (e.ctrlKey && e.key === 'y') || + (e.ctrlKey && e.shiftKey && e.key === 'Z') + ) { + e.preventDefault() + document.getElementById('redo').click() } // Ctrl+G: Toggle Grid if (e.ctrlKey && e.key === 'g') { - e.preventDefault(); - document.getElementById('toggle-grid').click(); + e.preventDefault() + document.getElementById('toggle-grid').click() } // Tool shortcuts switch (e.key.toLowerCase()) { case 'b': // Pencil Tool - document.getElementById('brush-pencil').click(); - break; + document.getElementById('brush-pencil').click() + break case 'e': // Eraser Tool - document.getElementById('brush-eraser').click(); - break; + document.getElementById('brush-eraser').click() + break case 'f': // Fill Tool - document.getElementById('brush-fill').click(); - break; + document.getElementById('brush-fill').click() + break case 'l': // Line Tool - document.getElementById('brush-line').click(); - break; + document.getElementById('brush-line').click() + break case 'r': // Rectangle Tool - document.getElementById('brush-rect').click(); - break; + document.getElementById('brush-rect').click() + break case 'o': // Ellipse Tool - document.getElementById('brush-ellipse').click(); - break; + document.getElementById('brush-ellipse').click() + break case 'g': // Glitch Tool - document.getElementById('brush-glitch').click(); - break; + document.getElementById('brush-glitch').click() + break case 's': // Static Tool - document.getElementById('brush-static').click(); - break; + document.getElementById('brush-static').click() + break } - }); + }) } /** * Toggle a menu * @param {string} menuId - ID of the menu to toggle */ - toggleMenu(menuId) { - const menu = document.getElementById(menuId); + toggleMenu (menuId) { + const menu = document.getElementById(menuId) - if (!menu) return; + if (!menu) return // If this menu is already active, close it if (this.activeMenu === menuId) { - this.closeMenu(menuId); - return; + this.closeMenu(menuId) + return } // Close any open menu - this.closeAllMenus(); + this.closeAllMenus() // Open this menu - this.openMenu(menuId); + this.openMenu(menuId) } /** * Open a menu * @param {string} menuId - ID of the menu to open */ - openMenu(menuId) { - const menu = document.getElementById(menuId); - const button = document.getElementById(`${menuId}-button`); + openMenu (menuId) { + const menu = document.getElementById(menuId) + const button = document.getElementById(`${menuId}-button`) - if (!menu || !button) return; + if (!menu || !button) return // Position the menu - this.positionMenu(menu, button); + this.positionMenu(menu, button) // Show the menu - menu.style.display = 'flex'; - menu.classList.add('visible'); + menu.style.display = 'flex' + menu.classList.add('visible') // Add active class to the button - button.classList.add('active'); + button.classList.add('active') // Set as active menu - this.activeMenu = menuId; + this.activeMenu = menuId // Log for debugging - console.log(`Opening menu: ${menuId}`); + console.log(`Opening menu: ${menuId}`) } /** * Close a menu * @param {string} menuId - ID of the menu to close */ - closeMenu(menuId) { - const menu = document.getElementById(menuId); - const button = document.getElementById(`${menuId}-button`); + closeMenu (menuId) { + const menu = document.getElementById(menuId) + const button = document.getElementById(`${menuId}-button`) - if (!menu || !button) return; + if (!menu || !button) return // Hide the menu - menu.style.display = 'none'; - menu.classList.remove('visible'); + menu.style.display = 'none' + menu.classList.remove('visible') // Remove active class from the button - button.classList.remove('active'); + button.classList.remove('active') // Clear active menu - this.activeMenu = null; + this.activeMenu = null // Log for debugging - console.log(`Closing menu: ${menuId}`); + console.log(`Closing menu: ${menuId}`) } /** * Close all menus */ - closeAllMenus() { - this.menuDropdowns.forEach(menu => { - menu.style.display = 'none'; - menu.classList.remove('visible'); - }); + closeAllMenus () { + this.menuDropdowns.forEach((menu) => { + menu.style.display = 'none' + menu.classList.remove('visible') + }) - this.menuButtons.forEach(button => { - button.classList.remove('active'); - }); + this.menuButtons.forEach((button) => { + button.classList.remove('active') + }) - this.activeMenu = null; + this.activeMenu = null // Log for debugging - console.log('Closing all menus'); + console.log('Closing all menus') } /** @@ -217,15 +223,15 @@ class MenuSystem { * @param {HTMLElement} menu - Menu element * @param {HTMLElement} button - Button element */ - positionMenu(menu, button) { - const buttonRect = button.getBoundingClientRect(); + positionMenu (menu, button) { + const buttonRect = button.getBoundingClientRect() // Position the menu below the button - menu.style.left = `${buttonRect.left}px`; - menu.style.top = `${buttonRect.bottom}px`; + menu.style.left = `${buttonRect.left}px` + menu.style.top = `${buttonRect.bottom}px` // Ensure the menu is visible by setting a high z-index - menu.style.zIndex = '2000'; + menu.style.zIndex = '2000' } /** @@ -233,82 +239,82 @@ class MenuSystem { * @param {MouseEvent} e - Mouse event * @param {Array} items - Array of menu item objects */ - createContextMenu(e, items) { + createContextMenu (e, items) { // Prevent default context menu - e.preventDefault(); + e.preventDefault() // Close any open menus - this.closeAllMenus(); + this.closeAllMenus() // Create or get the context menu element - let contextMenu = document.getElementById('context-menu'); + let contextMenu = document.getElementById('context-menu') if (!contextMenu) { - contextMenu = document.createElement('div'); - contextMenu.id = 'context-menu'; - contextMenu.className = 'menu-dropdown'; - document.body.appendChild(contextMenu); + contextMenu = document.createElement('div') + contextMenu.id = 'context-menu' + contextMenu.className = 'menu-dropdown' + document.body.appendChild(contextMenu) } // Clear the context menu - contextMenu.innerHTML = ''; + contextMenu.innerHTML = '' // Add menu items - items.forEach(item => { + items.forEach((item) => { if (item.separator) { // Add separator - const separator = document.createElement('div'); - separator.className = 'menu-separator'; - contextMenu.appendChild(separator); + const separator = document.createElement('div') + separator.className = 'menu-separator' + contextMenu.appendChild(separator) } else { // Add menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.type = 'button'; // Add type attribute - menuItem.textContent = item.label; + const menuItem = document.createElement('button') + menuItem.className = 'menu-item' + menuItem.type = 'button' // Add type attribute + menuItem.textContent = item.label if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add('disabled') } else { menuItem.addEventListener('click', () => { - this.hideContextMenu(); - if (item.action) item.action(); - }); + this.hideContextMenu() + if (item.action) item.action() + }) } - contextMenu.appendChild(menuItem); + contextMenu.appendChild(menuItem) } - }); + }) // Position the context menu - contextMenu.style.left = `${e.clientX}px`; - contextMenu.style.top = `${e.clientY}px`; + contextMenu.style.left = `${e.clientX}px` + contextMenu.style.top = `${e.clientY}px` // Show the context menu - contextMenu.style.display = 'flex'; + contextMenu.style.display = 'flex' // Bind the hideContextMenu method to this instance - this._boundHideContextMenu = this.hideContextMenu.bind(this); + this._boundHideContextMenu = this.hideContextMenu.bind(this) // Add event listener to hide the context menu when clicking outside setTimeout(() => { - document.addEventListener('click', this._boundHideContextMenu); - }, 0); + document.addEventListener('click', this._boundHideContextMenu) + }, 0) } /** * Hide the context menu */ - hideContextMenu() { - const contextMenu = document.getElementById('context-menu'); + hideContextMenu () { + const contextMenu = document.getElementById('context-menu') if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = 'none' } // Remove the event listener using the bound method if (this._boundHideContextMenu) { - document.removeEventListener('click', this._boundHideContextMenu); + document.removeEventListener('click', this._boundHideContextMenu) } } @@ -318,37 +324,37 @@ class MenuSystem { * @param {Object} item - Menu item object * @param {number} position - Position to insert the item (optional) */ - addMenuItem(menuId, item, position = null) { - const menu = document.getElementById(menuId); + addMenuItem (menuId, item, position = null) { + const menu = document.getElementById(menuId) - if (!menu) return; + if (!menu) return // Create the menu item - const menuItem = document.createElement('button'); - menuItem.className = 'menu-item'; - menuItem.id = item.id; - menuItem.type = 'button'; // Add type attribute - menuItem.textContent = item.label; + const menuItem = document.createElement('button') + menuItem.className = 'menu-item' + menuItem.id = item.id + menuItem.type = 'button' // Add type attribute + menuItem.textContent = item.label if (item.disabled) { - menuItem.classList.add('disabled'); + menuItem.classList.add('disabled') } else { - menuItem.addEventListener('click', item.action); + menuItem.addEventListener('click', item.action) } // Add shortcut text if provided if (item.shortcut) { - const shortcutSpan = document.createElement('span'); - shortcutSpan.className = 'menu-shortcut'; - shortcutSpan.textContent = item.shortcut; - menuItem.appendChild(shortcutSpan); + const shortcutSpan = document.createElement('span') + shortcutSpan.className = 'menu-shortcut' + shortcutSpan.textContent = item.shortcut + menuItem.appendChild(shortcutSpan) } // Insert at the specified position or append to the end if (position !== null && position < menu.children.length) { - menu.insertBefore(menuItem, menu.children[position]); + menu.insertBefore(menuItem, menu.children[position]) } else { - menu.appendChild(menuItem); + menu.appendChild(menuItem) } } @@ -356,11 +362,11 @@ class MenuSystem { * Remove a menu item from a menu * @param {string} menuItemId - ID of the menu item to remove */ - removeMenuItem(menuItemId) { - const menuItem = document.getElementById(menuItemId); + removeMenuItem (menuItemId) { + const menuItem = document.getElementById(menuItemId) if (menuItem?.parentNode) { - menuItem.parentNode.removeChild(menuItem); + menuItem.parentNode.removeChild(menuItem) } } @@ -369,14 +375,14 @@ class MenuSystem { * @param {string} menuItemId - ID of the menu item * @param {boolean} enabled - Whether the item should be enabled */ - setMenuItemEnabled(menuItemId, enabled) { - const menuItem = document.getElementById(menuItemId); + setMenuItemEnabled (menuItemId, enabled) { + const menuItem = document.getElementById(menuItemId) if (menuItem) { if (enabled) { - menuItem.classList.remove('disabled'); + menuItem.classList.remove('disabled') } else { - menuItem.classList.add('disabled'); + menuItem.classList.add('disabled') } } } From 36b8da4f530ee3ab6379cf2cc4574e34a9dc7b00 Mon Sep 17 00:00:00 2001 From: numbpill3d Date: Tue, 17 Jun 2025 13:53:46 -0400 Subject: [PATCH 7/9] nnn --- .history/src/scripts/app_20250617123815.js | 664 ++++++++++++++ .history/src/scripts/app_20250617124102.js | 666 ++++++++++++++ .history/src/scripts/app_20250617124427.js | 675 ++++++++++++++ .history/src/scripts/app_20250617124617.js | 676 ++++++++++++++ .history/src/scripts/app_20250617124803.js | 686 ++++++++++++++ .history/src/scripts/app_20250617124923.js | 708 +++++++++++++++ .history/src/scripts/app_20250617125036.js | 730 +++++++++++++++ .../src/scripts/lib/voidAPI_20250617122406.js | 0 .../src/scripts/lib/voidAPI_20250617122524.js | 37 + .../src/scripts/lib/voidAPI_20250617124603.js | 37 + .../tools/BrushEngine_20250617134734.js | 839 ++++++++++++++++++ .../tools/BrushEngine_20250617134921.js | 839 ++++++++++++++++++ src/scripts/app.js | 106 ++- src/scripts/lib/voidAPI.js | 37 + 14 files changed, 6679 insertions(+), 21 deletions(-) create mode 100644 .history/src/scripts/app_20250617123815.js create mode 100644 .history/src/scripts/app_20250617124102.js create mode 100644 .history/src/scripts/app_20250617124427.js create mode 100644 .history/src/scripts/app_20250617124617.js create mode 100644 .history/src/scripts/app_20250617124803.js create mode 100644 .history/src/scripts/app_20250617124923.js create mode 100644 .history/src/scripts/app_20250617125036.js create mode 100644 .history/src/scripts/lib/voidAPI_20250617122406.js create mode 100644 .history/src/scripts/lib/voidAPI_20250617122524.js create mode 100644 .history/src/scripts/lib/voidAPI_20250617124603.js create mode 100644 .history/src/scripts/tools/BrushEngine_20250617134734.js create mode 100644 .history/src/scripts/tools/BrushEngine_20250617134921.js create mode 100644 src/scripts/lib/voidAPI.js diff --git a/.history/src/scripts/app_20250617123815.js b/.history/src/scripts/app_20250617123815.js new file mode 100644 index 0000000..d3281a1 --- /dev/null +++ b/.history/src/scripts/app_20250617123815.js @@ -0,0 +1,664 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. + */ + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Timeline (glitchTool removed as unused) + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = 100; // Default value since element doesn't exist + + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/app_20250617124102.js b/.history/src/scripts/app_20250617124102.js new file mode 100644 index 0000000..b3b161f --- /dev/null +++ b/.history/src/scripts/app_20250617124102.js @@ -0,0 +1,666 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. + */ + +const voidAPI = require('./lib/voidAPI'); + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Timeline (glitchTool removed as unused) + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value) || 100; + + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/app_20250617124427.js b/.history/src/scripts/app_20250617124427.js new file mode 100644 index 0000000..a33db5c --- /dev/null +++ b/.history/src/scripts/app_20250617124427.js @@ -0,0 +1,675 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. + */ + +const voidAPI = require('./lib/voidAPI'); + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Timeline (glitchTool removed as unused) + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + try { + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.showToast(`PNG export error: ${error.message}`, 'error'); + } + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); + + try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/app_20250617124617.js b/.history/src/scripts/app_20250617124617.js new file mode 100644 index 0000000..b1546e3 --- /dev/null +++ b/.history/src/scripts/app_20250617124617.js @@ -0,0 +1,676 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. +const voidAPI = require('./lib/voidAPI'); + */ + +const voidAPI = require('./lib/voidAPI'); + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Timeline (glitchTool removed as unused) + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + try { + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.showToast(`PNG export error: ${error.message}`, 'error'); + } + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); + + try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/app_20250617124803.js b/.history/src/scripts/app_20250617124803.js new file mode 100644 index 0000000..180739b --- /dev/null +++ b/.history/src/scripts/app_20250617124803.js @@ -0,0 +1,686 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. +const voidAPI = require('./lib/voidAPI'); + */ + +const voidAPI = require('./lib/voidAPI'); + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Timeline (glitchTool removed as unused) + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + try { + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { +voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.showToast(`PNG export error: ${error.message}`, 'error'); + } + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); + + try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/app_20250617124923.js b/.history/src/scripts/app_20250617124923.js new file mode 100644 index 0000000..a537b2d --- /dev/null +++ b/.history/src/scripts/app_20250617124923.js @@ -0,0 +1,708 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. +const voidAPI = require('./lib/voidAPI'); + */ + +const voidAPI = require('./lib/voidAPI'); + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Timeline (glitchTool removed as unused) + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + try { + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { +voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.showToast(`PNG export error: ${error.message}`, 'error'); + } + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); +try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } + + try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/app_20250617125036.js b/.history/src/scripts/app_20250617125036.js new file mode 100644 index 0000000..df3dd16 --- /dev/null +++ b/.history/src/scripts/app_20250617125036.js @@ -0,0 +1,730 @@ +/** + * Conjuration - Main Application + * + * This is the main entry point for the application that initializes + * all components and manages the application state. +const voidAPI = require('./lib/voidAPI'); + */ + +const voidAPI = require('./lib/voidAPI'); + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize UI Manager + const uiManager = new UIManager(); + + // Initialize Theme Manager + const themeManager = new ThemeManager(); + + // Add data-text attributes to section titles for glitch effect + document.querySelectorAll('.section-title').forEach(title => { + title.setAttribute('data-text', title.textContent); + }); + + // Initialize Menu System + const menuSystem = new MenuSystem(); + + // Initialize Canvas with temporary size (will be changed by user selection) + const pixelCanvas = new PixelCanvas({ + canvasId: 'pixel-canvas', + effectsCanvasId: 'effects-canvas', + uiCanvasId: 'ui-canvas', + width: 64, + height: 64, + pixelSize: 8 + }); + + // Show canvas size selection dialog on startup + showCanvasSizeSelectionDialog(); + + // Initialize Brush Engine + const brushEngine = new BrushEngine(pixelCanvas); + + // Initialize Symmetry Tools + const symmetryTools = new SymmetryTools(pixelCanvas); + + // Initialize Palette Tool with brush engine + const paletteTool = new PaletteTool(pixelCanvas, brushEngine); + + // Initialize Timeline (glitchTool removed as unused) + const timeline = new Timeline(pixelCanvas); + + // Initialize GIF Exporter + const gifExporter = new GifExporter(timeline); + + // Set up event listeners + setupEventListeners(); + + // Initialize the first frame + timeline.addFrame(); + + // Show welcome message + uiManager.showToast('Welcome to Conjuration', 'success'); + + /** + * Set up all event listeners for the application + */ + function setupEventListeners() { + setupWindowControls(); + setupMenuManager(); + setupFileMenu(); + setupEditMenu(); + setupExportMenu(); + setupThemeMenu(); + setupLoreMenu(); + setupToolButtons(); + setupPaletteOptions(); + setupEffectControls(); + setupBrushControls(); + setupTimelineControls(); + setupAnimationControls(); + setupZoomControls(); + setupMiscControls(); + + updateCanvasSizeDisplay(); + uiManager.setActiveTool('brush-pencil'); + uiManager.setActiveSymmetry('symmetry-none'); + uiManager.setActivePalette('palette-monochrome'); + } + + function setupWindowControls() { + document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow()); + + document.getElementById('maximize-button').addEventListener('click', () => { + voidAPI.maximizeWindow().then(result => { + document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]'; + }); + }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } + + function setupMenuManager() { + // Already handled by MenuManager + } + + function setupFileMenu() { + document.getElementById('new-project').addEventListener('click', handleNewProject); + document.getElementById('open-project').addEventListener('click', handleOpenProject); + document.getElementById('save-project').addEventListener('click', handleSaveProject); + } + + function setupEditMenu() { + document.getElementById('undo').addEventListener('click', handleUndo); + document.getElementById('redo').addEventListener('click', handleRedo); + document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid); + document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas); + } + + function setupExportMenu() { + document.getElementById('export-png').addEventListener('click', handleExportPNG); + document.getElementById('export-gif').addEventListener('click', handleExportGIF); + } + + function setupThemeMenu() { + document.getElementById('theme-lain-dive').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-morrowind-glyph').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + }); + + document.getElementById('theme-monolith').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + }); + } + + function setupLoreMenu() { + document.getElementById('lore-option1').addEventListener('click', () => { + themeManager.setTheme('lain-dive'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Lain Dive activated', 'success'); + }); + + document.getElementById('lore-option2').addEventListener('click', () => { + themeManager.setTheme('morrowind-glyph'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Morrowind Glyph activated', 'success'); + }); + + document.getElementById('lore-option3').addEventListener('click', () => { + themeManager.setTheme('monolith'); + menuSystem.closeAllMenus(); + uiManager.showToast('Lore: Monolith activated', 'success'); + }); + } + + function setupToolButtons() { + document.querySelectorAll('.tool-button').forEach(button => { + button.addEventListener('click', () => { + const toolId = button.id; + + if (toolId.startsWith('brush-')) { + const brushType = toolId.replace('brush-', ''); + brushEngine.setActiveBrush(brushType); + uiManager.setActiveTool(toolId); + } + + if (toolId.startsWith('symmetry-')) { + const symmetryType = toolId.replace('symmetry-', ''); + symmetryTools.setSymmetryMode(symmetryType); + uiManager.setActiveSymmetry(toolId); + } + }); + }); + } + + function setupPaletteOptions() { + document.querySelectorAll('.palette-option').forEach(option => { + option.addEventListener('click', () => { + const paletteId = option.id; + const paletteName = paletteId.replace('palette-', ''); + paletteTool.setPalette(paletteName); + uiManager.setActivePalette(paletteId); + }); + }); + } + + function setupEffectControls() { + document.querySelectorAll('.effect-checkbox input').forEach(checkbox => { + checkbox.addEventListener('change', updateEffects); + }); + + document.getElementById('effect-intensity').addEventListener('input', updateEffects); + } + + function setupBrushControls() { + document.getElementById('brush-size').addEventListener('input', (e) => { + const size = parseInt(e.target.value); + brushEngine.setBrushSize(size); + document.getElementById('brush-size-value').textContent = size; + }); + } + + function setupTimelineControls() { + document.getElementById('add-frame').addEventListener('click', () => timeline.addFrame()); + document.getElementById('duplicate-frame').addEventListener('click', () => timeline.duplicateCurrentFrame()); + document.getElementById('delete-frame').addEventListener('click', handleDeleteFrame); + } + + function setupAnimationControls() { + document.getElementById('play-animation').addEventListener('click', () => timeline.playAnimation()); + document.getElementById('stop-animation').addEventListener('click', () => timeline.stopAnimation()); + + document.getElementById('loop-animation').addEventListener('click', (e) => { + const loopButton = e.currentTarget; + loopButton.classList.toggle('active'); + timeline.setLooping(loopButton.classList.contains('active')); + }); + + document.getElementById('onion-skin').addEventListener('change', (e) => { + timeline.setOnionSkinning(e.target.checked); + }); + } + + function setupZoomControls() { + document.getElementById('zoom-in').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + }); + + document.getElementById('zoom-in-menu').addEventListener('click', () => { + pixelCanvas.zoomIn(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-out-menu').addEventListener('click', () => { + pixelCanvas.zoomOut(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + pixelCanvas.resetZoom(); + updateZoomLevel(); + menuSystem.closeAllMenus(); + }); + + document.getElementById('canvas-wrapper').addEventListener('wheel', (e) => { + e.preventDefault(); + pixelCanvas.zoomIn(e.deltaY < 0); + updateZoomLevel(); + }, { passive: false }); + } + + function setupMiscControls() { + document.querySelectorAll('.menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + document.querySelectorAll('.menu-dropdown').forEach(m => m.style.display = 'none'); + document.querySelectorAll('.menu-button').forEach(b => b.classList.remove('active')); + }); + }); + } + + function handleNewProject() { + uiManager.showConfirmDialog( + 'Create New Project', + 'This will clear your current project. Are you sure?', + () => { + pixelCanvas.clear(); + timeline.clear(); + timeline.addFrame(); + menuSystem.closeAllMenus(); + uiManager.showToast('New project created', 'success'); + } + ); + } + + function handleOpenProject() { + voidAPI.openProject().then(result => { + if (result.success) { + try { + const projectData = result.data; + pixelCanvas.setDimensions(projectData.width, projectData.height); + timeline.loadFromData(projectData.frames); + menuSystem.closeAllMenus(); + uiManager.showToast('Project loaded successfully', 'success'); + } catch (error) { + uiManager.showToast('Failed to load project: ' + error.message, 'error'); + } + } + }); + } + + function handleSaveProject() { + const projectData = { + width: pixelCanvas.width, + height: pixelCanvas.height, + frames: timeline.getFramesData(), + palette: paletteTool.getCurrentPalette(), + effects: { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value + } + }; + + voidAPI.saveProject(projectData).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('Project saved successfully', 'success'); + } else { + uiManager.showToast('Failed to save project', 'error'); + } + }); + } + + function handleUndo() { + if (pixelCanvas.undo()) { + uiManager.showToast('Undo successful', 'info'); + } else { + uiManager.showToast('Nothing to undo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleRedo() { + if (pixelCanvas.redo()) { + uiManager.showToast('Redo successful', 'info'); + } else { + uiManager.showToast('Nothing to redo', 'info'); + } + menuSystem.closeAllMenus(); + } + + function handleToggleGrid() { + pixelCanvas.toggleGrid(); + menuSystem.closeAllMenus(); + uiManager.showToast('Grid toggled', 'info'); + } + + function handleResizeCanvas() { + const content = ` +
+ +
+ + + + + + + +
+
+
+ +
+ + × + +
+
+
+ +
+ `; + + uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus()); + + document.querySelectorAll('.preset-size-button').forEach(button => { + button.addEventListener('click', () => { + const width = parseInt(button.dataset.width); + const height = parseInt(button.dataset.height); + document.getElementById('canvas-width').value = width; + document.getElementById('canvas-height').value = height; + }); + }); + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + const cancelButton = document.createElement('button'); + cancelButton.className = 'modal-button'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => uiManager.hideModal()); + + const resizeButton = document.createElement('button'); + resizeButton.className = 'modal-button primary'; + resizeButton.textContent = 'Resize'; + resizeButton.addEventListener('click', () => { + const width = parseInt(document.getElementById('canvas-width').value); + const height = parseInt(document.getElementById('canvas-height').value); + const preserveContent = document.getElementById('preserve-content').checked; + + if (width > 0 && height > 0 && width <= 1024 && height <= 1024) { + pixelCanvas.resize(width, height, preserveContent); + updateCanvasSizeDisplay(); + uiManager.hideModal(); + uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success'); + } else { + uiManager.showToast('Invalid dimensions', 'error'); + } + }); + + modalFooter.appendChild(cancelButton); + modalFooter.appendChild(resizeButton); + document.querySelector('.modal-dialog').appendChild(modalFooter); + menuSystem.closeAllMenus(); + } + + function handleExportPNG() { + const pngDataUrl = pixelCanvas.exportToPNG(); + try { + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { +voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.showToast(`PNG export error: ${error.message}`, 'error'); + } + } + + function handleExportGIF() { + uiManager.showLoadingDialog('Generating GIF...'); + const frameDelay = parseInt(document.getElementById('frame-delay').value); +try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } +try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } + + try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } + } + + function handleDeleteFrame() { + if (timeline.getFrameCount() > 1) { + timeline.deleteCurrentFrame(); + } else { + uiManager.showToast('Cannot delete the only frame', 'error'); + } + } + + /** + * Update all active effects + */ + function updateEffects() { + const effects = { + grain: document.getElementById('effect-grain').checked, + static: document.getElementById('effect-static').checked, + glitch: document.getElementById('effect-glitch').checked, + crt: document.getElementById('effect-crt').checked, + intensity: document.getElementById('effect-intensity').value / 100 + }; + + pixelCanvas.setEffects(effects); + } + + /** + * Update the zoom level display + */ + function updateZoomLevel() { + const zoomPercent = Math.round(pixelCanvas.getZoom() * 100); + document.getElementById('zoom-level').textContent = zoomPercent + '%'; + } + + /** + * Update the canvas size display + */ + function updateCanvasSizeDisplay() { + const width = pixelCanvas.width; + const height = pixelCanvas.height; + document.getElementById('canvas-size').textContent = `${width}x${height}`; + } + + /** + * Show canvas size selection dialog with visual previews + */ + function showCanvasSizeSelectionDialog() { + // Create canvas size options with silhouettes + const canvasSizes = [ + { width: 32, height: 32, name: '32×32', description: 'Tiny pixel art' }, + { width: 64, height: 64, name: '64×64', description: 'Standard pixel art' }, + { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, + { width: 120, height: 60, name: '120×60', description: 'Small banner' }, + { width: 120, height: 80, name: '120×80', description: 'Small animation' }, + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } + ]; + + // Create HTML for size options with silhouettes + let sizesHTML = '
'; + + canvasSizes.forEach(size => { + // Calculate silhouette dimensions to match aspect ratio + let silhouetteWidth, silhouetteHeight; + + if (size.width > size.height) { + silhouetteWidth = "70%"; + silhouetteHeight = `${Math.round((size.height / size.width) * 70)}%`; + } else { + silhouetteHeight = "70%"; + silhouetteWidth = `${Math.round((size.width / size.height) * 70)}%`; + } + + sizesHTML += ` +
+
+
+
+
+
${size.name}
+
${size.description}
+
+
+ `; + }); + + sizesHTML += '
'; + + // Show the modal with size options and a title + const modalContent = ` +

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/lib/voidAPI_20250617122406.js b/.history/src/scripts/lib/voidAPI_20250617122406.js new file mode 100644 index 0000000..e69de29 diff --git a/.history/src/scripts/lib/voidAPI_20250617122524.js b/.history/src/scripts/lib/voidAPI_20250617122524.js new file mode 100644 index 0000000..86d38b2 --- /dev/null +++ b/.history/src/scripts/lib/voidAPI_20250617122524.js @@ -0,0 +1,37 @@ +/** + * VoidAPI - Interface for Electron IPC communication + */ +const { ipcRenderer } = require('electron'); + +const voidAPI = { + minimizeWindow: () => ipcRenderer.send('minimize-window'), + + maximizeWindow: () => new Promise((resolve) => { + ipcRenderer.send('maximize-window'); + ipcRenderer.once('maximize-reply', (_, result) => resolve(result)); + }), + + closeWindow: () => ipcRenderer.send('close-window'), + + openProject: () => new Promise((resolve) => { + ipcRenderer.send('open-project'); + ipcRenderer.once('open-project-reply', (_, result) => resolve(result)); + }), + + saveProject: (projectData) => new Promise((resolve) => { + ipcRenderer.send('save-project', projectData); + ipcRenderer.once('save-project-reply', (_, result) => resolve(result)); + }), + + exportPng: (dataUrl) => new Promise((resolve) => { + ipcRenderer.send('export-png', dataUrl); + ipcRenderer.once('export-png-reply', (_, result) => resolve(result)); + }), + + exportGif: (gifData) => new Promise((resolve) => { + ipcRenderer.send('export-gif', gifData); + ipcRenderer.once('export-gif-reply', (_, result) => resolve(result)); + }) +}; + +module.exports = voidAPI; \ No newline at end of file diff --git a/.history/src/scripts/lib/voidAPI_20250617124603.js b/.history/src/scripts/lib/voidAPI_20250617124603.js new file mode 100644 index 0000000..86d38b2 --- /dev/null +++ b/.history/src/scripts/lib/voidAPI_20250617124603.js @@ -0,0 +1,37 @@ +/** + * VoidAPI - Interface for Electron IPC communication + */ +const { ipcRenderer } = require('electron'); + +const voidAPI = { + minimizeWindow: () => ipcRenderer.send('minimize-window'), + + maximizeWindow: () => new Promise((resolve) => { + ipcRenderer.send('maximize-window'); + ipcRenderer.once('maximize-reply', (_, result) => resolve(result)); + }), + + closeWindow: () => ipcRenderer.send('close-window'), + + openProject: () => new Promise((resolve) => { + ipcRenderer.send('open-project'); + ipcRenderer.once('open-project-reply', (_, result) => resolve(result)); + }), + + saveProject: (projectData) => new Promise((resolve) => { + ipcRenderer.send('save-project', projectData); + ipcRenderer.once('save-project-reply', (_, result) => resolve(result)); + }), + + exportPng: (dataUrl) => new Promise((resolve) => { + ipcRenderer.send('export-png', dataUrl); + ipcRenderer.once('export-png-reply', (_, result) => resolve(result)); + }), + + exportGif: (gifData) => new Promise((resolve) => { + ipcRenderer.send('export-gif', gifData); + ipcRenderer.once('export-gif-reply', (_, result) => resolve(result)); + }) +}; + +module.exports = voidAPI; \ No newline at end of file diff --git a/.history/src/scripts/tools/BrushEngine_20250617134734.js b/.history/src/scripts/tools/BrushEngine_20250617134734.js new file mode 100644 index 0000000..f6b2583 --- /dev/null +++ b/.history/src/scripts/tools/BrushEngine_20250617134734.js @@ -0,0 +1,839 @@ +/** + * BrushEngine Class + * + * Handles different brush types and drawing operations. + */ +class BrushEngine { + /** + * Create a new BrushEngine + * @param {PixelCanvas} canvas - The PixelCanvas instance + */ + constructor(canvas) { + this.canvas = canvas; + this.activeBrush = 'pencil'; + this.brushSize = 1; + this.primaryColor = '#ffffff'; + this.secondaryColor = '#000000'; + this.isDrawing = false; + this.startX = 0; + this.startY = 0; + this.lastX = 0; + this.lastY = 0; + + // Set up event listeners + this.setupEventListeners(); + } + + /** + * Set up event listeners for mouse/touch interaction + */ + setupEventListeners() { + const canvas = this.canvas.canvas; + + // Mouse events + canvas.addEventListener('mousedown', this.handleMouseDown.bind(this)); + document.addEventListener('mousemove', this.handleMouseMove.bind(this)); + document.addEventListener('mouseup', this.handleMouseUp.bind(this)); + + // Prevent context menu on right-click + canvas.addEventListener('contextmenu', (e) => { + e.preventDefault(); + }); + } + + /** + * Handle mouse down event + * @param {MouseEvent} e - Mouse event + */ + handleMouseDown(e) { + this.isDrawing = true; + + // Get pixel coordinates + const rect = this.canvas.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.canvas.pixelSize * this.canvas.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.canvas.pixelSize * this.canvas.zoom)); + + // Store start position + this.startX = x; + this.startY = y; + this.lastX = x; + this.lastY = y; + + // Get color based on mouse button + const color = e.buttons === 2 ? this.secondaryColor : this.primaryColor; + + // Handle different brush types + switch (this.activeBrush) { + case 'pencil': + this.drawWithPencil(x, y, color); + break; + case 'brush': + this.drawWithBrush(x, y, color); + break; + case 'eraser': + this.drawWithEraser(x, y); + break; + case 'fill': + this.fillArea(x, y, color); + break; + case 'line': + case 'rect': + case 'ellipse': + // These are handled in mouseMove and mouseUp + break; + case 'glitch': + this.applyGlitchBrush(x, y, color); + break; + case 'static': + this.applyStaticBrush(x, y, color); + break; + case 'spray': + this.applySprayBrush(x, y, color); + break; + case 'pixel': + this.drawPixelBrush(x, y, color); + break; + case 'dither': + this.applyDitherBrush(x, y, color); + break; + case 'pattern': + this.applyPatternBrush(x, y, color); + break; + } + + // Render the canvas + this.canvas.render(); + } + + /** + * Handle mouse move event + * @param {MouseEvent} e - Mouse event + */ + handleMouseMove(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.canvas.pixelSize * this.canvas.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.canvas.pixelSize * this.canvas.zoom)); + + // Get color based on mouse button + const color = e.buttons === 2 ? this.secondaryColor : this.primaryColor; + + // Handle different brush types + switch (this.activeBrush) { + case 'pencil': + this.drawWithPencil(x, y, color); + break; + case 'brush': + this.drawWithBrush(x, y, color); + break; + case 'eraser': + this.drawWithEraser(x, y); + break; + case 'line': + this.previewLine(this.startX, this.startY, x, y, color); + break; + case 'rect': + this.previewRect(this.startX, this.startY, x, y, color); + break; + case 'ellipse': + this.previewEllipse(this.startX, this.startY, x, y, color); + break; + case 'glitch': + this.applyGlitchBrush(x, y, color); + break; + case 'static': + this.applyStaticBrush(x, y, color); + break; + case 'spray': + this.applySprayBrush(x, y, color); + break; + case 'pixel': + this.drawPixelBrush(x, y, color); + break; + case 'dither': + this.applyDitherBrush(x, y, color); + break; + case 'pattern': + this.applyPatternBrush(x, y, color); + break; + } + + // Update last position + this.lastX = x; + this.lastY = y; + + // Render the canvas + this.canvas.render(); + } + + /** + * Handle mouse up event + * @param {MouseEvent} e - Mouse event + */ + handleMouseUp(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.canvas.pixelSize * this.canvas.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.canvas.pixelSize * this.canvas.zoom)); + + // Get color based on mouse button + const color = e.buttons === 2 ? this.secondaryColor : this.primaryColor; + + // Handle different brush types + switch (this.activeBrush) { + case 'line': + this.drawLine(this.startX, this.startY, x, y, color); + break; + case 'rect': + this.drawRect(this.startX, this.startY, x, y, color); + break; + case 'ellipse': + this.drawEllipse(this.startX, this.startY, x, y, color); + break; + } + + this.isDrawing = false; + + // Render the canvas + this.canvas.render(); + } + + /** + * Set the active brush + * @param {string} brushType - Type of brush to set as active + */ + setActiveBrush(brushType) { + this.activeBrush = brushType; + + // Set default brush size based on brush type + switch (brushType) { + case 'pencil': + case 'eraser': + this.brushSize = 1; + break; + case 'spray': + this.brushSize = 5; + break; + case 'brush': + this.brushSize = 3; + break; + case 'pixel': + this.brushSize = 1; + break; + case 'dither': + this.brushSize = 3; + break; + case 'pattern': + this.brushSize = 4; + break; + } + + // Update brush size slider if it exists + const brushSizeSlider = document.getElementById('brush-size'); + if (brushSizeSlider) { + brushSizeSlider.value = this.brushSize; + + // Update the displayed value + const brushSizeValue = document.getElementById('brush-size-value'); + if (brushSizeValue) { + brushSizeValue.textContent = this.brushSize; + } + } + } + + /** + * Set the brush size + * @param {number} size - Size of the brush in pixels + */ + setBrushSize(size) { + this.brushSize = size; + } + + /** + * Set the primary color + * @param {string} color - Color in hex format + */ + setPrimaryColor(color) { + this.primaryColor = color; + } + + /** + * Set the secondary color + * @param {string} color - Color in hex format + */ + setSecondaryColor(color) { + this.secondaryColor = color; + } + + /** + * Draw with the pencil brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawWithPencil(x, y, color) { + if (this.brushSize === 1) { + // Single pixel + this.canvas.drawPixel(x, y, color); + } else { + // Draw a square of pixels + const offset = Math.floor(this.brushSize / 2); + for (let i = -offset; i <= offset; i++) { + for (let j = -offset; j <= offset; j++) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + this.canvas.drawLine(this.lastX, this.lastY, x, y, color); + } + } + + /** + * Draw with the eraser brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + */ + drawWithEraser(x, y) { + if (this.brushSize === 1) { + // Single pixel + this.canvas.drawPixel(x, y, '#000000'); + } else { + // Draw a square of pixels + const offset = Math.floor(this.brushSize / 2); + for (let i = -offset; i <= offset; i++) { + for (let j = -offset; j <= offset; j++) { + this.canvas.drawPixel(x + i, y + j, '#000000'); + } + } + } + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + this.canvas.drawLine(this.lastX, this.lastY, x, y, '#000000'); + } + } + + /** + * Fill an area with a color + * @param {number} x - Starting X coordinate + * @param {number} y - Starting Y coordinate + * @param {string} color - Color to fill with + */ + fillArea(x, y, color) { + this.canvas.floodFill(x, y, color); + } + + /** + * Preview a line + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + previewLine(x1, y1, x2, y2, color) { + // Save the current pixel data + const originalPixels = this.canvas.getPixelData(); + + // Draw the line + this.canvas.drawLine(x1, y1, x2, y2, color); + + // Restore the original pixel data on the next frame + setTimeout(() => { + this.canvas.setPixelData(originalPixels); + }, 0); + } + + /** + * Draw a line + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawLine(x1, y1, x2, y2, color) { + this.canvas.drawLine(x1, y1, x2, y2, color); + } + + /** + * Preview a rectangle + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + previewRect(x1, y1, x2, y2, color) { + // Save the current pixel data + const originalPixels = this.canvas.getPixelData(); + + // Calculate rectangle dimensions + const left = Math.min(x1, x2); + const top = Math.min(y1, y2); + const width = Math.abs(x2 - x1) + 1; + const height = Math.abs(y2 - y1) + 1; + + // Draw the rectangle + this.canvas.drawRect(left, top, width, height, color); + + // Restore the original pixel data on the next frame + setTimeout(() => { + this.canvas.setPixelData(originalPixels); + }, 0); + } + + /** + * Draw a rectangle + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawRect(x1, y1, x2, y2, color) { + // Calculate rectangle dimensions + const left = Math.min(x1, x2); + const top = Math.min(y1, y2); + const width = Math.abs(x2 - x1) + 1; + const height = Math.abs(y2 - y1) + 1; + + // Draw the rectangle + this.canvas.drawRect(left, top, width, height, color); + } + + /** + * Preview an ellipse + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + previewEllipse(x1, y1, x2, y2, color) { + // Save the current pixel data + const originalPixels = this.canvas.getPixelData(); + + // Calculate ellipse parameters + const centerX = Math.floor((x1 + x2) / 2); + const centerY = Math.floor((y1 + y2) / 2); + const radiusX = Math.abs(x2 - x1) / 2; + const radiusY = Math.abs(y2 - y1) / 2; + + // Draw the ellipse + this.canvas.drawEllipse(centerX, centerY, radiusX, radiusY, color); + + // Restore the original pixel data on the next frame + setTimeout(() => { + this.canvas.setPixelData(originalPixels); + }, 0); + } + + /** + * Draw an ellipse + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawEllipse(x1, y1, x2, y2, color) { + // Calculate ellipse parameters + const centerX = Math.floor((x1 + x2) / 2); + const centerY = Math.floor((y1 + y2) / 2); + const radiusX = Math.abs(x2 - x1) / 2; + const radiusY = Math.abs(y2 - y1) / 2; + + // Draw the ellipse + this.canvas.drawEllipse(centerX, centerY, radiusX, radiusY, color); + } + + /** + * Apply the glitch brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyGlitchBrush(x, y, color) { + // Draw a basic pixel + this.canvas.drawPixel(x, y, color); + + // Add random glitch effects - reduced probability and effect size + if (Math.random() < 0.15) { + // Randomly shift a row but only in a limited area around the cursor + const rowY = y; + // Smaller shift amount to make it less aggressive + const shiftAmount = Math.floor((Math.random() - 0.5) * 5); + + // Only affect pixels in a limited range around the cursor + const range = this.brushSize * 3; + const startX = Math.max(0, x - range); + const endX = Math.min(this.canvas.width - 1, x + range); + + for (let i = startX; i <= endX; i++) { + const srcX = Math.max(0, Math.min(this.canvas.width - 1, (i - shiftAmount))); + const srcColor = this.canvas.getPixel(srcX, rowY); + if (srcColor) { + this.canvas.drawPixel(i, rowY, srcColor); + } + } + } + + // Occasionally add random noise - reduced area and amount + if (Math.random() < 0.1) { + // Fewer noise pixels + const noiseCount = 3 + this.brushSize; + for (let i = 0; i < noiseCount; i++) { + // Smaller noise area + const noiseX = x + Math.floor((Math.random() - 0.5) * (5 + this.brushSize)); + const noiseY = y + Math.floor((Math.random() - 0.5) * (5 + this.brushSize)); + this.canvas.drawPixel(noiseX, noiseY, color); + } + } + } + + /** + * Apply the static brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyStaticBrush(x, y, color) { + // Draw random noise in a circular area + const radius = this.brushSize; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Probability decreases with distance from center + const probability = 0.7 * (1 - distance / radius); + + if (Math.random() < probability) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + } + + /** + * Draw with the brush tool (soft brush) + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawWithBrush(x, y, color) { + // Draw a circle with opacity falloff from center + const radius = this.brushSize; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Calculate opacity based on distance from center + const opacity = 1 - (distance / radius); + + // Get the current pixel color + const currentColor = this.canvas.getPixel(x + i, y + j) || '#000000'; + + // Blend the colors + const blendedColor = this.blendColors(currentColor, color, opacity); + + // Draw the pixel + this.canvas.drawPixel(x + i, y + j, blendedColor); + } + } + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + const steps = Math.max(Math.abs(x - this.lastX), Math.abs(y - this.lastY)); + + for (let i = 0; i <= steps; i++) { + const t = steps === 0 ? 0 : i / steps; + const px = Math.floor(this.lastX + (x - this.lastX) * t); + const py = Math.floor(this.lastY + (y - this.lastY) * t); + + // Draw a circle at each step + for (let j = -radius; j <= radius; j++) { + for (let k = -radius; k <= radius; k++) { + // Calculate distance from center + const distance = Math.sqrt(j * j + k * k); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Calculate opacity based on distance from center + const opacity = 1 - (distance / radius); + + // Get the current pixel color + const currentColor = this.canvas.getPixel(px + j, py + k) || '#000000'; + + // Blend the colors + const blendedColor = this.blendColors(currentColor, color, opacity); + + // Draw the pixel + this.canvas.drawPixel(px + j, py + k, blendedColor); + } + } + } + } + } + + /** + * Apply the spray brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applySprayBrush(x, y, color) { + // Draw random dots in a circular area + const radius = this.brushSize * 2; + const density = 0.5; // Increased density + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Calculate probability based on distance from center + const probability = density * (1 - (distance / radius)); + + // Draw the pixel with probability + if (Math.random() < probability) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + } + + /** + * Draw with the pixel brush (sharp pixel art brush) + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawPixelBrush(x, y, color) { + // Draw a single pixel + this.canvas.drawPixel(x, y, color); + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + this.canvas.drawLine(this.lastX, this.lastY, x, y, color); + } + } + + /** + * Apply the dither brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyDitherBrush(x, y, color) { + // Apply a dithering pattern + const radius = this.brushSize; + + // 2x2 Bayer matrix pattern + const pattern = [ + [0, 2], + [3, 1] + ]; + const patternSize = pattern.length; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Get pattern coordinates + const patternX = (x + i) % patternSize; + const patternY = (y + j) % patternSize; + const px = patternX < 0 ? patternSize + patternX : patternX; + const py = patternY < 0 ? patternSize + patternY : patternY; + const patternValue = pattern[py][px]; + + // Draw the pixel based on pattern + if (patternValue > 1) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + + // Draw a line of dithered pixels from last position to current position + if (this.lastX !== x || this.lastY !== y) { + const steps = Math.max(Math.abs(x - this.lastX), Math.abs(y - this.lastY)); + + for (let i = 0; i <= steps; i++) { + const t = steps === 0 ? 0 : i / steps; + const px = Math.floor(this.lastX + (x - this.lastX) * t); + const py = Math.floor(this.lastY + (y - this.lastY) * t); + + for (let j = -offset; j <= offset; j++) { + for (let k = -offset; k <= offset; k++) { + // Calculate distance from center + const distance = Math.sqrt(j * j + k * k); + + // Skip pixels outside the radius + if (distance > offset) continue; + + // Get pattern value (0-3) + const patternX = Math.abs(px + j) % 2; + const patternY = Math.abs(py + k) % 2; + const patternValue = pattern[patternY][patternX]; + + // Draw the pixel based on pattern + if (patternValue > 1) { + this.canvas.drawPixel(px + j, py + k, color); + } + } + } + } + } + } + + /** + * Apply the pattern brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyPatternBrush(x, y, color) { + // Define some patterns with better variety + const patterns = [ + // Checkerboard + [ + [1, 0], + [0, 1] + ], + // Diagonal lines + [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ], + // Dots + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0] + ], + // Cross + [ + [0, 1, 0], + [1, 1, 1], + [0, 1, 0] + ] + ]; + + // Select a pattern based on brush size + const patternIndex = (this.brushSize - 1) % patterns.length; + const pattern = patterns[patternIndex]; + const patSize = pattern.length; + + // Apply the pattern + const radius = this.brushSize; + const patternSize = pattern.length; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Get pattern coordinates using modulo to wrap around + const patternX = (x + i) % patSize; + const patternY = (y + j) % patSize; + + // Ensure positive indices + const px = patternX < 0 ? patSize + patternX : patternX; + const py = patternY < 0 ? patSize + patternY : patternY; + + // Draw the pixel based on pattern + if (pattern[py][px]) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + + // Draw a line of patterned pixels from last position to current position + if (this.lastX !== x || this.lastY !== y) { + const steps = Math.max(Math.abs(x - this.lastX), Math.abs(y - this.lastY)); + + for (let i = 0; i <= steps; i++) { + const t = steps === 0 ? 0 : i / steps; + const px = Math.floor(this.lastX + (x - this.lastX) * t); + const py = Math.floor(this.lastY + (y - this.lastY) * t); + + // Apply pattern at each step with a smaller radius + const lineOffset = Math.max(1, Math.floor(offset / 2)); + + for (let j = -lineOffset; j <= lineOffset; j++) { + for (let k = -lineOffset; k <= lineOffset; k++) { + // Calculate distance from center + const distance = Math.sqrt(j * j + k * k); + + // Skip pixels outside the radius + if (distance > lineOffset) continue; + + // Get pattern value with proper centering + const patternX = (px + j + patternOffsetX + patternSize) % patternSize; + const patternY = (py + k + patternOffsetY + patternSize) % patternSize; + + // Draw the pixel based on pattern + if (pattern[patternY][patternX]) { + this.canvas.drawPixel(px + j, py + k, color); + } + } + } + } + } + } + + /** + * Blend two colors with opacity + * @param {string} color1 - First color in hex format + * @param {string} color2 - Second color in hex format + * @param {number} opacity - Opacity of the second color (0-1) + * @returns {string} Blended color in hex format + */ + blendColors(color1, color2, opacity) { + // Convert hex to RGB + const r1 = parseInt(color1.substr(1, 2), 16); + const g1 = parseInt(color1.substr(3, 2), 16); + const b1 = parseInt(color1.substr(5, 2), 16); + + const r2 = parseInt(color2.substr(1, 2), 16); + const g2 = parseInt(color2.substr(3, 2), 16); + const b2 = parseInt(color2.substr(5, 2), 16); + + // Blend the colors + const r = Math.round(r1 * (1 - opacity) + r2 * opacity); + const g = Math.round(g1 * (1 - opacity) + g2 * opacity); + const b = Math.round(b1 * (1 - opacity) + b2 * opacity); + + // Convert back to hex + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + } +} diff --git a/.history/src/scripts/tools/BrushEngine_20250617134921.js b/.history/src/scripts/tools/BrushEngine_20250617134921.js new file mode 100644 index 0000000..f6b2583 --- /dev/null +++ b/.history/src/scripts/tools/BrushEngine_20250617134921.js @@ -0,0 +1,839 @@ +/** + * BrushEngine Class + * + * Handles different brush types and drawing operations. + */ +class BrushEngine { + /** + * Create a new BrushEngine + * @param {PixelCanvas} canvas - The PixelCanvas instance + */ + constructor(canvas) { + this.canvas = canvas; + this.activeBrush = 'pencil'; + this.brushSize = 1; + this.primaryColor = '#ffffff'; + this.secondaryColor = '#000000'; + this.isDrawing = false; + this.startX = 0; + this.startY = 0; + this.lastX = 0; + this.lastY = 0; + + // Set up event listeners + this.setupEventListeners(); + } + + /** + * Set up event listeners for mouse/touch interaction + */ + setupEventListeners() { + const canvas = this.canvas.canvas; + + // Mouse events + canvas.addEventListener('mousedown', this.handleMouseDown.bind(this)); + document.addEventListener('mousemove', this.handleMouseMove.bind(this)); + document.addEventListener('mouseup', this.handleMouseUp.bind(this)); + + // Prevent context menu on right-click + canvas.addEventListener('contextmenu', (e) => { + e.preventDefault(); + }); + } + + /** + * Handle mouse down event + * @param {MouseEvent} e - Mouse event + */ + handleMouseDown(e) { + this.isDrawing = true; + + // Get pixel coordinates + const rect = this.canvas.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.canvas.pixelSize * this.canvas.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.canvas.pixelSize * this.canvas.zoom)); + + // Store start position + this.startX = x; + this.startY = y; + this.lastX = x; + this.lastY = y; + + // Get color based on mouse button + const color = e.buttons === 2 ? this.secondaryColor : this.primaryColor; + + // Handle different brush types + switch (this.activeBrush) { + case 'pencil': + this.drawWithPencil(x, y, color); + break; + case 'brush': + this.drawWithBrush(x, y, color); + break; + case 'eraser': + this.drawWithEraser(x, y); + break; + case 'fill': + this.fillArea(x, y, color); + break; + case 'line': + case 'rect': + case 'ellipse': + // These are handled in mouseMove and mouseUp + break; + case 'glitch': + this.applyGlitchBrush(x, y, color); + break; + case 'static': + this.applyStaticBrush(x, y, color); + break; + case 'spray': + this.applySprayBrush(x, y, color); + break; + case 'pixel': + this.drawPixelBrush(x, y, color); + break; + case 'dither': + this.applyDitherBrush(x, y, color); + break; + case 'pattern': + this.applyPatternBrush(x, y, color); + break; + } + + // Render the canvas + this.canvas.render(); + } + + /** + * Handle mouse move event + * @param {MouseEvent} e - Mouse event + */ + handleMouseMove(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.canvas.pixelSize * this.canvas.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.canvas.pixelSize * this.canvas.zoom)); + + // Get color based on mouse button + const color = e.buttons === 2 ? this.secondaryColor : this.primaryColor; + + // Handle different brush types + switch (this.activeBrush) { + case 'pencil': + this.drawWithPencil(x, y, color); + break; + case 'brush': + this.drawWithBrush(x, y, color); + break; + case 'eraser': + this.drawWithEraser(x, y); + break; + case 'line': + this.previewLine(this.startX, this.startY, x, y, color); + break; + case 'rect': + this.previewRect(this.startX, this.startY, x, y, color); + break; + case 'ellipse': + this.previewEllipse(this.startX, this.startY, x, y, color); + break; + case 'glitch': + this.applyGlitchBrush(x, y, color); + break; + case 'static': + this.applyStaticBrush(x, y, color); + break; + case 'spray': + this.applySprayBrush(x, y, color); + break; + case 'pixel': + this.drawPixelBrush(x, y, color); + break; + case 'dither': + this.applyDitherBrush(x, y, color); + break; + case 'pattern': + this.applyPatternBrush(x, y, color); + break; + } + + // Update last position + this.lastX = x; + this.lastY = y; + + // Render the canvas + this.canvas.render(); + } + + /** + * Handle mouse up event + * @param {MouseEvent} e - Mouse event + */ + handleMouseUp(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.canvas.pixelSize * this.canvas.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.canvas.pixelSize * this.canvas.zoom)); + + // Get color based on mouse button + const color = e.buttons === 2 ? this.secondaryColor : this.primaryColor; + + // Handle different brush types + switch (this.activeBrush) { + case 'line': + this.drawLine(this.startX, this.startY, x, y, color); + break; + case 'rect': + this.drawRect(this.startX, this.startY, x, y, color); + break; + case 'ellipse': + this.drawEllipse(this.startX, this.startY, x, y, color); + break; + } + + this.isDrawing = false; + + // Render the canvas + this.canvas.render(); + } + + /** + * Set the active brush + * @param {string} brushType - Type of brush to set as active + */ + setActiveBrush(brushType) { + this.activeBrush = brushType; + + // Set default brush size based on brush type + switch (brushType) { + case 'pencil': + case 'eraser': + this.brushSize = 1; + break; + case 'spray': + this.brushSize = 5; + break; + case 'brush': + this.brushSize = 3; + break; + case 'pixel': + this.brushSize = 1; + break; + case 'dither': + this.brushSize = 3; + break; + case 'pattern': + this.brushSize = 4; + break; + } + + // Update brush size slider if it exists + const brushSizeSlider = document.getElementById('brush-size'); + if (brushSizeSlider) { + brushSizeSlider.value = this.brushSize; + + // Update the displayed value + const brushSizeValue = document.getElementById('brush-size-value'); + if (brushSizeValue) { + brushSizeValue.textContent = this.brushSize; + } + } + } + + /** + * Set the brush size + * @param {number} size - Size of the brush in pixels + */ + setBrushSize(size) { + this.brushSize = size; + } + + /** + * Set the primary color + * @param {string} color - Color in hex format + */ + setPrimaryColor(color) { + this.primaryColor = color; + } + + /** + * Set the secondary color + * @param {string} color - Color in hex format + */ + setSecondaryColor(color) { + this.secondaryColor = color; + } + + /** + * Draw with the pencil brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawWithPencil(x, y, color) { + if (this.brushSize === 1) { + // Single pixel + this.canvas.drawPixel(x, y, color); + } else { + // Draw a square of pixels + const offset = Math.floor(this.brushSize / 2); + for (let i = -offset; i <= offset; i++) { + for (let j = -offset; j <= offset; j++) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + this.canvas.drawLine(this.lastX, this.lastY, x, y, color); + } + } + + /** + * Draw with the eraser brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + */ + drawWithEraser(x, y) { + if (this.brushSize === 1) { + // Single pixel + this.canvas.drawPixel(x, y, '#000000'); + } else { + // Draw a square of pixels + const offset = Math.floor(this.brushSize / 2); + for (let i = -offset; i <= offset; i++) { + for (let j = -offset; j <= offset; j++) { + this.canvas.drawPixel(x + i, y + j, '#000000'); + } + } + } + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + this.canvas.drawLine(this.lastX, this.lastY, x, y, '#000000'); + } + } + + /** + * Fill an area with a color + * @param {number} x - Starting X coordinate + * @param {number} y - Starting Y coordinate + * @param {string} color - Color to fill with + */ + fillArea(x, y, color) { + this.canvas.floodFill(x, y, color); + } + + /** + * Preview a line + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + previewLine(x1, y1, x2, y2, color) { + // Save the current pixel data + const originalPixels = this.canvas.getPixelData(); + + // Draw the line + this.canvas.drawLine(x1, y1, x2, y2, color); + + // Restore the original pixel data on the next frame + setTimeout(() => { + this.canvas.setPixelData(originalPixels); + }, 0); + } + + /** + * Draw a line + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawLine(x1, y1, x2, y2, color) { + this.canvas.drawLine(x1, y1, x2, y2, color); + } + + /** + * Preview a rectangle + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + previewRect(x1, y1, x2, y2, color) { + // Save the current pixel data + const originalPixels = this.canvas.getPixelData(); + + // Calculate rectangle dimensions + const left = Math.min(x1, x2); + const top = Math.min(y1, y2); + const width = Math.abs(x2 - x1) + 1; + const height = Math.abs(y2 - y1) + 1; + + // Draw the rectangle + this.canvas.drawRect(left, top, width, height, color); + + // Restore the original pixel data on the next frame + setTimeout(() => { + this.canvas.setPixelData(originalPixels); + }, 0); + } + + /** + * Draw a rectangle + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawRect(x1, y1, x2, y2, color) { + // Calculate rectangle dimensions + const left = Math.min(x1, x2); + const top = Math.min(y1, y2); + const width = Math.abs(x2 - x1) + 1; + const height = Math.abs(y2 - y1) + 1; + + // Draw the rectangle + this.canvas.drawRect(left, top, width, height, color); + } + + /** + * Preview an ellipse + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + previewEllipse(x1, y1, x2, y2, color) { + // Save the current pixel data + const originalPixels = this.canvas.getPixelData(); + + // Calculate ellipse parameters + const centerX = Math.floor((x1 + x2) / 2); + const centerY = Math.floor((y1 + y2) / 2); + const radiusX = Math.abs(x2 - x1) / 2; + const radiusY = Math.abs(y2 - y1) / 2; + + // Draw the ellipse + this.canvas.drawEllipse(centerX, centerY, radiusX, radiusY, color); + + // Restore the original pixel data on the next frame + setTimeout(() => { + this.canvas.setPixelData(originalPixels); + }, 0); + } + + /** + * Draw an ellipse + * @param {number} x1 - Starting X coordinate + * @param {number} y1 - Starting Y coordinate + * @param {number} x2 - Ending X coordinate + * @param {number} y2 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawEllipse(x1, y1, x2, y2, color) { + // Calculate ellipse parameters + const centerX = Math.floor((x1 + x2) / 2); + const centerY = Math.floor((y1 + y2) / 2); + const radiusX = Math.abs(x2 - x1) / 2; + const radiusY = Math.abs(y2 - y1) / 2; + + // Draw the ellipse + this.canvas.drawEllipse(centerX, centerY, radiusX, radiusY, color); + } + + /** + * Apply the glitch brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyGlitchBrush(x, y, color) { + // Draw a basic pixel + this.canvas.drawPixel(x, y, color); + + // Add random glitch effects - reduced probability and effect size + if (Math.random() < 0.15) { + // Randomly shift a row but only in a limited area around the cursor + const rowY = y; + // Smaller shift amount to make it less aggressive + const shiftAmount = Math.floor((Math.random() - 0.5) * 5); + + // Only affect pixels in a limited range around the cursor + const range = this.brushSize * 3; + const startX = Math.max(0, x - range); + const endX = Math.min(this.canvas.width - 1, x + range); + + for (let i = startX; i <= endX; i++) { + const srcX = Math.max(0, Math.min(this.canvas.width - 1, (i - shiftAmount))); + const srcColor = this.canvas.getPixel(srcX, rowY); + if (srcColor) { + this.canvas.drawPixel(i, rowY, srcColor); + } + } + } + + // Occasionally add random noise - reduced area and amount + if (Math.random() < 0.1) { + // Fewer noise pixels + const noiseCount = 3 + this.brushSize; + for (let i = 0; i < noiseCount; i++) { + // Smaller noise area + const noiseX = x + Math.floor((Math.random() - 0.5) * (5 + this.brushSize)); + const noiseY = y + Math.floor((Math.random() - 0.5) * (5 + this.brushSize)); + this.canvas.drawPixel(noiseX, noiseY, color); + } + } + } + + /** + * Apply the static brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyStaticBrush(x, y, color) { + // Draw random noise in a circular area + const radius = this.brushSize; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Probability decreases with distance from center + const probability = 0.7 * (1 - distance / radius); + + if (Math.random() < probability) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + } + + /** + * Draw with the brush tool (soft brush) + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawWithBrush(x, y, color) { + // Draw a circle with opacity falloff from center + const radius = this.brushSize; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Calculate opacity based on distance from center + const opacity = 1 - (distance / radius); + + // Get the current pixel color + const currentColor = this.canvas.getPixel(x + i, y + j) || '#000000'; + + // Blend the colors + const blendedColor = this.blendColors(currentColor, color, opacity); + + // Draw the pixel + this.canvas.drawPixel(x + i, y + j, blendedColor); + } + } + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + const steps = Math.max(Math.abs(x - this.lastX), Math.abs(y - this.lastY)); + + for (let i = 0; i <= steps; i++) { + const t = steps === 0 ? 0 : i / steps; + const px = Math.floor(this.lastX + (x - this.lastX) * t); + const py = Math.floor(this.lastY + (y - this.lastY) * t); + + // Draw a circle at each step + for (let j = -radius; j <= radius; j++) { + for (let k = -radius; k <= radius; k++) { + // Calculate distance from center + const distance = Math.sqrt(j * j + k * k); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Calculate opacity based on distance from center + const opacity = 1 - (distance / radius); + + // Get the current pixel color + const currentColor = this.canvas.getPixel(px + j, py + k) || '#000000'; + + // Blend the colors + const blendedColor = this.blendColors(currentColor, color, opacity); + + // Draw the pixel + this.canvas.drawPixel(px + j, py + k, blendedColor); + } + } + } + } + } + + /** + * Apply the spray brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applySprayBrush(x, y, color) { + // Draw random dots in a circular area + const radius = this.brushSize * 2; + const density = 0.5; // Increased density + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Calculate probability based on distance from center + const probability = density * (1 - (distance / radius)); + + // Draw the pixel with probability + if (Math.random() < probability) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + } + + /** + * Draw with the pixel brush (sharp pixel art brush) + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawPixelBrush(x, y, color) { + // Draw a single pixel + this.canvas.drawPixel(x, y, color); + + // Draw a line from last position to current position + if (this.lastX !== x || this.lastY !== y) { + this.canvas.drawLine(this.lastX, this.lastY, x, y, color); + } + } + + /** + * Apply the dither brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyDitherBrush(x, y, color) { + // Apply a dithering pattern + const radius = this.brushSize; + + // 2x2 Bayer matrix pattern + const pattern = [ + [0, 2], + [3, 1] + ]; + const patternSize = pattern.length; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Get pattern coordinates + const patternX = (x + i) % patternSize; + const patternY = (y + j) % patternSize; + const px = patternX < 0 ? patternSize + patternX : patternX; + const py = patternY < 0 ? patternSize + patternY : patternY; + const patternValue = pattern[py][px]; + + // Draw the pixel based on pattern + if (patternValue > 1) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + + // Draw a line of dithered pixels from last position to current position + if (this.lastX !== x || this.lastY !== y) { + const steps = Math.max(Math.abs(x - this.lastX), Math.abs(y - this.lastY)); + + for (let i = 0; i <= steps; i++) { + const t = steps === 0 ? 0 : i / steps; + const px = Math.floor(this.lastX + (x - this.lastX) * t); + const py = Math.floor(this.lastY + (y - this.lastY) * t); + + for (let j = -offset; j <= offset; j++) { + for (let k = -offset; k <= offset; k++) { + // Calculate distance from center + const distance = Math.sqrt(j * j + k * k); + + // Skip pixels outside the radius + if (distance > offset) continue; + + // Get pattern value (0-3) + const patternX = Math.abs(px + j) % 2; + const patternY = Math.abs(py + k) % 2; + const patternValue = pattern[patternY][patternX]; + + // Draw the pixel based on pattern + if (patternValue > 1) { + this.canvas.drawPixel(px + j, py + k, color); + } + } + } + } + } + } + + /** + * Apply the pattern brush + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + applyPatternBrush(x, y, color) { + // Define some patterns with better variety + const patterns = [ + // Checkerboard + [ + [1, 0], + [0, 1] + ], + // Diagonal lines + [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ], + // Dots + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0] + ], + // Cross + [ + [0, 1, 0], + [1, 1, 1], + [0, 1, 0] + ] + ]; + + // Select a pattern based on brush size + const patternIndex = (this.brushSize - 1) % patterns.length; + const pattern = patterns[patternIndex]; + const patSize = pattern.length; + + // Apply the pattern + const radius = this.brushSize; + const patternSize = pattern.length; + + for (let i = -radius; i <= radius; i++) { + for (let j = -radius; j <= radius; j++) { + // Calculate distance from center + const distance = Math.sqrt(i * i + j * j); + + // Skip pixels outside the radius + if (distance > radius) continue; + + // Get pattern coordinates using modulo to wrap around + const patternX = (x + i) % patSize; + const patternY = (y + j) % patSize; + + // Ensure positive indices + const px = patternX < 0 ? patSize + patternX : patternX; + const py = patternY < 0 ? patSize + patternY : patternY; + + // Draw the pixel based on pattern + if (pattern[py][px]) { + this.canvas.drawPixel(x + i, y + j, color); + } + } + } + + // Draw a line of patterned pixels from last position to current position + if (this.lastX !== x || this.lastY !== y) { + const steps = Math.max(Math.abs(x - this.lastX), Math.abs(y - this.lastY)); + + for (let i = 0; i <= steps; i++) { + const t = steps === 0 ? 0 : i / steps; + const px = Math.floor(this.lastX + (x - this.lastX) * t); + const py = Math.floor(this.lastY + (y - this.lastY) * t); + + // Apply pattern at each step with a smaller radius + const lineOffset = Math.max(1, Math.floor(offset / 2)); + + for (let j = -lineOffset; j <= lineOffset; j++) { + for (let k = -lineOffset; k <= lineOffset; k++) { + // Calculate distance from center + const distance = Math.sqrt(j * j + k * k); + + // Skip pixels outside the radius + if (distance > lineOffset) continue; + + // Get pattern value with proper centering + const patternX = (px + j + patternOffsetX + patternSize) % patternSize; + const patternY = (py + k + patternOffsetY + patternSize) % patternSize; + + // Draw the pixel based on pattern + if (pattern[patternY][patternX]) { + this.canvas.drawPixel(px + j, py + k, color); + } + } + } + } + } + } + + /** + * Blend two colors with opacity + * @param {string} color1 - First color in hex format + * @param {string} color2 - Second color in hex format + * @param {number} opacity - Opacity of the second color (0-1) + * @returns {string} Blended color in hex format + */ + blendColors(color1, color2, opacity) { + // Convert hex to RGB + const r1 = parseInt(color1.substr(1, 2), 16); + const g1 = parseInt(color1.substr(3, 2), 16); + const b1 = parseInt(color1.substr(5, 2), 16); + + const r2 = parseInt(color2.substr(1, 2), 16); + const g2 = parseInt(color2.substr(3, 2), 16); + const b2 = parseInt(color2.substr(5, 2), 16); + + // Blend the colors + const r = Math.round(r1 * (1 - opacity) + r2 * opacity); + const g = Math.round(g1 * (1 - opacity) + g2 * opacity); + const b = Math.round(b1 * (1 - opacity) + b2 * opacity); + + // Convert back to hex + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + } +} diff --git a/src/scripts/app.js b/src/scripts/app.js index dd777c4..df3dd16 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -3,8 +3,11 @@ * * This is the main entry point for the application that initializes * all components and manages the application state. +const voidAPI = require('./lib/voidAPI'); */ +const voidAPI = require('./lib/voidAPI'); + // Wait for DOM to be fully loaded document.addEventListener('DOMContentLoaded', () => { // Initialize UI Manager @@ -43,10 +46,7 @@ document.addEventListener('DOMContentLoaded', () => { // Initialize Palette Tool with brush engine const paletteTool = new PaletteTool(pixelCanvas, brushEngine); - // Initialize Glitch Tool - const glitchTool = new GlitchTool(pixelCanvas); - - // Initialize Timeline + // Initialize Timeline (glitchTool removed as unused) const timeline = new Timeline(pixelCanvas); // Initialize GIF Exporter @@ -426,34 +426,102 @@ document.addEventListener('DOMContentLoaded', () => { function handleExportPNG() { const pngDataUrl = pixelCanvas.exportToPNG(); - voidAPI.exportPng(pngDataUrl).then(result => { + try { + voidAPI.exportPng(pngDataUrl).then(result => { + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('PNG exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export PNG', 'error'); + } + }).catch(error => { +voidAPI.exportPng(pngDataUrl).then(result => { if (result.success) { menuSystem.closeAllMenus(); uiManager.showToast('PNG exported successfully', 'success'); } else { uiManager.showToast('Failed to export PNG', 'error'); } + }).catch(error => { + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); }); + uiManager.showToast(`PNG export failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.showToast(`PNG export error: ${error.message}`, 'error'); + } } function handleExportGIF() { uiManager.showLoadingDialog('Generating GIF...'); const frameDelay = parseInt(document.getElementById('frame-delay').value); +try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } +try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); + }); + } catch (error) { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } - gifExporter.generateGif(frameDelay).then(gifData => { - voidAPI.exportGif(gifData).then(result => { + try { + gifExporter.generateGif(frameDelay).then(gifData => { + voidAPI.exportGif(gifData).then(result => { + uiManager.hideLoadingDialog(); + if (result.success) { + menuSystem.closeAllMenus(); + uiManager.showToast('GIF exported successfully', 'success'); + } else { + uiManager.showToast('Failed to export GIF', 'error'); + } + }).catch(error => { + uiManager.hideLoadingDialog(); + uiManager.showToast(`GIF export failed: ${error.message}`, 'error'); + }); + }).catch(error => { uiManager.hideLoadingDialog(); - if (result.success) { - menuSystem.closeAllMenus(); - uiManager.showToast('GIF exported successfully', 'success'); - } else { - uiManager.showToast('Failed to export GIF', 'error'); - } + uiManager.showToast(`GIF generation failed: ${error.message}`, 'error'); }); - }).catch(error => { + } catch (error) { uiManager.hideLoadingDialog(); - uiManager.showToast('Failed to generate GIF: ' + error.message, 'error'); - }); + uiManager.showToast(`GIF export error: ${error.message}`, 'error'); + } } function handleDeleteFrame() { @@ -473,13 +541,9 @@ document.addEventListener('DOMContentLoaded', () => { static: document.getElementById('effect-static').checked, glitch: document.getElementById('effect-glitch').checked, crt: document.getElementById('effect-crt').checked, - scanLines: document.getElementById('effect-scanLines').checked, - vignette: document.getElementById('effect-vignette').checked, - noise: document.getElementById('effect-noise').checked, - pixelate: document.getElementById('effect-pixelate').checked, intensity: document.getElementById('effect-intensity').value / 100 }; - + pixelCanvas.setEffects(effects); } diff --git a/src/scripts/lib/voidAPI.js b/src/scripts/lib/voidAPI.js new file mode 100644 index 0000000..86d38b2 --- /dev/null +++ b/src/scripts/lib/voidAPI.js @@ -0,0 +1,37 @@ +/** + * VoidAPI - Interface for Electron IPC communication + */ +const { ipcRenderer } = require('electron'); + +const voidAPI = { + minimizeWindow: () => ipcRenderer.send('minimize-window'), + + maximizeWindow: () => new Promise((resolve) => { + ipcRenderer.send('maximize-window'); + ipcRenderer.once('maximize-reply', (_, result) => resolve(result)); + }), + + closeWindow: () => ipcRenderer.send('close-window'), + + openProject: () => new Promise((resolve) => { + ipcRenderer.send('open-project'); + ipcRenderer.once('open-project-reply', (_, result) => resolve(result)); + }), + + saveProject: (projectData) => new Promise((resolve) => { + ipcRenderer.send('save-project', projectData); + ipcRenderer.once('save-project-reply', (_, result) => resolve(result)); + }), + + exportPng: (dataUrl) => new Promise((resolve) => { + ipcRenderer.send('export-png', dataUrl); + ipcRenderer.once('export-png-reply', (_, result) => resolve(result)); + }), + + exportGif: (gifData) => new Promise((resolve) => { + ipcRenderer.send('export-gif', gifData); + ipcRenderer.once('export-gif-reply', (_, result) => resolve(result)); + }) +}; + +module.exports = voidAPI; \ No newline at end of file From 3638d87b0b34bf8acce7878235049b7e93799201 Mon Sep 17 00:00:00 2001 From: numbpill3d Date: Tue, 24 Jun 2025 04:42:34 -0400 Subject: [PATCH 8/9] feat: implement unsaved changes prompt and enhance file save functionality --- main.js | 63 ++++- scripts/app.js | 375 +++++++++++++++++++++++------- src/scripts/canvas/PixelCanvas.js | 40 ++-- 3 files changed, 361 insertions(+), 117 deletions(-) diff --git a/main.js b/main.js index 5b54afb..babd7c0 100644 --- a/main.js +++ b/main.js @@ -42,6 +42,38 @@ function createWindow() { mainWindow.webContents.openDevTools(); } + // Handle close event + mainWindow.on('close', async (e) => { + e.preventDefault(); // Prevent immediate closing + + // Check if there are unsaved changes + const result = await mainWindow.webContents.executeJavaScript('window.voidApp && window.voidApp.hasUnsavedChanges()'); + + if (result) { + const { response } = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Save', "Don't Save", 'Cancel'], + title: 'Unsaved Changes', + message: 'Do you want to save your changes before closing?', + defaultId: 0, + cancelId: 2 + }); + + if (response === 0) { // Save + // Trigger save + const saveResult = await mainWindow.webContents.executeJavaScript('window.voidApp.saveProject()'); + if (saveResult.success) { + mainWindow.destroy(); + } + } else if (response === 1) { // Don't Save + mainWindow.destroy(); + } + // If response is 2 (Cancel), do nothing and keep the window open + } else { + mainWindow.destroy(); + } + }); + // Window events mainWindow.on('closed', () => { mainWindow = null; @@ -74,16 +106,22 @@ ipcMain.handle('save-project', async (event, projectData) => { filters: [{ name: 'VOIDSKETCH Files', extensions: ['void'] }] }); - if (filePath) { - try { - fs.writeFileSync(filePath, JSON.stringify(projectData)); - return { success: true, filePath }; - } catch (error) { - return { success: false, error: error.message }; + if (!filePath) { + return { success: false, error: 'No file path selected' }; + } + + try { + // Ensure the directory exists + const directory = path.dirname(filePath); + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); } + + fs.writeFileSync(filePath, JSON.stringify(projectData, null, 2)); + return { success: true, filePath }; + } catch (error) { + return { success: false, error: error.message }; } - - return { success: false }; }); ipcMain.handle('open-project', async () => { @@ -115,7 +153,14 @@ ipcMain.handle('export-gif', async (event, gifData) => { if (filePath) { try { // Handle binary data from renderer - const buffer = Buffer.from(gifData); + const buffer = Buffer.from(gifData instanceof Uint8Array ? gifData : new Uint8Array(gifData)); + + // Ensure the directory exists + const directory = path.dirname(filePath); + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } + fs.writeFileSync(filePath, buffer); return { success: true, filePath }; } catch (error) { diff --git a/scripts/app.js b/scripts/app.js index 7599df7..70f6b25 100644 --- a/scripts/app.js +++ b/scripts/app.js @@ -7,6 +7,12 @@ document.addEventListener('DOMContentLoaded', () => { // Initialize the application const app = new VoidSketchApp(); + + // Expose app instance to window for Electron IPC + window.voidApp = { + hasUnsavedChanges: () => app.hasUnsavedChanges(), + saveProject: () => app.saveProject() + }; }); /** @@ -32,6 +38,9 @@ class VoidSketchApp { } }; + // Store event listeners for cleanup + this.eventListeners = new Map(); + // Initialize components this.initializeComponents(); @@ -43,6 +52,26 @@ class VoidSketchApp { // Set window title this.updateWindowTitle(); + + // Handle window unload + window.addEventListener('unload', () => this.cleanup()); + } + + cleanup() { + // Remove all registered event listeners + this.eventListeners.forEach((listener, event) => { + document.removeEventListener(event, listener); + }); + + // Clear event listener map + this.eventListeners.clear(); + + // Clean up components + this.pixelCanvas.cleanup(); + this.timeline.cleanup(); + this.effectsEngine.cleanup(); + this.brushEngine.cleanup(); + this.glitchTool.cleanup(); } initializeComponents() { @@ -89,58 +118,68 @@ class VoidSketchApp { } setupEventListeners() { + // Helper function to add tracked event listener + const addListener = (event, handler) => { + this.eventListeners.set(event, handler); + document.addEventListener(event, handler); + }; + // UI Event Listeners - document.addEventListener('ui-new-project', () => this.createNewProject()); - document.addEventListener('ui-open-project', () => this.openProject()); - document.addEventListener('ui-save-project', () => this.saveProject()); - document.addEventListener('ui-save-project-as', () => this.saveProjectAs()); + addListener('ui-new-project', () => this.createNewProject()); + addListener('ui-open-project', () => this.openProject()); + addListener('ui-save-project', () => this.saveProject()); + addListener('ui-save-project-as', () => this.saveProjectAs()); - document.addEventListener('ui-undo', () => this.pixelCanvas.undo()); - document.addEventListener('ui-redo', () => this.pixelCanvas.redo()); + addListener('ui-undo', () => this.pixelCanvas.undo()); + addListener('ui-redo', () => this.pixelCanvas.redo()); - document.addEventListener('ui-copy', () => this.copySelection()); - document.addEventListener('ui-cut', () => this.cutSelection()); - document.addEventListener('ui-paste', () => this.pasteSelection()); - document.addEventListener('ui-select-all', () => this.selectAll()); - document.addEventListener('ui-deselect', () => this.deselect()); + addListener('ui-copy', () => this.copySelection()); + addListener('ui-cut', () => this.cutSelection()); + addListener('ui-paste', () => this.pasteSelection()); + addListener('ui-select-all', () => this.selectAll()); + addListener('ui-deselect', () => this.deselect()); - document.addEventListener('ui-toggle-grid', () => this.pixelCanvas.toggleGrid()); - document.addEventListener('ui-toggle-rulers', () => this.toggleRulers()); + addListener('ui-toggle-grid', () => this.pixelCanvas.toggleGrid()); + addListener('ui-toggle-rulers', () => this.toggleRulers()); - document.addEventListener('ui-export-png', () => this.exportPNG()); - document.addEventListener('ui-export-gif', () => this.exportGIF()); - document.addEventListener('ui-export-sprite-sheet', () => this.exportSpriteSheet()); + addListener('ui-export-png', () => this.exportPNG()); + addListener('ui-export-gif', () => this.exportGIF()); + addListener('ui-export-sprite-sheet', () => this.exportSpriteSheet()); - document.addEventListener('ui-toggle-lore-layer', () => this.toggleLoreLayer()); - document.addEventListener('ui-get-metadata', (event) => { + addListener('ui-toggle-lore-layer', () => this.toggleLoreLayer()); + addListener('ui-get-metadata', (event) => { if (event.detail && typeof event.detail === 'function') { event.detail(this.state.metadata); } }); - document.addEventListener('ui-save-metadata', (event) => this.saveMetadata(event.detail)); - document.addEventListener('ui-add-sigil', (event) => this.addSigil(event.detail)); - document.addEventListener('ui-apply-glitch', (event) => this.applyGlitch(event.detail)); + addListener('ui-save-metadata', (event) => this.saveMetadata(event.detail)); + addListener('ui-add-sigil', (event) => this.addSigil(event.detail)); + addListener('ui-apply-glitch', (event) => this.applyGlitch(event.detail)); - document.addEventListener('ui-set-tool', (event) => { + addListener('ui-set-tool', (event) => { if (event.detail) { this.brushEngine.setTool(event.detail); } }); // Handle palette changes - document.addEventListener('palette-changed', () => { + addListener('palette-changed', () => { this.markAsModified(); }); // Handle theme changes - document.addEventListener('theme-changed', () => { + addListener('theme-changed', () => { this.updateEffectsForTheme(); }); // Track modifications - this.pixelCanvas.canvas.addEventListener('mouseup', () => { - this.markAsModified(); - }); + const modificationHandler = () => this.markAsModified(); + this.pixelCanvas.canvas.addEventListener('mouseup', modificationHandler); + this.eventListeners.set('canvas-mouseup', modificationHandler); + } + + hasUnsavedChanges() { + return this.state.isModified; } updateWindowTitle() { @@ -198,60 +237,112 @@ class VoidSketchApp { async openProject() { try { + // Check for unsaved changes first + if (this.state.isModified) { + const { response } = await this.uiManager.showConfirmDialog( + 'Unsaved Changes', + 'Do you want to save your changes before opening another project?', + ['Save', "Don't Save", 'Cancel'] + ); + + if (response === 0) { // Save + const saveResult = await this.saveProject(); + if (!saveResult.success) { + return; // Don't proceed if save failed + } + } else if (response === 2) { // Cancel + return; + } + // If response is 1 (Don't Save), proceed with opening + } + // Show loading indicator - this.uiManager.showToast('Loading project...', 'info'); + const progressToast = this.uiManager.showToast('Opening project...', 'info', 0); // Use Electron IPC to open a file dialog const result = await window.voidAPI.openProject(); if (result.success && result.data) { - // Parse the project data - const projectData = result.data; - - // Load canvas size - if (projectData.canvasSize) { - this.pixelCanvas.setCanvasSize( - projectData.canvasSize.width, - projectData.canvasSize.height - ); + try { + // Parse the project data + const projectData = result.data; + + // Validate project data + if (!projectData || !projectData.frames || !Array.isArray(projectData.frames)) { + throw new Error('Invalid project file format'); + } + + progressToast.updateMessage('Loading canvas size...'); + + // Load canvas size + if (projectData.canvasSize) { + if (!projectData.canvasSize.width || !projectData.canvasSize.height || + projectData.canvasSize.width <= 0 || projectData.canvasSize.height <= 0) { + throw new Error('Invalid canvas dimensions'); + } + + this.pixelCanvas.setCanvasSize( + projectData.canvasSize.width, + projectData.canvasSize.height + ); + } + + progressToast.updateMessage('Loading frames...'); + + // Load frames + if (projectData.frames && Array.isArray(projectData.frames)) { + await this.timeline.setFramesFromData(projectData.frames, (progress) => { + progressToast.updateMessage(`Loading frames... ${Math.round(progress * 100)}%`); + }); + } + + progressToast.updateMessage('Loading palette...'); + + // Load palette + if (projectData.palette) { + this.paletteTool.setPalette(projectData.palette); + } + + progressToast.updateMessage('Loading effects...'); + + // Load effects + if (projectData.effects) { + this.effectsEngine.setEffectsSettings(projectData.effects); + } + + progressToast.updateMessage('Loading metadata...'); + + // Load metadata + if (projectData.metadata) { + this.state.metadata = projectData.metadata; + } + + // Load lore layer + if (projectData.loreLayer) { + this.state.loreLayer = projectData.loreLayer; + } + + // Update project state + this.state.currentFilePath = result.filePath; + this.state.projectName = this.getFileNameFromPath(result.filePath); + this.clearModified(); + + progressToast.close(); + this.uiManager.showToast('Project loaded successfully', 'success'); + } catch (parseError) { + throw new Error(`Failed to parse project file: ${parseError.message}`); } - - // Load frames - if (projectData.frames && Array.isArray(projectData.frames)) { - await this.timeline.setFramesFromData(projectData.frames); - } - - // Load palette - if (projectData.palette) { - this.paletteTool.setPalette(projectData.palette); - } - - // Load effects - if (projectData.effects) { - this.effectsEngine.setEffectsSettings(projectData.effects); - } - - // Load metadata - if (projectData.metadata) { - this.state.metadata = projectData.metadata; - } - - // Load lore layer - if (projectData.loreLayer) { - this.state.loreLayer = projectData.loreLayer; - } - - // Update project state - this.state.currentFilePath = result.filePath; - this.state.projectName = this.getFileNameFromPath(result.filePath); - this.clearModified(); - - // Show success message - this.uiManager.showToast('Project loaded successfully', 'success'); + } else { + throw new Error(result.error || 'No file selected'); } } catch (error) { console.error('Error opening project:', error); - this.uiManager.showToast('Failed to open project: ' + error.message, 'error'); + this.uiManager.showToast(`Failed to open project: ${error.message}`, 'error', 5000); + + // Log detailed error for debugging + if (error.stack) { + console.error('Stack trace:', error.stack); + } } } @@ -263,34 +354,65 @@ class VoidSketchApp { try { // Show saving indicator - this.uiManager.showToast('Saving project...', 'info'); + const progressToast = this.uiManager.showToast('Preparing project data...', 'info', 0); + + // Save current frame first + this.timeline.frames[this.timeline.currentFrameIndex].setImageData( + this.pixelCanvas.getCanvasImageData() + ); // Prepare project data const projectData = this.prepareProjectData(); + if (!projectData || !projectData.frames || projectData.frames.length === 0) { + throw new Error('Invalid project data'); + } + + progressToast.updateMessage('Saving project file...'); + // Use Electron IPC to save the file const result = await window.voidAPI.saveProject(projectData); if (result.success) { + progressToast.close(); this.clearModified(); this.uiManager.showToast('Project saved successfully', 'success'); + return { success: true }; } else { - throw new Error(result.error || 'Unknown error'); + throw new Error(result.error || 'Failed to save project file'); } } catch (error) { console.error('Error saving project:', error); - this.uiManager.showToast('Failed to save project: ' + error.message, 'error'); + this.uiManager.showToast(`Failed to save project: ${error.message}`, 'error', 5000); + + // Log detailed error for debugging + if (error.stack) { + console.error('Stack trace:', error.stack); + } + + return { success: false, error: error.message }; } } async saveProjectAs() { try { // Show saving indicator - this.uiManager.showToast('Saving project...', 'info'); + const progressToast = this.uiManager.showToast('Preparing project data...', 'info', 0); + + // Save current frame first + this.timeline.frames[this.timeline.currentFrameIndex].setImageData( + this.pixelCanvas.getCanvasImageData() + ); // Prepare project data const projectData = this.prepareProjectData(); + if (!projectData || !projectData.frames || projectData.frames.length === 0) { + throw new Error('Invalid project data'); + } + + progressToast.updateMessage('Saving project file...'); + // Use Electron IPC to save the file with dialog const result = await window.voidAPI.saveProject(projectData); @@ -301,13 +423,22 @@ class VoidSketchApp { this.clearModified(); this.updateWindowTitle(); + progressToast.close(); this.uiManager.showToast('Project saved successfully', 'success'); + return { success: true }; } else { - throw new Error(result.error || 'Unknown error'); + throw new Error(result.error || 'Failed to save project file'); } } catch (error) { console.error('Error saving project:', error); - this.uiManager.showToast('Failed to save project: ' + error.message, 'error'); + this.uiManager.showToast(`Failed to save project: ${error.message}`, 'error', 5000); + + // Log detailed error for debugging + if (error.stack) { + console.error('Stack trace:', error.stack); + } + + return { success: false, error: error.message }; } } @@ -346,20 +477,35 @@ class VoidSketchApp { async exportPNG() { try { + // Show loading indicator + const progressToast = this.uiManager.showToast('Preparing PNG export...', 'info', 0); + // Get canvas data URL const dataUrl = this.pixelCanvas.getCanvasData(); + if (!dataUrl || !dataUrl.startsWith('data:image/png;base64,')) { + throw new Error('Invalid PNG data generated'); + } + + progressToast.updateMessage('Saving PNG file...'); + // Use Electron IPC to save the PNG const result = await window.voidAPI.exportPng(dataUrl); if (result.success) { + progressToast.close(); this.uiManager.showToast('PNG exported successfully', 'success'); } else { - throw new Error(result.error || 'Unknown error'); + throw new Error(result.error || 'Failed to save PNG file'); } } catch (error) { console.error('Error exporting PNG:', error); - this.uiManager.showToast('Failed to export PNG: ' + error.message, 'error'); + this.uiManager.showToast(`Failed to export PNG: ${error.message}`, 'error', 5000); + + // Log detailed error for debugging + if (error.stack) { + console.error('Stack trace:', error.stack); + } } } @@ -481,47 +627,100 @@ class VoidSketchApp { async processGifExport(options) { try { - // Show loading indicator - this.uiManager.showToast('Generating GIF...', 'info'); + // Show loading indicator with progress + const progressToast = this.uiManager.showToast('Preparing frames for GIF export...', 'info', 0); // Save current frame first to ensure it's included this.timeline.frames[this.timeline.currentFrameIndex].setImageData( this.pixelCanvas.getCanvasImageData() ); - // Create GIF with options - const gifData = await this.gifExporter.exportGif(options); + // Validate frames + if (this.timeline.frames.length === 0) { + throw new Error('No frames to export'); + } + + // Update progress + progressToast.updateMessage('Generating GIF...'); + + // Create GIF with options and progress callback + const gifData = await this.gifExporter.exportGif(options, (progress) => { + progressToast.updateMessage(`Generating GIF... ${Math.round(progress * 100)}%`); + }); + + if (!gifData || gifData.length === 0) { + throw new Error('Failed to generate GIF data'); + } + + // Update progress + progressToast.updateMessage('Saving GIF file...'); // Use Electron IPC to save the GIF const result = await window.voidAPI.exportGif(gifData); if (result.success) { + // Close progress toast + progressToast.close(); this.uiManager.showToast('GIF exported successfully', 'success'); } else { - throw new Error(result.error || 'Unknown error'); + throw new Error(result.error || 'Failed to save GIF file'); } } catch (error) { console.error('Error exporting GIF:', error); - this.uiManager.showToast('Failed to export GIF: ' + error.message, 'error'); + this.uiManager.showToast(`Failed to export GIF: ${error.message}`, 'error', 5000); + + // Log detailed error for debugging + if (error.stack) { + console.error('Stack trace:', error.stack); + } } } async exportSpriteSheet() { try { + // Show loading indicator + const progressToast = this.uiManager.showToast('Preparing sprite sheet...', 'info', 0); + + // Validate frames + if (this.timeline.frames.length === 0) { + throw new Error('No frames available for sprite sheet export'); + } + + // Save current frame first to ensure it's included + this.timeline.frames[this.timeline.currentFrameIndex].setImageData( + this.pixelCanvas.getCanvasImageData() + ); + + progressToast.updateMessage('Generating sprite sheet...'); + // Generate sprite sheet - const spriteSheetDataUrl = await this.gifExporter.exportSpriteSheet(); + const spriteSheetDataUrl = await this.gifExporter.exportSpriteSheet((progress) => { + progressToast.updateMessage(`Generating sprite sheet... ${Math.round(progress * 100)}%`); + }); + + if (!spriteSheetDataUrl || !spriteSheetDataUrl.startsWith('data:image/png;base64,')) { + throw new Error('Invalid sprite sheet data generated'); + } + + progressToast.updateMessage('Saving sprite sheet...'); // Use Electron IPC to save the sprite sheet const result = await window.voidAPI.exportPng(spriteSheetDataUrl); if (result.success) { + progressToast.close(); this.uiManager.showToast('Sprite sheet exported successfully', 'success'); } else { - throw new Error(result.error || 'Unknown error'); + throw new Error(result.error || 'Failed to save sprite sheet'); } } catch (error) { console.error('Error exporting sprite sheet:', error); - this.uiManager.showToast('Failed to export sprite sheet: ' + error.message, 'error'); + this.uiManager.showToast(`Failed to export sprite sheet: ${error.message}`, 'error', 5000); + + // Log detailed error for debugging + if (error.stack) { + console.error('Stack trace:', error.stack); + } } } diff --git a/src/scripts/canvas/PixelCanvas.js b/src/scripts/canvas/PixelCanvas.js index 2a45920..700347c 100644 --- a/src/scripts/canvas/PixelCanvas.js +++ b/src/scripts/canvas/PixelCanvas.js @@ -68,6 +68,10 @@ class PixelCanvas { // Grid display this.showGrid = true; + // Selection and clipboard + this.selection = null; // {x, y, width, height} + this.clipboard = null; // {pixels, width, height} + // Initialize the canvas this.initCanvas(); @@ -467,6 +471,22 @@ class PixelCanvas { if (this.showGrid) { this.drawGrid(); } + + // Draw selection rectangle if present + if (this.selection) { + this.uiCtx.save(); + this.uiCtx.strokeStyle = '#FFD700'; + this.uiCtx.lineWidth = 2; + this.uiCtx.setLineDash([4, 2]); + const { x, y, width, height } = this.selection; + this.uiCtx.strokeRect( + x * this.pixelSize * this.zoom, + y * this.pixelSize * this.zoom, + width * this.pixelSize * this.zoom, + height * this.pixelSize * this.zoom + ); + this.uiCtx.restore(); + } } /** @@ -491,26 +511,6 @@ class PixelCanvas { // Draw horizontal lines for (let y = 0; y <= this.height; y++) { - const yPos = y * this.pixelSize * this.zoom; - this.uiCtx.moveTo(0, yPos); - this.uiCtx.lineTo(this.canvas.width, yPos); - } - - // Draw all lines at once - this.uiCtx.stroke(); - } - } - - /** - * Set the effects settings - * @param {Object} effects - Effects settings - */ - setEffects(effects) { - this.effects = {...this.effects, ...effects}; - } - - /** - * Animate the effects */ animateEffects() { // Use a bound function to avoid creating a new function on each frame From cd651f2852e7394daf2b80145d629d85026b854b Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 08:43:25 +0000 Subject: [PATCH 9/9] style: format code with Prettier and StandardJS This commit fixes the style issues introduced in 3638d87 according to the output from Prettier and StandardJS. Details: None --- main.js | 118 ++-- scripts/app.js | 1754 +++++++++++++++++++++++++----------------------- 2 files changed, 987 insertions(+), 885 deletions(-) diff --git a/main.js b/main.js index babd7c0..a281987 100644 --- a/main.js +++ b/main.js @@ -1,12 +1,12 @@ -const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron'); -const path = require('path'); -const fs = require('fs'); +const { app, BrowserWindow, Menu, ipcMain, dialog } = require("electron"); +const path = require("path"); +const fs = require("fs"); // Keep a global reference of the window object let mainWindow; // Custom file format -const VOID_FILE_EXTENSION = '.void'; +const VOID_FILE_EXTENSION = ".void"; function createWindow() { // Create the browser window with a dark background to avoid white flash @@ -15,10 +15,10 @@ function createWindow() { height: 800, minWidth: 800, minHeight: 600, - backgroundColor: '#000000', + backgroundColor: "#000000", show: false, // Don't show until ready-to-show webPreferences: { - preload: path.join(__dirname, 'preload.js'), + preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, enableRemoteModule: false, @@ -26,46 +26,52 @@ function createWindow() { // Remove default frame for custom UI frame: false, // Set default icon - icon: path.join(__dirname, 'src/assets/images/ui/icon.png') + icon: path.join(__dirname, "src/assets/images/ui/icon.png"), }); // Load the index.html of the app - mainWindow.loadFile(path.join(__dirname, 'src/index.html')); + mainWindow.loadFile(path.join(__dirname, "src/index.html")); // Show window when ready (prevents white flash) - mainWindow.once('ready-to-show', () => { + mainWindow.once("ready-to-show", () => { mainWindow.show(); }); // Open DevTools in development - if (process.env.NODE_ENV === 'development') { + if (process.env.NODE_ENV === "development") { mainWindow.webContents.openDevTools(); } // Handle close event - mainWindow.on('close', async (e) => { + mainWindow.on("close", async (e) => { e.preventDefault(); // Prevent immediate closing - + // Check if there are unsaved changes - const result = await mainWindow.webContents.executeJavaScript('window.voidApp && window.voidApp.hasUnsavedChanges()'); - + const result = await mainWindow.webContents.executeJavaScript( + "window.voidApp && window.voidApp.hasUnsavedChanges()", + ); + if (result) { const { response } = await dialog.showMessageBox(mainWindow, { - type: 'question', - buttons: ['Save', "Don't Save", 'Cancel'], - title: 'Unsaved Changes', - message: 'Do you want to save your changes before closing?', + type: "question", + buttons: ["Save", "Don't Save", "Cancel"], + title: "Unsaved Changes", + message: "Do you want to save your changes before closing?", defaultId: 0, - cancelId: 2 + cancelId: 2, }); - if (response === 0) { // Save + if (response === 0) { + // Save // Trigger save - const saveResult = await mainWindow.webContents.executeJavaScript('window.voidApp.saveProject()'); + const saveResult = await mainWindow.webContents.executeJavaScript( + "window.voidApp.saveProject()", + ); if (saveResult.success) { mainWindow.destroy(); } - } else if (response === 1) { // Don't Save + } else if (response === 1) { + // Don't Save mainWindow.destroy(); } // If response is 2 (Cancel), do nothing and keep the window open @@ -75,7 +81,7 @@ function createWindow() { }); // Window events - mainWindow.on('closed', () => { + mainWindow.on("closed", () => { mainWindow = null; }); } @@ -84,7 +90,7 @@ function createWindow() { app.whenReady().then(() => { createWindow(); - app.on('activate', () => { + app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } @@ -92,22 +98,22 @@ app.whenReady().then(() => { }); // Quit when all windows are closed, except on macOS -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { app.quit(); } }); // IPC handlers for file operations -ipcMain.handle('save-project', async (event, projectData) => { +ipcMain.handle("save-project", async (event, projectData) => { const { filePath } = await dialog.showSaveDialog(mainWindow, { - title: 'Save VOIDSKETCH Project', - defaultPath: 'untitled' + VOID_FILE_EXTENSION, - filters: [{ name: 'VOIDSKETCH Files', extensions: ['void'] }] + title: "Save VOIDSKETCH Project", + defaultPath: "untitled" + VOID_FILE_EXTENSION, + filters: [{ name: "VOIDSKETCH Files", extensions: ["void"] }], }); if (!filePath) { - return { success: false, error: 'No file path selected' }; + return { success: false, error: "No file path selected" }; } try { @@ -124,82 +130,84 @@ ipcMain.handle('save-project', async (event, projectData) => { } }); -ipcMain.handle('open-project', async () => { +ipcMain.handle("open-project", async () => { const { filePaths } = await dialog.showOpenDialog(mainWindow, { - title: 'Open VOIDSKETCH Project', - filters: [{ name: 'VOIDSKETCH Files', extensions: ['void'] }], - properties: ['openFile'] + title: "Open VOIDSKETCH Project", + filters: [{ name: "VOIDSKETCH Files", extensions: ["void"] }], + properties: ["openFile"], }); if (filePaths && filePaths.length > 0) { try { - const data = fs.readFileSync(filePaths[0], 'utf8'); + const data = fs.readFileSync(filePaths[0], "utf8"); return { success: true, data: JSON.parse(data), filePath: filePaths[0] }; } catch (error) { return { success: false, error: error.message }; } } - + return { success: false }; }); -ipcMain.handle('export-gif', async (event, gifData) => { +ipcMain.handle("export-gif", async (event, gifData) => { const { filePath } = await dialog.showSaveDialog(mainWindow, { - title: 'Export Animation as GIF', - defaultPath: 'voidsketch-animation.gif', - filters: [{ name: 'GIF Images', extensions: ['gif'] }] + title: "Export Animation as GIF", + defaultPath: "voidsketch-animation.gif", + filters: [{ name: "GIF Images", extensions: ["gif"] }], }); if (filePath) { try { // Handle binary data from renderer - const buffer = Buffer.from(gifData instanceof Uint8Array ? gifData : new Uint8Array(gifData)); - + const buffer = Buffer.from( + gifData instanceof Uint8Array ? gifData : new Uint8Array(gifData), + ); + // Ensure the directory exists const directory = path.dirname(filePath); if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { recursive: true }); } - + fs.writeFileSync(filePath, buffer); return { success: true, filePath }; } catch (error) { return { success: false, error: error.message }; } } - + return { success: false }; }); -ipcMain.handle('export-png', async (event, pngDataUrl) => { +ipcMain.handle("export-png", async (event, pngDataUrl) => { const { filePath } = await dialog.showSaveDialog(mainWindow, { - title: 'Export Canvas as PNG', - defaultPath: 'voidsketch-frame.png', - filters: [{ name: 'PNG Images', extensions: ['png'] }] + title: "Export Canvas as PNG", + defaultPath: "voidsketch-frame.png", + filters: [{ name: "PNG Images", extensions: ["png"] }], }); if (filePath) { try { // Convert data URL to buffer - const base64Data = pngDataUrl.replace(/^data:image\/png;base64,/, ''); - const buffer = Buffer.from(base64Data, 'base64'); + const base64Data = pngDataUrl.replace(/^data:image\/png;base64,/, ""); + const buffer = Buffer.from(base64Data, "base64"); fs.writeFileSync(filePath, buffer); return { success: true, filePath }; } catch (error) { return { success: false, error: error.message }; } } - + return { success: false }; }); // Window control handlers -ipcMain.handle('minimize-window', () => { +ipcMain.handle("minimize-window", () => { mainWindow.minimize(); return { success: true }; }); -ipcMain.handle('maximize-window', () => { +ipcMain.handle("maximize-window", () => { if (mainWindow.isMaximized()) { mainWindow.unmaximize(); } else { @@ -208,7 +216,7 @@ ipcMain.handle('maximize-window', () => { return { success: true, isMaximized: mainWindow.isMaximized() }; }); -ipcMain.handle('close-window', () => { +ipcMain.handle("close-window", () => { mainWindow.close(); return { success: true }; }); diff --git a/scripts/app.js b/scripts/app.js index 70f6b25..97014e2 100644 --- a/scripts/app.js +++ b/scripts/app.js @@ -4,15 +4,15 @@ */ // Wait for DOM to be fully loaded -document.addEventListener('DOMContentLoaded', () => { - // Initialize the application - const app = new VoidSketchApp(); - - // Expose app instance to window for Electron IPC - window.voidApp = { - hasUnsavedChanges: () => app.hasUnsavedChanges(), - saveProject: () => app.saveProject() - }; +document.addEventListener("DOMContentLoaded", () => { + // Initialize the application + const app = new VoidSketchApp(); + + // Expose app instance to window for Electron IPC + window.voidApp = { + hasUnsavedChanges: () => app.hasUnsavedChanges(), + saveProject: () => app.saveProject(), + }; }); /** @@ -20,846 +20,940 @@ document.addEventListener('DOMContentLoaded', () => { * Main application class that initializes and ties together all components */ class VoidSketchApp { - constructor() { - // Application state - this.state = { - currentFilePath: null, - projectName: 'Untitled', - isModified: false, - metadata: { - title: '', - author: '', - message: '', - encoding: 'plain' - }, - loreLayer: { - enabled: false, - sigils: [] - } - }; - - // Store event listeners for cleanup - this.eventListeners = new Map(); - - // Initialize components - this.initializeComponents(); - - // Set up event listeners - this.setupEventListeners(); - - // Show welcome message - this.uiManager.showToast('VOIDSKETCH initialized. Draw in the dark. Animate the echo.', 'info', 5000); - - // Set window title - this.updateWindowTitle(); - - // Handle window unload - window.addEventListener('unload', () => this.cleanup()); - } - - cleanup() { - // Remove all registered event listeners - this.eventListeners.forEach((listener, event) => { - document.removeEventListener(event, listener); - }); - - // Clear event listener map - this.eventListeners.clear(); - - // Clean up components - this.pixelCanvas.cleanup(); - this.timeline.cleanup(); - this.effectsEngine.cleanup(); - this.brushEngine.cleanup(); - this.glitchTool.cleanup(); - } - - initializeComponents() { - // Initialize UI Manager - this.uiManager = new UIManager(); - - // Initialize Theme Manager - this.themeManager = new ThemeManager(); - - // Initialize Menu System - this.menuSystem = new MenuSystem(this.uiManager); - - // Initialize Pixel Canvas - this.pixelCanvas = new PixelCanvas({ - width: 64, - height: 64, - pixelSize: 8, - backgroundColor: '#000000' - }); - - // Initialize Timeline - this.timeline = new Timeline(this.pixelCanvas); - - // Initialize GIF Exporter - this.gifExporter = new GifExporter(this.timeline); - - // Initialize Dithering Engine - this.ditheringEngine = new DitheringEngine(this.pixelCanvas); - - // Initialize Effects Engine - this.effectsEngine = new EffectsEngine(this.pixelCanvas); - - // Initialize Palette Tool - this.paletteTool = new PaletteTool(this.pixelCanvas); - - // Initialize Brush Engine - this.brushEngine = new BrushEngine(this.pixelCanvas); - - // Initialize Symmetry Tools - this.symmetryTools = new SymmetryTools(this.pixelCanvas); - - // Initialize Glitch Tool - this.glitchTool = new GlitchTool(this.pixelCanvas); - } - - setupEventListeners() { - // Helper function to add tracked event listener - const addListener = (event, handler) => { - this.eventListeners.set(event, handler); - document.addEventListener(event, handler); - }; - - // UI Event Listeners - addListener('ui-new-project', () => this.createNewProject()); - addListener('ui-open-project', () => this.openProject()); - addListener('ui-save-project', () => this.saveProject()); - addListener('ui-save-project-as', () => this.saveProjectAs()); - - addListener('ui-undo', () => this.pixelCanvas.undo()); - addListener('ui-redo', () => this.pixelCanvas.redo()); - - addListener('ui-copy', () => this.copySelection()); - addListener('ui-cut', () => this.cutSelection()); - addListener('ui-paste', () => this.pasteSelection()); - addListener('ui-select-all', () => this.selectAll()); - addListener('ui-deselect', () => this.deselect()); - - addListener('ui-toggle-grid', () => this.pixelCanvas.toggleGrid()); - addListener('ui-toggle-rulers', () => this.toggleRulers()); - - addListener('ui-export-png', () => this.exportPNG()); - addListener('ui-export-gif', () => this.exportGIF()); - addListener('ui-export-sprite-sheet', () => this.exportSpriteSheet()); - - addListener('ui-toggle-lore-layer', () => this.toggleLoreLayer()); - addListener('ui-get-metadata', (event) => { - if (event.detail && typeof event.detail === 'function') { - event.detail(this.state.metadata); - } - }); - addListener('ui-save-metadata', (event) => this.saveMetadata(event.detail)); - addListener('ui-add-sigil', (event) => this.addSigil(event.detail)); - addListener('ui-apply-glitch', (event) => this.applyGlitch(event.detail)); - - addListener('ui-set-tool', (event) => { - if (event.detail) { - this.brushEngine.setTool(event.detail); - } - }); - - // Handle palette changes - addListener('palette-changed', () => { - this.markAsModified(); - }); - - // Handle theme changes - addListener('theme-changed', () => { - this.updateEffectsForTheme(); - }); - - // Track modifications - const modificationHandler = () => this.markAsModified(); - this.pixelCanvas.canvas.addEventListener('mouseup', modificationHandler); - this.eventListeners.set('canvas-mouseup', modificationHandler); - } - - hasUnsavedChanges() { - return this.state.isModified; - } - - updateWindowTitle() { - // Update window title with project name and modified indicator - const modifiedIndicator = this.state.isModified ? '*' : ''; - document.title = `${this.state.projectName}${modifiedIndicator} - VOIDSKETCH`; - - // Update title bar text - document.getElementById('title-bar-text').textContent = - `VOIDSKETCH :: ${this.state.projectName}${modifiedIndicator}`; - } - - markAsModified() { - if (!this.state.isModified) { - this.state.isModified = true; - this.updateWindowTitle(); - } - } - - clearModified() { - if (this.state.isModified) { - this.state.isModified = false; - this.updateWindowTitle(); - } - } - - createNewProject() { - // Reset the canvas - this.pixelCanvas.clear(); - - // Reset the timeline - this.timeline.frames = []; - this.timeline.createNewFrame(); - this.timeline.updateFramesUI(); - - // Reset the project state - this.state.currentFilePath = null; - this.state.projectName = 'Untitled'; - this.state.isModified = false; - this.state.metadata = { - title: '', - author: '', - message: '', - encoding: 'plain' - }; - this.state.loreLayer = { - enabled: false, - sigils: [] - }; - - // Update the UI - this.updateWindowTitle(); - this.uiManager.showToast('Created new project', 'success'); + constructor() { + // Application state + this.state = { + currentFilePath: null, + projectName: "Untitled", + isModified: false, + metadata: { + title: "", + author: "", + message: "", + encoding: "plain", + }, + loreLayer: { + enabled: false, + sigils: [], + }, + }; + + // Store event listeners for cleanup + this.eventListeners = new Map(); + + // Initialize components + this.initializeComponents(); + + // Set up event listeners + this.setupEventListeners(); + + // Show welcome message + this.uiManager.showToast( + "VOIDSKETCH initialized. Draw in the dark. Animate the echo.", + "info", + 5000, + ); + + // Set window title + this.updateWindowTitle(); + + // Handle window unload + window.addEventListener("unload", () => this.cleanup()); + } + + cleanup() { + // Remove all registered event listeners + this.eventListeners.forEach((listener, event) => { + document.removeEventListener(event, listener); + }); + + // Clear event listener map + this.eventListeners.clear(); + + // Clean up components + this.pixelCanvas.cleanup(); + this.timeline.cleanup(); + this.effectsEngine.cleanup(); + this.brushEngine.cleanup(); + this.glitchTool.cleanup(); + } + + initializeComponents() { + // Initialize UI Manager + this.uiManager = new UIManager(); + + // Initialize Theme Manager + this.themeManager = new ThemeManager(); + + // Initialize Menu System + this.menuSystem = new MenuSystem(this.uiManager); + + // Initialize Pixel Canvas + this.pixelCanvas = new PixelCanvas({ + width: 64, + height: 64, + pixelSize: 8, + backgroundColor: "#000000", + }); + + // Initialize Timeline + this.timeline = new Timeline(this.pixelCanvas); + + // Initialize GIF Exporter + this.gifExporter = new GifExporter(this.timeline); + + // Initialize Dithering Engine + this.ditheringEngine = new DitheringEngine(this.pixelCanvas); + + // Initialize Effects Engine + this.effectsEngine = new EffectsEngine(this.pixelCanvas); + + // Initialize Palette Tool + this.paletteTool = new PaletteTool(this.pixelCanvas); + + // Initialize Brush Engine + this.brushEngine = new BrushEngine(this.pixelCanvas); + + // Initialize Symmetry Tools + this.symmetryTools = new SymmetryTools(this.pixelCanvas); + + // Initialize Glitch Tool + this.glitchTool = new GlitchTool(this.pixelCanvas); + } + + setupEventListeners() { + // Helper function to add tracked event listener + const addListener = (event, handler) => { + this.eventListeners.set(event, handler); + document.addEventListener(event, handler); + }; + + // UI Event Listeners + addListener("ui-new-project", () => this.createNewProject()); + addListener("ui-open-project", () => this.openProject()); + addListener("ui-save-project", () => this.saveProject()); + addListener("ui-save-project-as", () => this.saveProjectAs()); + + addListener("ui-undo", () => this.pixelCanvas.undo()); + addListener("ui-redo", () => this.pixelCanvas.redo()); + + addListener("ui-copy", () => this.copySelection()); + addListener("ui-cut", () => this.cutSelection()); + addListener("ui-paste", () => this.pasteSelection()); + addListener("ui-select-all", () => this.selectAll()); + addListener("ui-deselect", () => this.deselect()); + + addListener("ui-toggle-grid", () => this.pixelCanvas.toggleGrid()); + addListener("ui-toggle-rulers", () => this.toggleRulers()); + + addListener("ui-export-png", () => this.exportPNG()); + addListener("ui-export-gif", () => this.exportGIF()); + addListener("ui-export-sprite-sheet", () => this.exportSpriteSheet()); + + addListener("ui-toggle-lore-layer", () => this.toggleLoreLayer()); + addListener("ui-get-metadata", (event) => { + if (event.detail && typeof event.detail === "function") { + event.detail(this.state.metadata); + } + }); + addListener("ui-save-metadata", (event) => this.saveMetadata(event.detail)); + addListener("ui-add-sigil", (event) => this.addSigil(event.detail)); + addListener("ui-apply-glitch", (event) => this.applyGlitch(event.detail)); + + addListener("ui-set-tool", (event) => { + if (event.detail) { + this.brushEngine.setTool(event.detail); + } + }); + + // Handle palette changes + addListener("palette-changed", () => { + this.markAsModified(); + }); + + // Handle theme changes + addListener("theme-changed", () => { + this.updateEffectsForTheme(); + }); + + // Track modifications + const modificationHandler = () => this.markAsModified(); + this.pixelCanvas.canvas.addEventListener("mouseup", modificationHandler); + this.eventListeners.set("canvas-mouseup", modificationHandler); + } + + hasUnsavedChanges() { + return this.state.isModified; + } + + updateWindowTitle() { + // Update window title with project name and modified indicator + const modifiedIndicator = this.state.isModified ? "*" : ""; + document.title = `${this.state.projectName}${modifiedIndicator} - VOIDSKETCH`; + + // Update title bar text + document.getElementById("title-bar-text").textContent = + `VOIDSKETCH :: ${this.state.projectName}${modifiedIndicator}`; + } + + markAsModified() { + if (!this.state.isModified) { + this.state.isModified = true; + this.updateWindowTitle(); } - - async openProject() { - try { - // Check for unsaved changes first - if (this.state.isModified) { - const { response } = await this.uiManager.showConfirmDialog( - 'Unsaved Changes', - 'Do you want to save your changes before opening another project?', - ['Save', "Don't Save", 'Cancel'] - ); - - if (response === 0) { // Save - const saveResult = await this.saveProject(); - if (!saveResult.success) { - return; // Don't proceed if save failed - } - } else if (response === 2) { // Cancel - return; - } - // If response is 1 (Don't Save), proceed with opening - } - - // Show loading indicator - const progressToast = this.uiManager.showToast('Opening project...', 'info', 0); - - // Use Electron IPC to open a file dialog - const result = await window.voidAPI.openProject(); - - if (result.success && result.data) { - try { - // Parse the project data - const projectData = result.data; - - // Validate project data - if (!projectData || !projectData.frames || !Array.isArray(projectData.frames)) { - throw new Error('Invalid project file format'); - } - - progressToast.updateMessage('Loading canvas size...'); - - // Load canvas size - if (projectData.canvasSize) { - if (!projectData.canvasSize.width || !projectData.canvasSize.height || - projectData.canvasSize.width <= 0 || projectData.canvasSize.height <= 0) { - throw new Error('Invalid canvas dimensions'); - } - - this.pixelCanvas.setCanvasSize( - projectData.canvasSize.width, - projectData.canvasSize.height - ); - } - - progressToast.updateMessage('Loading frames...'); - - // Load frames - if (projectData.frames && Array.isArray(projectData.frames)) { - await this.timeline.setFramesFromData(projectData.frames, (progress) => { - progressToast.updateMessage(`Loading frames... ${Math.round(progress * 100)}%`); - }); - } - - progressToast.updateMessage('Loading palette...'); - - // Load palette - if (projectData.palette) { - this.paletteTool.setPalette(projectData.palette); - } - - progressToast.updateMessage('Loading effects...'); - - // Load effects - if (projectData.effects) { - this.effectsEngine.setEffectsSettings(projectData.effects); - } - - progressToast.updateMessage('Loading metadata...'); - - // Load metadata - if (projectData.metadata) { - this.state.metadata = projectData.metadata; - } - - // Load lore layer - if (projectData.loreLayer) { - this.state.loreLayer = projectData.loreLayer; - } - - // Update project state - this.state.currentFilePath = result.filePath; - this.state.projectName = this.getFileNameFromPath(result.filePath); - this.clearModified(); - - progressToast.close(); - this.uiManager.showToast('Project loaded successfully', 'success'); - } catch (parseError) { - throw new Error(`Failed to parse project file: ${parseError.message}`); - } - } else { - throw new Error(result.error || 'No file selected'); - } - } catch (error) { - console.error('Error opening project:', error); - this.uiManager.showToast(`Failed to open project: ${error.message}`, 'error', 5000); - - // Log detailed error for debugging - if (error.stack) { - console.error('Stack trace:', error.stack); - } - } + } + + clearModified() { + if (this.state.isModified) { + this.state.isModified = false; + this.updateWindowTitle(); } - - async saveProject() { - if (!this.state.currentFilePath) { - // If no current path, use Save As instead - return this.saveProjectAs(); + } + + createNewProject() { + // Reset the canvas + this.pixelCanvas.clear(); + + // Reset the timeline + this.timeline.frames = []; + this.timeline.createNewFrame(); + this.timeline.updateFramesUI(); + + // Reset the project state + this.state.currentFilePath = null; + this.state.projectName = "Untitled"; + this.state.isModified = false; + this.state.metadata = { + title: "", + author: "", + message: "", + encoding: "plain", + }; + this.state.loreLayer = { + enabled: false, + sigils: [], + }; + + // Update the UI + this.updateWindowTitle(); + this.uiManager.showToast("Created new project", "success"); + } + + async openProject() { + try { + // Check for unsaved changes first + if (this.state.isModified) { + const { response } = await this.uiManager.showConfirmDialog( + "Unsaved Changes", + "Do you want to save your changes before opening another project?", + ["Save", "Don't Save", "Cancel"], + ); + + if (response === 0) { + // Save + const saveResult = await this.saveProject(); + if (!saveResult.success) { + return; // Don't proceed if save failed + } + } else if (response === 2) { + // Cancel + return; } - + // If response is 1 (Don't Save), proceed with opening + } + + // Show loading indicator + const progressToast = this.uiManager.showToast( + "Opening project...", + "info", + 0, + ); + + // Use Electron IPC to open a file dialog + const result = await window.voidAPI.openProject(); + + if (result.success && result.data) { try { - // Show saving indicator - const progressToast = this.uiManager.showToast('Preparing project data...', 'info', 0); - - // Save current frame first - this.timeline.frames[this.timeline.currentFrameIndex].setImageData( - this.pixelCanvas.getCanvasImageData() - ); - - // Prepare project data - const projectData = this.prepareProjectData(); - - if (!projectData || !projectData.frames || projectData.frames.length === 0) { - throw new Error('Invalid project data'); - } - - progressToast.updateMessage('Saving project file...'); - - // Use Electron IPC to save the file - const result = await window.voidAPI.saveProject(projectData); - - if (result.success) { - progressToast.close(); - this.clearModified(); - this.uiManager.showToast('Project saved successfully', 'success'); - return { success: true }; - } else { - throw new Error(result.error || 'Failed to save project file'); - } - } catch (error) { - console.error('Error saving project:', error); - this.uiManager.showToast(`Failed to save project: ${error.message}`, 'error', 5000); - - // Log detailed error for debugging - if (error.stack) { - console.error('Stack trace:', error.stack); + // Parse the project data + const projectData = result.data; + + // Validate project data + if ( + !projectData || + !projectData.frames || + !Array.isArray(projectData.frames) + ) { + throw new Error("Invalid project file format"); + } + + progressToast.updateMessage("Loading canvas size..."); + + // Load canvas size + if (projectData.canvasSize) { + if ( + !projectData.canvasSize.width || + !projectData.canvasSize.height || + projectData.canvasSize.width <= 0 || + projectData.canvasSize.height <= 0 + ) { + throw new Error("Invalid canvas dimensions"); } - - return { success: false, error: error.message }; - } - } - - async saveProjectAs() { - try { - // Show saving indicator - const progressToast = this.uiManager.showToast('Preparing project data...', 'info', 0); - - // Save current frame first - this.timeline.frames[this.timeline.currentFrameIndex].setImageData( - this.pixelCanvas.getCanvasImageData() + + this.pixelCanvas.setCanvasSize( + projectData.canvasSize.width, + projectData.canvasSize.height, ); - - // Prepare project data - const projectData = this.prepareProjectData(); - - if (!projectData || !projectData.frames || projectData.frames.length === 0) { - throw new Error('Invalid project data'); - } - - progressToast.updateMessage('Saving project file...'); - - // Use Electron IPC to save the file with dialog - const result = await window.voidAPI.saveProject(projectData); - - if (result.success) { - // Update project state - this.state.currentFilePath = result.filePath; - this.state.projectName = this.getFileNameFromPath(result.filePath); - this.clearModified(); - this.updateWindowTitle(); - - progressToast.close(); - this.uiManager.showToast('Project saved successfully', 'success'); - return { success: true }; - } else { - throw new Error(result.error || 'Failed to save project file'); - } - } catch (error) { - console.error('Error saving project:', error); - this.uiManager.showToast(`Failed to save project: ${error.message}`, 'error', 5000); - - // Log detailed error for debugging - if (error.stack) { - console.error('Stack trace:', error.stack); - } - - return { success: false, error: error.message }; - } - } - - prepareProjectData() { - // Create a complete project data object - return { - appVersion: '0.1.0', - timestamp: new Date().toISOString(), - canvasSize: { - width: this.pixelCanvas.width, - height: this.pixelCanvas.height - }, - frames: this.timeline.getFramesData(), - palette: this.paletteTool.currentPalette, - effects: this.effectsEngine.getEffectsSettings(), - metadata: this.state.metadata, - loreLayer: this.state.loreLayer - }; - } - - getFileNameFromPath(filePath) { - if (!filePath) return 'Untitled'; - - // Extract file name from path - const pathParts = filePath.split(/[/\\]/); - let fileName = pathParts[pathParts.length - 1]; - - // Remove extension - const extensionIndex = fileName.lastIndexOf('.'); - if (extensionIndex > 0) { - fileName = fileName.substring(0, extensionIndex); - } - - return fileName; - } - - async exportPNG() { - try { - // Show loading indicator - const progressToast = this.uiManager.showToast('Preparing PNG export...', 'info', 0); - - // Get canvas data URL - const dataUrl = this.pixelCanvas.getCanvasData(); - - if (!dataUrl || !dataUrl.startsWith('data:image/png;base64,')) { - throw new Error('Invalid PNG data generated'); - } - - progressToast.updateMessage('Saving PNG file...'); - - // Use Electron IPC to save the PNG - const result = await window.voidAPI.exportPng(dataUrl); - - if (result.success) { - progressToast.close(); - this.uiManager.showToast('PNG exported successfully', 'success'); - } else { - throw new Error(result.error || 'Failed to save PNG file'); - } - } catch (error) { - console.error('Error exporting PNG:', error); - this.uiManager.showToast(`Failed to export PNG: ${error.message}`, 'error', 5000); - - // Log detailed error for debugging - if (error.stack) { - console.error('Stack trace:', error.stack); - } - } - } - - async exportGIF() { - try { - // Show options dialog for GIF export - this.uiManager.showModal('Export GIF', this.createGifOptionsForm()); - } catch (error) { - console.error('Error showing GIF export dialog:', error); - this.uiManager.showToast('Failed to open GIF export options: ' + error.message, 'error'); - } - } - - createGifOptionsForm() { - const form = document.createElement('div'); - form.className = 'gif-options-form'; - - // Loop options - const loopGroup = document.createElement('div'); - loopGroup.className = 'form-group'; - - const loopLabel = document.createElement('label'); - loopLabel.textContent = 'Loop Count:'; - - const loopInput = document.createElement('input'); - loopInput.type = 'number'; - loopInput.id = 'gif-loop-count'; - loopInput.min = '0'; - loopInput.max = '100'; - loopInput.value = '0'; - - const loopHelp = document.createElement('small'); - loopHelp.textContent = '0 = infinite loop'; - - loopGroup.appendChild(loopLabel); - loopGroup.appendChild(loopInput); - loopGroup.appendChild(loopHelp); - - // Quality options - const qualityGroup = document.createElement('div'); - qualityGroup.className = 'form-group'; - - const qualityLabel = document.createElement('label'); - qualityLabel.textContent = 'Quality:'; - - const qualitySlider = document.createElement('input'); - qualitySlider.type = 'range'; - qualitySlider.id = 'gif-quality'; - qualitySlider.min = '1'; - qualitySlider.max = '20'; - qualitySlider.value = '10'; - - const qualityValue = document.createElement('span'); - qualityValue.id = 'quality-value'; - qualityValue.textContent = '10'; - - qualitySlider.addEventListener('input', () => { - qualityValue.textContent = qualitySlider.value; - }); - - qualityGroup.appendChild(qualityLabel); - qualityGroup.appendChild(qualitySlider); - qualityGroup.appendChild(qualityValue); - - // Dithering option - const ditherGroup = document.createElement('div'); - ditherGroup.className = 'form-group'; - - const ditherLabel = document.createElement('label'); - ditherLabel.textContent = 'Apply Dithering:'; - - const ditherCheckbox = document.createElement('input'); - ditherCheckbox.type = 'checkbox'; - ditherCheckbox.id = 'gif-dither'; - - ditherGroup.appendChild(ditherLabel); - ditherGroup.appendChild(ditherCheckbox); - - // Buttons - const buttonGroup = document.createElement('div'); - buttonGroup.className = 'form-buttons'; - - const exportButton = document.createElement('button'); - exportButton.type = 'button'; - exportButton.className = 'save-button'; - exportButton.textContent = 'Export GIF'; - exportButton.addEventListener('click', () => { - this.uiManager.closeModal(); - - // Get options - const options = { - quality: parseInt(qualitySlider.value), - dither: ditherCheckbox.checked, - loop: parseInt(loopInput.value) - }; - - this.processGifExport(options); - }); - - const cancelButton = document.createElement('button'); - cancelButton.type = 'button'; - cancelButton.className = 'cancel-button'; - cancelButton.textContent = 'Cancel'; - cancelButton.addEventListener('click', () => { - this.uiManager.closeModal(); - }); - - buttonGroup.appendChild(exportButton); - buttonGroup.appendChild(cancelButton); - - // Assemble form - form.appendChild(loopGroup); - form.appendChild(qualityGroup); - form.appendChild(ditherGroup); - form.appendChild(buttonGroup); - - return form; - } - - async processGifExport(options) { - try { - // Show loading indicator with progress - const progressToast = this.uiManager.showToast('Preparing frames for GIF export...', 'info', 0); - - // Save current frame first to ensure it's included - this.timeline.frames[this.timeline.currentFrameIndex].setImageData( - this.pixelCanvas.getCanvasImageData() + } + + progressToast.updateMessage("Loading frames..."); + + // Load frames + if (projectData.frames && Array.isArray(projectData.frames)) { + await this.timeline.setFramesFromData( + projectData.frames, + (progress) => { + progressToast.updateMessage( + `Loading frames... ${Math.round(progress * 100)}%`, + ); + }, ); - - // Validate frames - if (this.timeline.frames.length === 0) { - throw new Error('No frames to export'); - } - - // Update progress - progressToast.updateMessage('Generating GIF...'); - - // Create GIF with options and progress callback - const gifData = await this.gifExporter.exportGif(options, (progress) => { - progressToast.updateMessage(`Generating GIF... ${Math.round(progress * 100)}%`); - }); - - if (!gifData || gifData.length === 0) { - throw new Error('Failed to generate GIF data'); - } - - // Update progress - progressToast.updateMessage('Saving GIF file...'); - - // Use Electron IPC to save the GIF - const result = await window.voidAPI.exportGif(gifData); - - if (result.success) { - // Close progress toast - progressToast.close(); - this.uiManager.showToast('GIF exported successfully', 'success'); - } else { - throw new Error(result.error || 'Failed to save GIF file'); - } - } catch (error) { - console.error('Error exporting GIF:', error); - this.uiManager.showToast(`Failed to export GIF: ${error.message}`, 'error', 5000); - - // Log detailed error for debugging - if (error.stack) { - console.error('Stack trace:', error.stack); - } + } + + progressToast.updateMessage("Loading palette..."); + + // Load palette + if (projectData.palette) { + this.paletteTool.setPalette(projectData.palette); + } + + progressToast.updateMessage("Loading effects..."); + + // Load effects + if (projectData.effects) { + this.effectsEngine.setEffectsSettings(projectData.effects); + } + + progressToast.updateMessage("Loading metadata..."); + + // Load metadata + if (projectData.metadata) { + this.state.metadata = projectData.metadata; + } + + // Load lore layer + if (projectData.loreLayer) { + this.state.loreLayer = projectData.loreLayer; + } + + // Update project state + this.state.currentFilePath = result.filePath; + this.state.projectName = this.getFileNameFromPath(result.filePath); + this.clearModified(); + + progressToast.close(); + this.uiManager.showToast("Project loaded successfully", "success"); + } catch (parseError) { + throw new Error( + `Failed to parse project file: ${parseError.message}`, + ); } + } else { + throw new Error(result.error || "No file selected"); + } + } catch (error) { + console.error("Error opening project:", error); + this.uiManager.showToast( + `Failed to open project: ${error.message}`, + "error", + 5000, + ); + + // Log detailed error for debugging + if (error.stack) { + console.error("Stack trace:", error.stack); + } } - - async exportSpriteSheet() { - try { - // Show loading indicator - const progressToast = this.uiManager.showToast('Preparing sprite sheet...', 'info', 0); - - // Validate frames - if (this.timeline.frames.length === 0) { - throw new Error('No frames available for sprite sheet export'); - } - - // Save current frame first to ensure it's included - this.timeline.frames[this.timeline.currentFrameIndex].setImageData( - this.pixelCanvas.getCanvasImageData() - ); - - progressToast.updateMessage('Generating sprite sheet...'); - - // Generate sprite sheet - const spriteSheetDataUrl = await this.gifExporter.exportSpriteSheet((progress) => { - progressToast.updateMessage(`Generating sprite sheet... ${Math.round(progress * 100)}%`); - }); - - if (!spriteSheetDataUrl || !spriteSheetDataUrl.startsWith('data:image/png;base64,')) { - throw new Error('Invalid sprite sheet data generated'); - } - - progressToast.updateMessage('Saving sprite sheet...'); - - // Use Electron IPC to save the sprite sheet - const result = await window.voidAPI.exportPng(spriteSheetDataUrl); - - if (result.success) { - progressToast.close(); - this.uiManager.showToast('Sprite sheet exported successfully', 'success'); - } else { - throw new Error(result.error || 'Failed to save sprite sheet'); - } - } catch (error) { - console.error('Error exporting sprite sheet:', error); - this.uiManager.showToast(`Failed to export sprite sheet: ${error.message}`, 'error', 5000); - - // Log detailed error for debugging - if (error.stack) { - console.error('Stack trace:', error.stack); - } - } + } + + async saveProject() { + if (!this.state.currentFilePath) { + // If no current path, use Save As instead + return this.saveProjectAs(); } - - toggleRulers() { - // Placeholder for ruler functionality - this.uiManager.showToast('Rulers feature coming soon', 'info'); + + try { + // Show saving indicator + const progressToast = this.uiManager.showToast( + "Preparing project data...", + "info", + 0, + ); + + // Save current frame first + this.timeline.frames[this.timeline.currentFrameIndex].setImageData( + this.pixelCanvas.getCanvasImageData(), + ); + + // Prepare project data + const projectData = this.prepareProjectData(); + + if ( + !projectData || + !projectData.frames || + projectData.frames.length === 0 + ) { + throw new Error("Invalid project data"); + } + + progressToast.updateMessage("Saving project file..."); + + // Use Electron IPC to save the file + const result = await window.voidAPI.saveProject(projectData); + + if (result.success) { + progressToast.close(); + this.clearModified(); + this.uiManager.showToast("Project saved successfully", "success"); + return { success: true }; + } else { + throw new Error(result.error || "Failed to save project file"); + } + } catch (error) { + console.error("Error saving project:", error); + this.uiManager.showToast( + `Failed to save project: ${error.message}`, + "error", + 5000, + ); + + // Log detailed error for debugging + if (error.stack) { + console.error("Stack trace:", error.stack); + } + + return { success: false, error: error.message }; } - - copySelection() { - // Placeholder for copy functionality - this.uiManager.showToast('Copy feature coming soon', 'info'); + } + + async saveProjectAs() { + try { + // Show saving indicator + const progressToast = this.uiManager.showToast( + "Preparing project data...", + "info", + 0, + ); + + // Save current frame first + this.timeline.frames[this.timeline.currentFrameIndex].setImageData( + this.pixelCanvas.getCanvasImageData(), + ); + + // Prepare project data + const projectData = this.prepareProjectData(); + + if ( + !projectData || + !projectData.frames || + projectData.frames.length === 0 + ) { + throw new Error("Invalid project data"); + } + + progressToast.updateMessage("Saving project file..."); + + // Use Electron IPC to save the file with dialog + const result = await window.voidAPI.saveProject(projectData); + + if (result.success) { + // Update project state + this.state.currentFilePath = result.filePath; + this.state.projectName = this.getFileNameFromPath(result.filePath); + this.clearModified(); + this.updateWindowTitle(); + + progressToast.close(); + this.uiManager.showToast("Project saved successfully", "success"); + return { success: true }; + } else { + throw new Error(result.error || "Failed to save project file"); + } + } catch (error) { + console.error("Error saving project:", error); + this.uiManager.showToast( + `Failed to save project: ${error.message}`, + "error", + 5000, + ); + + // Log detailed error for debugging + if (error.stack) { + console.error("Stack trace:", error.stack); + } + + return { success: false, error: error.message }; } - - cutSelection() { - // Placeholder for cut functionality - this.uiManager.showToast('Cut feature coming soon', 'info'); + } + + prepareProjectData() { + // Create a complete project data object + return { + appVersion: "0.1.0", + timestamp: new Date().toISOString(), + canvasSize: { + width: this.pixelCanvas.width, + height: this.pixelCanvas.height, + }, + frames: this.timeline.getFramesData(), + palette: this.paletteTool.currentPalette, + effects: this.effectsEngine.getEffectsSettings(), + metadata: this.state.metadata, + loreLayer: this.state.loreLayer, + }; + } + + getFileNameFromPath(filePath) { + if (!filePath) return "Untitled"; + + // Extract file name from path + const pathParts = filePath.split(/[/\\]/); + let fileName = pathParts[pathParts.length - 1]; + + // Remove extension + const extensionIndex = fileName.lastIndexOf("."); + if (extensionIndex > 0) { + fileName = fileName.substring(0, extensionIndex); } - - pasteSelection() { - // Placeholder for paste functionality - this.uiManager.showToast('Paste feature coming soon', 'info'); + + return fileName; + } + + async exportPNG() { + try { + // Show loading indicator + const progressToast = this.uiManager.showToast( + "Preparing PNG export...", + "info", + 0, + ); + + // Get canvas data URL + const dataUrl = this.pixelCanvas.getCanvasData(); + + if (!dataUrl || !dataUrl.startsWith("data:image/png;base64,")) { + throw new Error("Invalid PNG data generated"); + } + + progressToast.updateMessage("Saving PNG file..."); + + // Use Electron IPC to save the PNG + const result = await window.voidAPI.exportPng(dataUrl); + + if (result.success) { + progressToast.close(); + this.uiManager.showToast("PNG exported successfully", "success"); + } else { + throw new Error(result.error || "Failed to save PNG file"); + } + } catch (error) { + console.error("Error exporting PNG:", error); + this.uiManager.showToast( + `Failed to export PNG: ${error.message}`, + "error", + 5000, + ); + + // Log detailed error for debugging + if (error.stack) { + console.error("Stack trace:", error.stack); + } } - - selectAll() { - // Placeholder for select all functionality - this.uiManager.showToast('Select All feature coming soon', 'info'); + } + + async exportGIF() { + try { + // Show options dialog for GIF export + this.uiManager.showModal("Export GIF", this.createGifOptionsForm()); + } catch (error) { + console.error("Error showing GIF export dialog:", error); + this.uiManager.showToast( + "Failed to open GIF export options: " + error.message, + "error", + ); } - - deselect() { - // Placeholder for deselect functionality - this.uiManager.showToast('Deselect feature coming soon', 'info'); + } + + createGifOptionsForm() { + const form = document.createElement("div"); + form.className = "gif-options-form"; + + // Loop options + const loopGroup = document.createElement("div"); + loopGroup.className = "form-group"; + + const loopLabel = document.createElement("label"); + loopLabel.textContent = "Loop Count:"; + + const loopInput = document.createElement("input"); + loopInput.type = "number"; + loopInput.id = "gif-loop-count"; + loopInput.min = "0"; + loopInput.max = "100"; + loopInput.value = "0"; + + const loopHelp = document.createElement("small"); + loopHelp.textContent = "0 = infinite loop"; + + loopGroup.appendChild(loopLabel); + loopGroup.appendChild(loopInput); + loopGroup.appendChild(loopHelp); + + // Quality options + const qualityGroup = document.createElement("div"); + qualityGroup.className = "form-group"; + + const qualityLabel = document.createElement("label"); + qualityLabel.textContent = "Quality:"; + + const qualitySlider = document.createElement("input"); + qualitySlider.type = "range"; + qualitySlider.id = "gif-quality"; + qualitySlider.min = "1"; + qualitySlider.max = "20"; + qualitySlider.value = "10"; + + const qualityValue = document.createElement("span"); + qualityValue.id = "quality-value"; + qualityValue.textContent = "10"; + + qualitySlider.addEventListener("input", () => { + qualityValue.textContent = qualitySlider.value; + }); + + qualityGroup.appendChild(qualityLabel); + qualityGroup.appendChild(qualitySlider); + qualityGroup.appendChild(qualityValue); + + // Dithering option + const ditherGroup = document.createElement("div"); + ditherGroup.className = "form-group"; + + const ditherLabel = document.createElement("label"); + ditherLabel.textContent = "Apply Dithering:"; + + const ditherCheckbox = document.createElement("input"); + ditherCheckbox.type = "checkbox"; + ditherCheckbox.id = "gif-dither"; + + ditherGroup.appendChild(ditherLabel); + ditherGroup.appendChild(ditherCheckbox); + + // Buttons + const buttonGroup = document.createElement("div"); + buttonGroup.className = "form-buttons"; + + const exportButton = document.createElement("button"); + exportButton.type = "button"; + exportButton.className = "save-button"; + exportButton.textContent = "Export GIF"; + exportButton.addEventListener("click", () => { + this.uiManager.closeModal(); + + // Get options + const options = { + quality: parseInt(qualitySlider.value), + dither: ditherCheckbox.checked, + loop: parseInt(loopInput.value), + }; + + this.processGifExport(options); + }); + + const cancelButton = document.createElement("button"); + cancelButton.type = "button"; + cancelButton.className = "cancel-button"; + cancelButton.textContent = "Cancel"; + cancelButton.addEventListener("click", () => { + this.uiManager.closeModal(); + }); + + buttonGroup.appendChild(exportButton); + buttonGroup.appendChild(cancelButton); + + // Assemble form + form.appendChild(loopGroup); + form.appendChild(qualityGroup); + form.appendChild(ditherGroup); + form.appendChild(buttonGroup); + + return form; + } + + async processGifExport(options) { + try { + // Show loading indicator with progress + const progressToast = this.uiManager.showToast( + "Preparing frames for GIF export...", + "info", + 0, + ); + + // Save current frame first to ensure it's included + this.timeline.frames[this.timeline.currentFrameIndex].setImageData( + this.pixelCanvas.getCanvasImageData(), + ); + + // Validate frames + if (this.timeline.frames.length === 0) { + throw new Error("No frames to export"); + } + + // Update progress + progressToast.updateMessage("Generating GIF..."); + + // Create GIF with options and progress callback + const gifData = await this.gifExporter.exportGif(options, (progress) => { + progressToast.updateMessage( + `Generating GIF... ${Math.round(progress * 100)}%`, + ); + }); + + if (!gifData || gifData.length === 0) { + throw new Error("Failed to generate GIF data"); + } + + // Update progress + progressToast.updateMessage("Saving GIF file..."); + + // Use Electron IPC to save the GIF + const result = await window.voidAPI.exportGif(gifData); + + if (result.success) { + // Close progress toast + progressToast.close(); + this.uiManager.showToast("GIF exported successfully", "success"); + } else { + throw new Error(result.error || "Failed to save GIF file"); + } + } catch (error) { + console.error("Error exporting GIF:", error); + this.uiManager.showToast( + `Failed to export GIF: ${error.message}`, + "error", + 5000, + ); + + // Log detailed error for debugging + if (error.stack) { + console.error("Stack trace:", error.stack); + } } - - toggleLoreLayer() { - // Toggle lore layer visibility - this.state.loreLayer.enabled = !this.state.loreLayer.enabled; - - // Update UI + } + + async exportSpriteSheet() { + try { + // Show loading indicator + const progressToast = this.uiManager.showToast( + "Preparing sprite sheet...", + "info", + 0, + ); + + // Validate frames + if (this.timeline.frames.length === 0) { + throw new Error("No frames available for sprite sheet export"); + } + + // Save current frame first to ensure it's included + this.timeline.frames[this.timeline.currentFrameIndex].setImageData( + this.pixelCanvas.getCanvasImageData(), + ); + + progressToast.updateMessage("Generating sprite sheet..."); + + // Generate sprite sheet + const spriteSheetDataUrl = await this.gifExporter.exportSpriteSheet( + (progress) => { + progressToast.updateMessage( + `Generating sprite sheet... ${Math.round(progress * 100)}%`, + ); + }, + ); + + if ( + !spriteSheetDataUrl || + !spriteSheetDataUrl.startsWith("data:image/png;base64,") + ) { + throw new Error("Invalid sprite sheet data generated"); + } + + progressToast.updateMessage("Saving sprite sheet..."); + + // Use Electron IPC to save the sprite sheet + const result = await window.voidAPI.exportPng(spriteSheetDataUrl); + + if (result.success) { + progressToast.close(); this.uiManager.showToast( - this.state.loreLayer.enabled ? 'Lore layer activated' : 'Lore layer deactivated', - 'info' + "Sprite sheet exported successfully", + "success", ); - - // Mark project as modified - this.markAsModified(); - } - - saveMetadata(metadata) { - // Update metadata - this.state.metadata = { - ...this.state.metadata, - ...metadata - }; - - // Show confirmation - this.uiManager.showToast('Metadata ritual recorded', 'success'); - - // Mark project as modified - this.markAsModified(); - } - - addSigil(sigilData) { - // Add sigil to lore layer - this.state.loreLayer.sigils.push({ - ...sigilData, - id: Date.now().toString(), - timestamp: new Date().toISOString() - }); - - // Show confirmation - this.uiManager.showToast('Hidden sigil embedded', 'success'); - - // Mark project as modified - this.markAsModified(); + } else { + throw new Error(result.error || "Failed to save sprite sheet"); + } + } catch (error) { + console.error("Error exporting sprite sheet:", error); + this.uiManager.showToast( + `Failed to export sprite sheet: ${error.message}`, + "error", + 5000, + ); + + // Log detailed error for debugging + if (error.stack) { + console.error("Stack trace:", error.stack); + } } - - applyGlitch(options) { - // Apply selected glitch effect - switch (options.type) { - case 'random': - this.glitchTool.applyRandomGlitch(); - break; - case 'shift': - this.glitchTool.shiftRows( - this.pixelCanvas.getCanvasImageData().data, - this.pixelCanvas.width, - this.pixelCanvas.height - ); - break; - case 'channel': - this.glitchTool.channelShift( - this.pixelCanvas.getCanvasImageData().data, - this.pixelCanvas.width, - this.pixelCanvas.height - ); - break; - case 'sort': - this.glitchTool.pixelSort( - this.pixelCanvas.getCanvasImageData().data, - this.pixelCanvas.width, - this.pixelCanvas.height - ); - break; - case 'noise': - this.glitchTool.addNoise(options.intensity); - break; - case 'corrupt': - this.glitchTool.corruptData(options.intensity); - break; - } - - // Show confirmation - this.uiManager.showToast('Glitch injected', 'success'); - - // Mark project as modified - this.markAsModified(); + } + + toggleRulers() { + // Placeholder for ruler functionality + this.uiManager.showToast("Rulers feature coming soon", "info"); + } + + copySelection() { + // Placeholder for copy functionality + this.uiManager.showToast("Copy feature coming soon", "info"); + } + + cutSelection() { + // Placeholder for cut functionality + this.uiManager.showToast("Cut feature coming soon", "info"); + } + + pasteSelection() { + // Placeholder for paste functionality + this.uiManager.showToast("Paste feature coming soon", "info"); + } + + selectAll() { + // Placeholder for select all functionality + this.uiManager.showToast("Select All feature coming soon", "info"); + } + + deselect() { + // Placeholder for deselect functionality + this.uiManager.showToast("Deselect feature coming soon", "info"); + } + + toggleLoreLayer() { + // Toggle lore layer visibility + this.state.loreLayer.enabled = !this.state.loreLayer.enabled; + + // Update UI + this.uiManager.showToast( + this.state.loreLayer.enabled + ? "Lore layer activated" + : "Lore layer deactivated", + "info", + ); + + // Mark project as modified + this.markAsModified(); + } + + saveMetadata(metadata) { + // Update metadata + this.state.metadata = { + ...this.state.metadata, + ...metadata, + }; + + // Show confirmation + this.uiManager.showToast("Metadata ritual recorded", "success"); + + // Mark project as modified + this.markAsModified(); + } + + addSigil(sigilData) { + // Add sigil to lore layer + this.state.loreLayer.sigils.push({ + ...sigilData, + id: Date.now().toString(), + timestamp: new Date().toISOString(), + }); + + // Show confirmation + this.uiManager.showToast("Hidden sigil embedded", "success"); + + // Mark project as modified + this.markAsModified(); + } + + applyGlitch(options) { + // Apply selected glitch effect + switch (options.type) { + case "random": + this.glitchTool.applyRandomGlitch(); + break; + case "shift": + this.glitchTool.shiftRows( + this.pixelCanvas.getCanvasImageData().data, + this.pixelCanvas.width, + this.pixelCanvas.height, + ); + break; + case "channel": + this.glitchTool.channelShift( + this.pixelCanvas.getCanvasImageData().data, + this.pixelCanvas.width, + this.pixelCanvas.height, + ); + break; + case "sort": + this.glitchTool.pixelSort( + this.pixelCanvas.getCanvasImageData().data, + this.pixelCanvas.width, + this.pixelCanvas.height, + ); + break; + case "noise": + this.glitchTool.addNoise(options.intensity); + break; + case "corrupt": + this.glitchTool.corruptData(options.intensity); + break; } - - updateEffectsForTheme() { - // Adjust effects based on current theme - const theme = this.themeManager.getCurrentTheme(); - - switch (theme.id) { - case 'lain-dive': - // Purple-tinted CRT effects - this.effectsEngine.settings.crt.intensity = 0.6; - break; - case 'morrowind-glyph': - // Sepia-toned grain - this.effectsEngine.settings.grain.intensity = 0.7; - break; - case 'monolith': - // High contrast, less effects - this.effectsEngine.settings.grain.intensity = 0.3; - this.effectsEngine.settings.crt.intensity = 0.4; - break; - } - - // Update effects - this.effectsEngine.updateEffects(); + + // Show confirmation + this.uiManager.showToast("Glitch injected", "success"); + + // Mark project as modified + this.markAsModified(); + } + + updateEffectsForTheme() { + // Adjust effects based on current theme + const theme = this.themeManager.getCurrentTheme(); + + switch (theme.id) { + case "lain-dive": + // Purple-tinted CRT effects + this.effectsEngine.settings.crt.intensity = 0.6; + break; + case "morrowind-glyph": + // Sepia-toned grain + this.effectsEngine.settings.grain.intensity = 0.7; + break; + case "monolith": + // High contrast, less effects + this.effectsEngine.settings.grain.intensity = 0.3; + this.effectsEngine.settings.crt.intensity = 0.4; + break; } + + // Update effects + this.effectsEngine.updateEffects(); + } }