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 diff --git a/.gitignore b/.gitignore index 3c3629e..37d7e73 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.env 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/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/.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/.history/src/scripts/ui/MenuSystem_20250519200450.js b/.history/src/scripts/ui/MenuSystem_20250519200450.js index b5219fe..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); } @@ -336,7 +342,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 b5219fe..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); } @@ -336,7 +342,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 e4e2801..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); } @@ -341,7 +347,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 cd48cfa..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); } @@ -343,7 +349,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 aa64f6c..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); } @@ -344,7 +350,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 55710c6..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); } @@ -347,7 +353,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 0e394c7..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); } @@ -351,7 +357,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 db0bc6a..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); } @@ -355,7 +361,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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 7e33967..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); } @@ -359,7 +365,7 @@ class MenuSystem { removeMenuItem(menuItemId) { const menuItem = document.getElementById(menuItemId); - if (menuItem && menuItem.parentNode) { + if (menuItem?.parentNode) { menuItem.parentNode.removeChild(menuItem); } } @@ -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/.whitesource b/.whitesource new file mode 100644 index 0000000..9c7ae90 --- /dev/null +++ b/.whitesource @@ -0,0 +1,14 @@ +{ + "scanSettings": { + "baseBranches": [] + }, + "checkRunSettings": { + "vulnerableCheckRunConclusionLevel": "failure", + "displayMode": "diff", + "useMendCheckNames": true + }, + "issueSettings": { + "minSeverityLevel": "LOW", + "issueType": "DEPENDENCY" + } +} \ No newline at end of file diff --git a/src/scripts/app.js b/src/scripts/app.js index a2cbc36..dd777c4 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('maximize-button').textContent = result.isMaximized ? '□' : '[]'; }); }); + + document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow()); + } - 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'); - } - }); - }); - - // 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'); + } + ); + } - // Zoom in or out based on wheel direction - if (e.deltaY < 0) { - pixelCanvas.zoomIn(); + 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 { - 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 */ @@ -667,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; @@ -735,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(); 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 diff --git a/src/scripts/ui/MenuSystem.js b/src/scripts/ui/MenuSystem.js index 7e33967..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 && menuItem.parentNode) { - menuItem.parentNode.removeChild(menuItem); + if (menuItem?.parentNode) { + 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') } } }