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

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

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

+ Select Canvas Size Template +

+

+ Choose a canvas size to begin your creation +

+ ${sizesHTML} + `; + + uiManager.showModal('Conjuration', modalContent, null, false); + + // Add event listeners to size options + document.querySelectorAll('.canvas-size-option').forEach(option => { + option.addEventListener('click', () => { + const width = parseInt(option.dataset.width); + const height = parseInt(option.dataset.height); + + // Resize the canvas + pixelCanvas.resize(width, height, false); + updateCanvasSizeDisplay(); + + // Close the modal + uiManager.hideModal(); + + // Show confirmation message + uiManager.showToast(`Canvas set to ${width}×${height}`, 'success'); + }); + }); + + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } + + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } + + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } + + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } + + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } + + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .canvas-size-info { + text-align: center; + width: 100%; + } + + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } + + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } + + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } + } +}); diff --git a/.history/src/scripts/canvas/PixelCanvas_20250605030852.js b/.history/src/scripts/canvas/PixelCanvas_20250605030852.js new file mode 100644 index 0000000..f34352d --- /dev/null +++ b/.history/src/scripts/canvas/PixelCanvas_20250605030852.js @@ -0,0 +1,996 @@ +/** + * PixelCanvas Class + * + * Handles the main drawing canvas, pixel manipulation, and rendering. + */ +class PixelCanvas { + /** + * Create a new PixelCanvas + * @param {Object} options - Configuration options + * @param {string} options.canvasId - ID of the main canvas element + * @param {string} options.effectsCanvasId - ID of the effects overlay canvas + * @param {string} options.uiCanvasId - ID of the UI overlay canvas + * @param {number} options.width - Width in pixels + * @param {number} options.height - Height in pixels + * @param {number} options.pixelSize - Size of each pixel in screen pixels + */ + constructor(options) { + // Canvas elements + this.canvas = document.getElementById(options.canvasId); + this.effectsCanvas = document.getElementById(options.effectsCanvasId); + this.uiCanvas = document.getElementById(options.uiCanvasId); + + // Canvas contexts + this.ctx = this.canvas.getContext('2d'); + this.effectsCtx = this.effectsCanvas.getContext('2d'); + this.uiCtx = this.uiCanvas.getContext('2d'); + + // Canvas dimensions + this.width = options.width || 64; + this.height = options.height || 64; + this.pixelSize = options.pixelSize || 8; + + // Zoom level + this.zoom = 1; + + // Pixel data + this.pixels = new Array(this.width * this.height).fill('#000000'); + + // Undo/Redo history + this.history = []; + this.historyIndex = -1; + this.maxHistorySize = 50; // Maximum number of states to store + + // Save initial state + this.saveToHistory(); + + // Effects settings + this.effects = { + grain: false, + static: false, + glitch: false, + crt: false, + scanLines: false, + vignette: false, + noise: false, + pixelate: false, + intensity: 0.5 + }; + + // Animation frame for effects + this.effectsAnimationFrame = null; + + // Mouse state + this.isDrawing = false; + this.lastX = 0; + this.lastY = 0; + + // Grid display + this.showGrid = true; + + // Initialize the canvas + this.initCanvas(); + + // Set up event listeners + this.setupEventListeners(); + + // Start the effects animation loop + this.animateEffects(); + } + + /** + * Initialize the canvas with the correct dimensions + */ + initCanvas() { + // Set canvas dimensions + this.canvas.width = this.width * this.pixelSize * this.zoom; + this.canvas.height = this.height * this.pixelSize * this.zoom; + + // Set effects canvas dimensions + this.effectsCanvas.width = this.canvas.width; + this.effectsCanvas.height = this.canvas.height; + + // Set UI canvas dimensions + this.uiCanvas.width = this.canvas.width; + this.uiCanvas.height = this.canvas.height; + + // Set rendering options for pixel art + this.ctx.imageSmoothingEnabled = false; + this.effectsCtx.imageSmoothingEnabled = false; + this.uiCtx.imageSmoothingEnabled = false; + + // Clear the canvas + this.clear(); + + // Draw the initial grid + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Set up event listeners for mouse/touch interaction + */ + setupEventListeners() { + // Bind event handlers to this instance + this._boundHandleMouseDown = this.handleMouseDown.bind(this); + this._boundHandleMouseMove = this.handleMouseMove.bind(this); + this._boundHandleMouseUp = this.handleMouseUp.bind(this); + this._boundUpdateCursorPosition = this.updateCursorPosition.bind(this); + this._boundHandleContextMenu = (e) => { e.preventDefault(); }; + this._boundHandleMouseLeave = () => { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + }; + + // Mouse events + this.canvas.addEventListener('mousedown', this._boundHandleMouseDown); + document.addEventListener('mousemove', this._boundHandleMouseMove); + document.addEventListener('mouseup', this._boundHandleMouseUp); + + // Prevent context menu on right-click + this.canvas.addEventListener('contextmenu', this._boundHandleContextMenu); + + // Update cursor position display + this.canvas.addEventListener('mousemove', this._boundUpdateCursorPosition); + + // Mouse leave + this.canvas.addEventListener('mouseleave', this._boundHandleMouseLeave); + } + + /** + * Handle mouse down event + * @param {MouseEvent} e - Mouse event + */ + handleMouseDown(e) { + this.isDrawing = true; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Store last position + this.lastX = x; + this.lastY = y; + + // Save the current state before drawing + this.saveToHistory(); + + // Draw a single pixel + this.drawPixel(x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Render the canvas + this.render(); + } + + /** + * Handle mouse move event + * @param {MouseEvent} e - Mouse event + */ + handleMouseMove(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Only draw if the position has changed + if (x !== this.lastX || y !== this.lastY) { + // Draw a line from last position to current position + this.drawLine(this.lastX, this.lastY, x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Update last position + this.lastX = x; + this.lastY = y; + + // Render the canvas + this.render(); + } + } + + /** + * Handle mouse up event + */ + handleMouseUp() { + this.isDrawing = false; + } + + /** + * Update the cursor position display + * @param {MouseEvent} e - Mouse event + */ + updateCursorPosition(e) { + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + document.getElementById('cursor-position').textContent = `X: ${x} Y: ${y}`; + } else { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + } + } + + /** + * Draw a single pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawPixel(x, y, color) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + this.pixels[index] = color; + } + } + + /** + * Draw a line using Bresenham's algorithm + * @param {number} x0 - Starting X coordinate + * @param {number} y0 - Starting Y coordinate + * @param {number} x1 - Ending X coordinate + * @param {number} y1 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawLine(x0, y0, x1, y1, color) { + const dx = Math.abs(x1 - x0); + const dy = Math.abs(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dx - dy; + + while (true) { + this.drawPixel(x0, y0, color); + + if (x0 === x1 && y0 === y1) break; + + const e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + /** + * Draw a rectangle + * @param {number} x - X coordinate of top-left corner + * @param {number} y - Y coordinate of top-left corner + * @param {number} width - Width of rectangle + * @param {number} height - Height of rectangle + * @param {string} color - Color in hex format + * @param {boolean} fill - Whether to fill the rectangle + */ + drawRect(x, y, width, height, color, fill = false) { + if (fill) { + for (let i = x; i < x + width; i++) { + for (let j = y; j < y + height; j++) { + this.drawPixel(i, j, color); + } + } + } else { + // Draw horizontal lines + for (let i = x; i < x + width; i++) { + this.drawPixel(i, y, color); + this.drawPixel(i, y + height - 1, color); + } + + // Draw vertical lines + for (let j = y; j < y + height; j++) { + this.drawPixel(x, j, color); + this.drawPixel(x + width - 1, j, color); + } + } + } + + /** + * Draw an ellipse + * @param {number} xc - X coordinate of center + * @param {number} yc - Y coordinate of center + * @param {number} a - Semi-major axis + * @param {number} b - Semi-minor axis + * @param {string} color - Color in hex format + */ + drawEllipse(xc, yc, a, b, color) { + let x = 0; + let y = b; + let a2 = a * a; + let b2 = b * b; + let d = b2 - a2 * b + a2 / 4; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + + while (a2 * (y - 0.5) > b2 * (x + 1)) { + if (d < 0) { + d += b2 * (2 * x + 3); + } else { + d += b2 * (2 * x + 3) + a2 * (-2 * y + 2); + y--; + } + x++; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + + d = b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2; + + while (y > 0) { + if (d < 0) { + d += b2 * (2 * x + 2) + a2 * (-2 * y + 3); + x++; + } else { + d += a2 * (-2 * y + 3); + } + y--; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + } + + /** + * Fill an area with a color (flood fill) + * @param {number} x - Starting X coordinate + * @param {number} y - Starting Y coordinate + * @param {string} fillColor - Color to fill with + */ + floodFill(x, y, fillColor) { + const targetColor = this.getPixel(x, y); + + // Don't fill if the target color is the same as the fill color + if (targetColor === fillColor) return; + + // Use a more efficient stack-based approach instead of queue + // This avoids the overhead of shift() operations + const stack = [{x, y}]; + + // Create a visited map to avoid checking the same pixel multiple times + const visited = new Set(); + const getKey = (x, y) => `${x},${y}`; + + while (stack.length > 0) { + const {x, y} = stack.pop(); + const key = getKey(x, y); + + // Skip if already visited + if (visited.has(key)) continue; + + // Mark as visited + visited.add(key); + + // Check if this pixel is the target color + if (this.getPixel(x, y) === targetColor) { + // Set the pixel to the fill color + this.drawPixel(x, y, fillColor); + + // Add adjacent pixels to the stack + // Check bounds before adding to avoid unnecessary getPixel calls + if (x > 0) stack.push({x: x - 1, y}); + if (x < this.width - 1) stack.push({x: x + 1, y}); + if (y > 0) stack.push({x, y: y - 1}); + if (y < this.height - 1) stack.push({x, y: y + 1}); + } + } + } + + /** + * Get the color of a pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @returns {string} Color in hex format + */ + getPixel(x, y) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + return this.pixels[index]; + } + return null; + } + + /** + * Clear the canvas + */ + clear() { + // Clear pixel data + this.pixels.fill('#000000'); + + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Clear UI canvas + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + } + + /** + * Render the canvas + */ + render() { + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Always use ImageData for rendering for consistent performance + const imageData = this.ctx.createImageData(this.width, this.height); + const data = imageData.data; + + for (let i = 0; i < this.pixels.length; i++) { + const color = this.pixels[i]; + // Parse hex color to RGB + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + + // Set RGBA values (4 bytes per pixel) + const pixelIndex = i * 4; + data[pixelIndex] = r; + data[pixelIndex + 1] = g; + data[pixelIndex + 2] = b; + data[pixelIndex + 3] = 255; // Alpha (fully opaque) + } + + // Create temporary canvas for efficient scaling + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.putImageData(imageData, 0, 0); + + // Draw scaled image + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage( + tempCanvas, + 0, 0, this.width, this.height, + 0, 0, this.canvas.width, this.canvas.height + ); + + // Draw grid if enabled + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Draw the grid + */ + drawGrid() { + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + + if (this.zoom >= 4) { + this.uiCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; + this.uiCtx.lineWidth = 1; + + // Use a single path for all grid lines for better performance + this.uiCtx.beginPath(); + + // Draw vertical lines + for (let x = 0; x <= this.width; x++) { + const xPos = x * this.pixelSize * this.zoom; + this.uiCtx.moveTo(xPos, 0); + this.uiCtx.lineTo(xPos, this.canvas.height); + } + + // Draw horizontal lines + for (let y = 0; y <= this.height; y++) { + const yPos = y * this.pixelSize * this.zoom; + this.uiCtx.moveTo(0, yPos); + this.uiCtx.lineTo(this.canvas.width, yPos); + } + + // Draw all lines at once + this.uiCtx.stroke(); + } + } + + /** + * Set the effects settings + * @param {Object} effects - Effects settings + */ + setEffects(effects) { + this.effects = {...this.effects, ...effects}; + } + + /** + * Animate the effects + */ + animateEffects() { + // Use a bound function to avoid creating a new function on each frame + if (!this._boundAnimateEffects) { + this._boundAnimateEffects = this.animateEffects.bind(this); + } + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Check if any effects are enabled + const hasEffects = + this.effects.grain || + this.effects.static || + this.effects.glitch || + this.effects.crt || + this.effects.scanLines || + this.effects.vignette || + this.effects.noise || + this.effects.pixelate; + + // Only apply effects if at least one is enabled + if (hasEffects) { + // Apply grain effect + if (this.effects.grain) { + this.applyGrainEffect(); + } + + // Apply static effect + if (this.effects.static) { + this.applyStaticEffect(); + } + + // Apply glitch effect + if (this.effects.glitch) { + this.applyGlitchEffect(); + } + + // Apply CRT effect + if (this.effects.crt) { + this.applyCRTEffect(); + } + + // Apply scan lines effect + if (this.effects.scanLines) { + this.applyScanLines(); + } + + // Apply vignette effect + if (this.effects.vignette) { + this.applyVignette(); + } + + // Apply noise effect + if (this.effects.noise) { + this.applyNoiseEffect(); + } + + // Apply pixelate effect + if (this.effects.pixelate) { + this.applyPixelateEffect(); + } + } + + // Request next frame + this.effectsAnimationFrame = requestAnimationFrame(this._boundAnimateEffects); + } + + /** + * Apply grain effect + */ + applyGrainEffect() { + const intensity = this.effects.intensity * 0.1; + + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + if (Math.random() < intensity) { + this.effectsCtx.fillRect( + x * this.pixelSize * this.zoom, + y * this.pixelSize * this.zoom, + this.pixelSize * this.zoom, + this.pixelSize * this.zoom + ); + } + } + } + } + + /** + * Apply static effect + */ + applyStaticEffect() { + const intensity = this.effects.intensity * 0.05; + + // Use a single path for better performance + this.effectsCtx.fillStyle = 'rgba(255, 255, 255, 0.1)'; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + // Only process every other line for the scan line effect + for (let y = 0; y < this.canvas.height; y += 2) { + if (Math.random() < intensity) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + } + + // Draw all static lines at once + this.effectsCtx.fill(); + } + + /** + * Apply glitch effect + */ + applyGlitchEffect() { + const intensity = this.effects.intensity; + + // Only apply glitch occasionally + if (Math.random() < intensity * 0.1) { + // Random offset for a few rows + const numRows = Math.floor(Math.random() * 5) + 1; + + for (let i = 0; i < numRows; i++) { + const y = Math.floor(Math.random() * this.height); + const offset = (Math.random() - 0.5) * 10 * intensity; + + // Create a temporary canvas to hold the row + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.pixelSize * this.zoom; + const tempCtx = tempCanvas.getContext('2d'); + + // Copy the row from the main canvas + tempCtx.drawImage( + this.canvas, + 0, y * this.pixelSize * this.zoom, + this.canvas.width, this.pixelSize * this.zoom, + 0, 0, + this.canvas.width, this.pixelSize * this.zoom + ); + + // Draw the row with offset + this.effectsCtx.drawImage( + tempCanvas, + offset * this.pixelSize * this.zoom, y * this.pixelSize * this.zoom + ); + } + } + } + + /** + * Apply CRT effect + */ + applyCRTEffect() { + const intensity = this.effects.intensity; + + // Scan lines + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.fillRect(0, y, this.canvas.width, 1); + } + + // Vignette + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity * 0.7})`); + + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply scan lines effect + */ + applyScanLines() { + const intensity = this.effects.intensity * 0.1; + + // Draw horizontal scan lines + this.effectsCtx.fillStyle = `rgba(0, 0, 0, ${intensity})`; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + + // Draw all scan lines at once + this.effectsCtx.fill(); + } + + /** + * Apply vignette effect + */ + applyVignette() { + const intensity = this.effects.intensity * 0.7; + + // Create radial gradient + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, Math.max(this.canvas.width, this.canvas.height) / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity})`); + + // Apply gradient + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply noise effect + */ + applyNoiseEffect() { + const intensity = this.effects.intensity * 0.05; + + // Create an ImageData object for better performance + const imageData = this.effectsCtx.createImageData(this.canvas.width, this.canvas.height); + const data = imageData.data; + + // Fill with noise + for (let i = 0; i < data.length; i += 4) { + if (Math.random() < intensity) { + // White noise pixel with 50% opacity + data[i] = 255; // R + data[i + 1] = 255; // G + data[i + 2] = 255; // B + data[i + 3] = 128; // A (50% opacity) + } + } + + // Put the image data back to the canvas + this.effectsCtx.putImageData(imageData, 0, 0); + } + + /** + * Apply pixelate effect + */ + applyPixelateEffect() { + const intensity = Math.max(1, Math.floor(this.effects.intensity * 10)); + + // Create a temporary canvas + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.canvas.height; + const tempCtx = tempCanvas.getContext('2d'); + + // Draw the main canvas to the temporary canvas + tempCtx.drawImage(this.canvas, 0, 0); + + // Clear the effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Draw pixelated version + for (let y = 0; y < this.canvas.height; y += intensity) { + for (let x = 0; x < this.canvas.width; x += intensity) { + // Get the color of the pixel at (x, y) + const pixelData = tempCtx.getImageData(x, y, 1, 1).data; + + // Set the fill style to the pixel color + this.effectsCtx.fillStyle = `rgba(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]}, 0.5)`; + + // Draw a rectangle with the size of the pixel block + this.effectsCtx.fillRect(x, y, intensity, intensity); + } + } + } + + /** + * Set canvas dimensions + * @param {number} width - Width in pixels + * @param {number} height - Height in pixels + */ + setDimensions(width, height) { + this.width = width; + this.height = height; + this.pixels = new Array(this.width * this.height).fill('#000000'); + this.initCanvas(); + } + + /** + * Zoom in + */ + zoomIn() { + if (this.zoom < 16) { + this.zoom *= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Zoom out + */ + zoomOut() { + if (this.zoom > 0.5) { + this.zoom /= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Get the current zoom level + * @returns {number} Zoom level + */ + getZoom() { + return this.zoom; + } + + /** + * Reset zoom to 100% + */ + resetZoom() { + this.zoom = 1; + this.initCanvas(); + this.render(); + } + + /** + * Toggle grid visibility + */ + toggleGrid() { + this.showGrid = !this.showGrid; + this.render(); + } + + /** + * Export the canvas as a PNG data URL + * @returns {string} PNG data URL + */ + exportToPNG() { + // Create a temporary canvas for export + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = this.width; + exportCanvas.height = this.height; + const exportCtx = exportCanvas.getContext('2d'); + + // Draw pixels at 1:1 scale + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + const index = y * this.width + x; + const color = this.pixels[index]; + + exportCtx.fillStyle = color; + exportCtx.fillRect(x, y, 1, 1); + } + } + + // Return data URL + return exportCanvas.toDataURL('image/png'); + } + + /** + * Get the pixel data as an array + * @returns {Array} Pixel data + */ + getPixelData() { + return [...this.pixels]; + } + + /** + * Set the pixel data from an array + * @param {Array} pixelData - Pixel data + * @param {boolean} saveHistory - Whether to save this change to history + */ + setPixelData(pixelData, saveHistory = true) { + if (pixelData.length === this.width * this.height) { + this.pixels = [...pixelData]; + if (saveHistory) { + this.saveToHistory(); + } + this.render(); + } + } + + /** + * Save the current state to history + */ + saveToHistory() { + // If we're not at the end of the history, remove future states + if (this.historyIndex < this.history.length - 1) { + this.history = this.history.slice(0, this.historyIndex + 1); + } + + // Add current state to history + this.history.push(this.getPixelData()); + this.historyIndex = this.history.length - 1; + + // Limit history size + if (this.history.length > this.maxHistorySize) { + this.history.shift(); + this.historyIndex--; + } + } + + /** + * Undo the last action + * @returns {boolean} Whether the undo was successful + */ + undo() { + if (this.historyIndex > 0) { + this.historyIndex--; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Redo the last undone action + * @returns {boolean} Whether the redo was successful + */ + redo() { + if (this.historyIndex < this.history.length - 1) { + this.historyIndex++; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Resize the canvas + * @param {number} width - New width in pixels + * @param {number} height - New height in pixels + * @param {boolean} preserveContent - Whether to preserve the current content + */ + resize(width, height, preserveContent = true) { + // Create a new pixel array + const newPixels = new Array(width * height).fill('#000000'); + + // Copy existing pixels if preserving content + if (preserveContent) { + const minWidth = Math.min(width, this.width); + const minHeight = Math.min(height, this.height); + + for (let y = 0; y < minHeight; y++) { + for (let x = 0; x < minWidth; x++) { + const oldIndex = y * this.width + x; + const newIndex = y * width + x; + newPixels[newIndex] = this.pixels[oldIndex]; + } + } + } + + // Update dimensions + this.width = width; + this.height = height; + this.pixels = newPixels; + + // Reinitialize the canvas + this.initCanvas(); + + // Save to history + this.saveToHistory(); + + // Render the canvas + this.render(); + } + + /** + * Clean up resources + * This should be called when the canvas is no longer needed + */ + cleanup() { + // Cancel animation frame if it exists + if (this.effectsAnimationFrame) { + cancelAnimationFrame(this.effectsAnimationFrame); + this.effectsAnimationFrame = null; + } + + // Remove event listeners using the bound handlers + this.canvas.removeEventListener('mousedown', this._boundHandleMouseDown); + document.removeEventListener('mousemove', this._boundHandleMouseMove); + document.removeEventListener('mouseup', this._boundHandleMouseUp); + this.canvas.removeEventListener('contextmenu', this._boundHandleContextMenu); + this.canvas.removeEventListener('mousemove', this._boundUpdateCursorPosition); + this.canvas.removeEventListener('mouseleave', this._boundHandleMouseLeave); + } +} diff --git a/.history/src/scripts/canvas/PixelCanvas_20250605031003.js b/.history/src/scripts/canvas/PixelCanvas_20250605031003.js new file mode 100644 index 0000000..2a45920 --- /dev/null +++ b/.history/src/scripts/canvas/PixelCanvas_20250605031003.js @@ -0,0 +1,999 @@ +/** + * PixelCanvas Class + * + * Handles the main drawing canvas, pixel manipulation, and rendering. + */ +class PixelCanvas { + /** + * Create a new PixelCanvas + * @param {Object} options - Configuration options + * @param {string} options.canvasId - ID of the main canvas element + * @param {string} options.effectsCanvasId - ID of the effects overlay canvas + * @param {string} options.uiCanvasId - ID of the UI overlay canvas + * @param {number} options.width - Width in pixels + * @param {number} options.height - Height in pixels + * @param {number} options.pixelSize - Size of each pixel in screen pixels + */ + constructor(options) { + // Canvas elements + this.canvas = document.getElementById(options.canvasId); + this.effectsCanvas = document.getElementById(options.effectsCanvasId); + this.uiCanvas = document.getElementById(options.uiCanvasId); + + // Canvas contexts + this.ctx = this.canvas.getContext('2d'); + this.effectsCtx = this.effectsCanvas.getContext('2d'); + this.uiCtx = this.uiCanvas.getContext('2d'); + + // Canvas dimensions + this.width = options.width || 64; + this.height = options.height || 64; + this.pixelSize = options.pixelSize || 8; + + // Zoom level + this.zoom = 1; + + // Pixel data + this.pixels = new Array(this.width * this.height).fill('#000000'); + + // Undo/Redo history + this.history = []; + this.historyIndex = -1; + this.maxHistorySize = 50; // Maximum number of states to store + + // Save initial state + this.saveToHistory(); + + // Effects settings + this.effects = { + grain: false, + static: false, + glitch: false, + crt: false, + scanLines: false, + vignette: false, + noise: false, + pixelate: false, + intensity: 0.5 + }; + + // Animation frame for effects + this.effectsAnimationFrame = null; + + // Mouse state + this.isDrawing = false; + this.lastX = 0; + this.lastY = 0; + + // Grid display + this.showGrid = true; + + // Initialize the canvas + this.initCanvas(); + + // Set up event listeners + this.setupEventListeners(); + + // Start the effects animation loop + this.animateEffects(); + } + + /** + * Initialize the canvas with the correct dimensions + */ + initCanvas() { + // Set canvas dimensions + this.canvas.width = this.width * this.pixelSize * this.zoom; + this.canvas.height = this.height * this.pixelSize * this.zoom; + + // Set effects canvas dimensions + this.effectsCanvas.width = this.canvas.width; + this.effectsCanvas.height = this.canvas.height; + + // Set UI canvas dimensions + this.uiCanvas.width = this.canvas.width; + this.uiCanvas.height = this.canvas.height; + + // Set rendering options for pixel art + this.ctx.imageSmoothingEnabled = false; + this.effectsCtx.imageSmoothingEnabled = false; + this.uiCtx.imageSmoothingEnabled = false; + + // Clear the canvas + this.clear(); + + // Draw the initial grid + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Set up event listeners for mouse/touch interaction + */ + setupEventListeners() { + // Bind event handlers to this instance + this._boundHandleMouseDown = this.handleMouseDown.bind(this); + this._boundHandleMouseMove = this.handleMouseMove.bind(this); + this._boundHandleMouseUp = this.handleMouseUp.bind(this); + this._boundUpdateCursorPosition = this.updateCursorPosition.bind(this); + this._boundHandleContextMenu = (e) => { e.preventDefault(); }; + this._boundHandleMouseLeave = () => { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + }; + + // Mouse events + this.canvas.addEventListener('mousedown', this._boundHandleMouseDown); + document.addEventListener('mousemove', this._boundHandleMouseMove); + document.addEventListener('mouseup', this._boundHandleMouseUp); + + // Prevent context menu on right-click + this.canvas.addEventListener('contextmenu', this._boundHandleContextMenu); + + // Update cursor position display + this.canvas.addEventListener('mousemove', this._boundUpdateCursorPosition); + + // Mouse leave + this.canvas.addEventListener('mouseleave', this._boundHandleMouseLeave); + } + + /** + * Handle mouse down event + * @param {MouseEvent} e - Mouse event + */ + handleMouseDown(e) { + this.isDrawing = true; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Store last position + this.lastX = x; + this.lastY = y; + + // Save the current state before drawing + this.saveToHistory(); + + // Draw a single pixel + this.drawPixel(x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Render the canvas + this.render(); + } + + /** + * Handle mouse move event + * @param {MouseEvent} e - Mouse event + */ + handleMouseMove(e) { + if (!this.isDrawing) return; + + // Get pixel coordinates + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + // Only draw if the position has changed + if (x !== this.lastX || y !== this.lastY) { + // Draw a line from last position to current position + this.drawLine(this.lastX, this.lastY, x, y, e.buttons === 2 ? '#000000' : '#ffffff'); + + // Update last position + this.lastX = x; + this.lastY = y; + + // Render the canvas + this.render(); + } + } + + /** + * Handle mouse up event + */ + handleMouseUp() { + this.isDrawing = false; + } + + /** + * Update the cursor position display + * @param {MouseEvent} e - Mouse event + */ + updateCursorPosition(e) { + const rect = this.canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (this.pixelSize * this.zoom)); + const y = Math.floor((e.clientY - rect.top) / (this.pixelSize * this.zoom)); + + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + document.getElementById('cursor-position').textContent = `X: ${x} Y: ${y}`; + } else { + document.getElementById('cursor-position').textContent = 'X: - Y: -'; + } + } + + /** + * Draw a single pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {string} color - Color in hex format + */ + drawPixel(x, y, color) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + this.pixels[index] = color; + } + } + + /** + * Draw a line using Bresenham's algorithm + * @param {number} x0 - Starting X coordinate + * @param {number} y0 - Starting Y coordinate + * @param {number} x1 - Ending X coordinate + * @param {number} y1 - Ending Y coordinate + * @param {string} color - Color in hex format + */ + drawLine(x0, y0, x1, y1, color) { + const dx = Math.abs(x1 - x0); + const dy = Math.abs(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dx - dy; + + while (true) { + this.drawPixel(x0, y0, color); + + if (x0 === x1 && y0 === y1) break; + + const e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + /** + * Draw a rectangle + * @param {number} x - X coordinate of top-left corner + * @param {number} y - Y coordinate of top-left corner + * @param {number} width - Width of rectangle + * @param {number} height - Height of rectangle + * @param {string} color - Color in hex format + * @param {boolean} fill - Whether to fill the rectangle + */ + drawRect(x, y, width, height, color, fill = false) { + if (fill) { + for (let i = x; i < x + width; i++) { + for (let j = y; j < y + height; j++) { + this.drawPixel(i, j, color); + } + } + } else { + // Draw horizontal lines + for (let i = x; i < x + width; i++) { + this.drawPixel(i, y, color); + this.drawPixel(i, y + height - 1, color); + } + + // Draw vertical lines + for (let j = y; j < y + height; j++) { + this.drawPixel(x, j, color); + this.drawPixel(x + width - 1, j, color); + } + } + } + + /** + * Draw an ellipse + * @param {number} xc - X coordinate of center + * @param {number} yc - Y coordinate of center + * @param {number} a - Semi-major axis + * @param {number} b - Semi-minor axis + * @param {string} color - Color in hex format + */ + drawEllipse(xc, yc, a, b, color) { + let x = 0; + let y = b; + let a2 = a * a; + let b2 = b * b; + let d = b2 - a2 * b + a2 / 4; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + + while (a2 * (y - 0.5) > b2 * (x + 1)) { + if (d < 0) { + d += b2 * (2 * x + 3); + } else { + d += b2 * (2 * x + 3) + a2 * (-2 * y + 2); + y--; + } + x++; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + + d = b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2; + + while (y > 0) { + if (d < 0) { + d += b2 * (2 * x + 2) + a2 * (-2 * y + 3); + x++; + } else { + d += a2 * (-2 * y + 3); + } + y--; + + this.drawPixel(xc + x, yc + y, color); + this.drawPixel(xc - x, yc + y, color); + this.drawPixel(xc + x, yc - y, color); + this.drawPixel(xc - x, yc - y, color); + } + } + + /** + * Fill an area with a color (flood fill) + * @param {number} x - Starting X coordinate + * @param {number} y - Starting Y coordinate + * @param {string} fillColor - Color to fill with + */ + floodFill(x, y, fillColor) { + const targetColor = this.getPixel(x, y); + + // Don't fill if the target color is the same as the fill color + if (targetColor === fillColor) return; + + // Use a more efficient stack-based approach instead of queue + // This avoids the overhead of shift() operations + const stack = [{x, y}]; + + // Create a visited array for efficient tracking + const visited = new Uint8Array(this.width * this.height); + const index = y * this.width + x; + + // Check starting pixel + if (index < 0 || index >= visited.length) return; + + while (stack.length > 0) { + const {x, y} = stack.pop(); + const index = y * this.width + x; + + // Skip if already visited + if (visited[index]) continue; + + // Mark as visited + visited[index] = 1; + + // Check if this pixel is the target color + if (this.getPixel(x, y) === targetColor) { + // Set the pixel to the fill color + this.drawPixel(x, y, fillColor); + + // Add adjacent pixels to the stack + // Check bounds before adding to avoid unnecessary getPixel calls + if (x > 0) stack.push({x: x - 1, y}); + if (x < this.width - 1) stack.push({x: x + 1, y}); + if (y > 0) stack.push({x, y: y - 1}); + if (y < this.height - 1) stack.push({x, y: y + 1}); + } + } + } + + /** + * Get the color of a pixel + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @returns {string} Color in hex format + */ + getPixel(x, y) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + const index = y * this.width + x; + return this.pixels[index]; + } + return null; + } + + /** + * Clear the canvas + */ + clear() { + // Clear pixel data + this.pixels.fill('#000000'); + + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Clear UI canvas + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + } + + /** + * Render the canvas + */ + render() { + // Clear canvas + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Always use ImageData for rendering for consistent performance + const imageData = this.ctx.createImageData(this.width, this.height); + const data = imageData.data; + + for (let i = 0; i < this.pixels.length; i++) { + const color = this.pixels[i]; + // Parse hex color to RGB + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + + // Set RGBA values (4 bytes per pixel) + const pixelIndex = i * 4; + data[pixelIndex] = r; + data[pixelIndex + 1] = g; + data[pixelIndex + 2] = b; + data[pixelIndex + 3] = 255; // Alpha (fully opaque) + } + + // Create temporary canvas for efficient scaling + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.putImageData(imageData, 0, 0); + + // Draw scaled image + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage( + tempCanvas, + 0, 0, this.width, this.height, + 0, 0, this.canvas.width, this.canvas.height + ); + + // Draw grid if enabled + if (this.showGrid) { + this.drawGrid(); + } + } + + /** + * Draw the grid + */ + drawGrid() { + this.uiCtx.clearRect(0, 0, this.uiCanvas.width, this.uiCanvas.height); + + if (this.zoom >= 4) { + this.uiCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; + this.uiCtx.lineWidth = 1; + + // Use a single path for all grid lines for better performance + this.uiCtx.beginPath(); + + // Draw vertical lines + for (let x = 0; x <= this.width; x++) { + const xPos = x * this.pixelSize * this.zoom; + this.uiCtx.moveTo(xPos, 0); + this.uiCtx.lineTo(xPos, this.canvas.height); + } + + // Draw horizontal lines + for (let y = 0; y <= this.height; y++) { + const yPos = y * this.pixelSize * this.zoom; + this.uiCtx.moveTo(0, yPos); + this.uiCtx.lineTo(this.canvas.width, yPos); + } + + // Draw all lines at once + this.uiCtx.stroke(); + } + } + + /** + * Set the effects settings + * @param {Object} effects - Effects settings + */ + setEffects(effects) { + this.effects = {...this.effects, ...effects}; + } + + /** + * Animate the effects + */ + animateEffects() { + // Use a bound function to avoid creating a new function on each frame + if (!this._boundAnimateEffects) { + this._boundAnimateEffects = this.animateEffects.bind(this); + } + + // Clear effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Check if any effects are enabled + const hasEffects = + this.effects.grain || + this.effects.static || + this.effects.glitch || + this.effects.crt || + this.effects.scanLines || + this.effects.vignette || + this.effects.noise || + this.effects.pixelate; + + // Only apply effects if at least one is enabled + if (hasEffects) { + // Apply grain effect + if (this.effects.grain) { + this.applyGrainEffect(); + } + + // Apply static effect + if (this.effects.static) { + this.applyStaticEffect(); + } + + // Apply glitch effect + if (this.effects.glitch) { + this.applyGlitchEffect(); + } + + // Apply CRT effect + if (this.effects.crt) { + this.applyCRTEffect(); + } + + // Apply scan lines effect + if (this.effects.scanLines) { + this.applyScanLines(); + } + + // Apply vignette effect + if (this.effects.vignette) { + this.applyVignette(); + } + + // Apply noise effect + if (this.effects.noise) { + this.applyNoiseEffect(); + } + + // Apply pixelate effect + if (this.effects.pixelate) { + this.applyPixelateEffect(); + } + } + + // Request next frame + this.effectsAnimationFrame = requestAnimationFrame(this._boundAnimateEffects); + } + + /** + * Apply grain effect + */ + applyGrainEffect() { + const intensity = this.effects.intensity * 0.1; + + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + if (Math.random() < intensity) { + this.effectsCtx.fillRect( + x * this.pixelSize * this.zoom, + y * this.pixelSize * this.zoom, + this.pixelSize * this.zoom, + this.pixelSize * this.zoom + ); + } + } + } + } + + /** + * Apply static effect + */ + applyStaticEffect() { + const intensity = this.effects.intensity * 0.05; + + // Use a single path for better performance + this.effectsCtx.fillStyle = 'rgba(255, 255, 255, 0.1)'; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + // Only process every other line for the scan line effect + for (let y = 0; y < this.canvas.height; y += 2) { + if (Math.random() < intensity) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + } + + // Draw all static lines at once + this.effectsCtx.fill(); + } + + /** + * Apply glitch effect + */ + applyGlitchEffect() { + const intensity = this.effects.intensity; + + // Only apply glitch occasionally + if (Math.random() < intensity * 0.1) { + // Random offset for a few rows + const numRows = Math.floor(Math.random() * 5) + 1; + + for (let i = 0; i < numRows; i++) { + const y = Math.floor(Math.random() * this.height); + const offset = (Math.random() - 0.5) * 10 * intensity; + + // Create a temporary canvas to hold the row + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.pixelSize * this.zoom; + const tempCtx = tempCanvas.getContext('2d'); + + // Copy the row from the main canvas + tempCtx.drawImage( + this.canvas, + 0, y * this.pixelSize * this.zoom, + this.canvas.width, this.pixelSize * this.zoom, + 0, 0, + this.canvas.width, this.pixelSize * this.zoom + ); + + // Draw the row with offset + this.effectsCtx.drawImage( + tempCanvas, + offset * this.pixelSize * this.zoom, y * this.pixelSize * this.zoom + ); + } + } + } + + /** + * Apply CRT effect + */ + applyCRTEffect() { + const intensity = this.effects.intensity; + + // Scan lines + this.effectsCtx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.fillRect(0, y, this.canvas.width, 1); + } + + // Vignette + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity * 0.7})`); + + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply scan lines effect + */ + applyScanLines() { + const intensity = this.effects.intensity * 0.1; + + // Draw horizontal scan lines + this.effectsCtx.fillStyle = `rgba(0, 0, 0, ${intensity})`; + + // Batch drawing operations for better performance + this.effectsCtx.beginPath(); + + for (let y = 0; y < this.canvas.height; y += 2) { + this.effectsCtx.rect(0, y, this.canvas.width, 1); + } + + // Draw all scan lines at once + this.effectsCtx.fill(); + } + + /** + * Apply vignette effect + */ + applyVignette() { + const intensity = this.effects.intensity * 0.7; + + // Create radial gradient + const gradient = this.effectsCtx.createRadialGradient( + this.canvas.width / 2, this.canvas.height / 2, 0, + this.canvas.width / 2, this.canvas.height / 2, Math.max(this.canvas.width, this.canvas.height) / 1.5 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, `rgba(0, 0, 0, ${intensity})`); + + // Apply gradient + this.effectsCtx.fillStyle = gradient; + this.effectsCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply noise effect + */ + applyNoiseEffect() { + const intensity = this.effects.intensity * 0.05; + + // Create an ImageData object for better performance + const imageData = this.effectsCtx.createImageData(this.canvas.width, this.canvas.height); + const data = imageData.data; + + // Fill with noise + for (let i = 0; i < data.length; i += 4) { + if (Math.random() < intensity) { + // White noise pixel with 50% opacity + data[i] = 255; // R + data[i + 1] = 255; // G + data[i + 2] = 255; // B + data[i + 3] = 128; // A (50% opacity) + } + } + + // Put the image data back to the canvas + this.effectsCtx.putImageData(imageData, 0, 0); + } + + /** + * Apply pixelate effect + */ + applyPixelateEffect() { + const intensity = Math.max(1, Math.floor(this.effects.intensity * 10)); + + // Create a temporary canvas + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.canvas.height; + const tempCtx = tempCanvas.getContext('2d'); + + // Draw the main canvas to the temporary canvas + tempCtx.drawImage(this.canvas, 0, 0); + + // Clear the effects canvas + this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height); + + // Draw pixelated version + for (let y = 0; y < this.canvas.height; y += intensity) { + for (let x = 0; x < this.canvas.width; x += intensity) { + // Get the color of the pixel at (x, y) + const pixelData = tempCtx.getImageData(x, y, 1, 1).data; + + // Set the fill style to the pixel color + this.effectsCtx.fillStyle = `rgba(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]}, 0.5)`; + + // Draw a rectangle with the size of the pixel block + this.effectsCtx.fillRect(x, y, intensity, intensity); + } + } + } + + /** + * Set canvas dimensions + * @param {number} width - Width in pixels + * @param {number} height - Height in pixels + */ + setDimensions(width, height) { + this.width = width; + this.height = height; + this.pixels = new Array(this.width * this.height).fill('#000000'); + this.initCanvas(); + } + + /** + * Zoom in + */ + zoomIn() { + if (this.zoom < 16) { + this.zoom *= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Zoom out + */ + zoomOut() { + if (this.zoom > 0.5) { + this.zoom /= 2; + this.initCanvas(); + this.render(); + } + } + + /** + * Get the current zoom level + * @returns {number} Zoom level + */ + getZoom() { + return this.zoom; + } + + /** + * Reset zoom to 100% + */ + resetZoom() { + this.zoom = 1; + this.initCanvas(); + this.render(); + } + + /** + * Toggle grid visibility + */ + toggleGrid() { + this.showGrid = !this.showGrid; + this.render(); + } + + /** + * Export the canvas as a PNG data URL + * @returns {string} PNG data URL + */ + exportToPNG() { + // Create a temporary canvas for export + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = this.width; + exportCanvas.height = this.height; + const exportCtx = exportCanvas.getContext('2d'); + + // Draw pixels at 1:1 scale + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + const index = y * this.width + x; + const color = this.pixels[index]; + + exportCtx.fillStyle = color; + exportCtx.fillRect(x, y, 1, 1); + } + } + + // Return data URL + return exportCanvas.toDataURL('image/png'); + } + + /** + * Get the pixel data as an array + * @returns {Array} Pixel data + */ + getPixelData() { + return [...this.pixels]; + } + + /** + * Set the pixel data from an array + * @param {Array} pixelData - Pixel data + * @param {boolean} saveHistory - Whether to save this change to history + */ + setPixelData(pixelData, saveHistory = true) { + if (pixelData.length === this.width * this.height) { + this.pixels = [...pixelData]; + if (saveHistory) { + this.saveToHistory(); + } + this.render(); + } + } + + /** + * Save the current state to history + */ + saveToHistory() { + // If we're not at the end of the history, remove future states + if (this.historyIndex < this.history.length - 1) { + this.history = this.history.slice(0, this.historyIndex + 1); + } + + // Add current state to history + this.history.push(this.getPixelData()); + this.historyIndex = this.history.length - 1; + + // Limit history size + if (this.history.length > this.maxHistorySize) { + this.history.shift(); + this.historyIndex--; + } + } + + /** + * Undo the last action + * @returns {boolean} Whether the undo was successful + */ + undo() { + if (this.historyIndex > 0) { + this.historyIndex--; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Redo the last undone action + * @returns {boolean} Whether the redo was successful + */ + redo() { + if (this.historyIndex < this.history.length - 1) { + this.historyIndex++; + this.setPixelData(this.history[this.historyIndex], false); + return true; + } + return false; + } + + /** + * Resize the canvas + * @param {number} width - New width in pixels + * @param {number} height - New height in pixels + * @param {boolean} preserveContent - Whether to preserve the current content + */ + resize(width, height, preserveContent = true) { + // Create a new pixel array + const newPixels = new Array(width * height).fill('#000000'); + + // Copy existing pixels if preserving content + if (preserveContent) { + const minWidth = Math.min(width, this.width); + const minHeight = Math.min(height, this.height); + + for (let y = 0; y < minHeight; y++) { + for (let x = 0; x < minWidth; x++) { + const oldIndex = y * this.width + x; + const newIndex = y * width + x; + newPixels[newIndex] = this.pixels[oldIndex]; + } + } + } + + // Update dimensions + this.width = width; + this.height = height; + this.pixels = newPixels; + + // Reinitialize the canvas + this.initCanvas(); + + // Save to history + this.saveToHistory(); + + // Render the canvas + this.render(); + } + + /** + * Clean up resources + * This should be called when the canvas is no longer needed + */ + cleanup() { + // Cancel animation frame if it exists + if (this.effectsAnimationFrame) { + cancelAnimationFrame(this.effectsAnimationFrame); + this.effectsAnimationFrame = null; + } + + // Remove event listeners using the bound handlers + this.canvas.removeEventListener('mousedown', this._boundHandleMouseDown); + document.removeEventListener('mousemove', this._boundHandleMouseMove); + document.removeEventListener('mouseup', this._boundHandleMouseUp); + this.canvas.removeEventListener('contextmenu', this._boundHandleContextMenu); + this.canvas.removeEventListener('mousemove', this._boundUpdateCursorPosition); + this.canvas.removeEventListener('mouseleave', this._boundHandleMouseLeave); + } +} diff --git a/src/scripts/app.js b/src/scripts/app.js index ec1b229..dd777c4 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -511,17 +511,14 @@ document.addEventListener('DOMContentLoaded', () => { { width: 88, height: 31, name: '88×31', description: 'Classic web button' }, { width: 120, height: 60, name: '120×60', description: 'Small banner' }, { width: 120, height: 80, name: '120×80', description: 'Small animation' }, - { width: 350, height: 350, name: '350×350', description: 'Medium square' }, - { width: 800, height: 500, name: '800×500', description: 'Large landscape' }, - { width: 900, height: 900, name: '900×900', description: 'Large square' } + { width: 128, height: 128, name: '128×128', description: 'Medium square' }, + { width: 256, height: 256, name: '256×256', description: 'Large square' } ]; // Create HTML for size options with silhouettes let sizesHTML = '
'; canvasSizes.forEach(size => { - // We don't need preview width/height anymore since we're using fixed size preview boxes - // Calculate silhouette dimensions to match aspect ratio let silhouetteWidth, silhouetteHeight; @@ -579,89 +576,91 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // Add some CSS for the size selection dialog - const style = document.createElement('style'); - style.textContent = ` - .modal-dialog { - width: 600px !important; - height: 500px !important; - max-width: 80% !important; - max-height: 80% !important; - } - - .modal-body { - max-height: 400px; - overflow-y: auto; - padding-right: 10px; - } + // Add CSS only once to prevent memory leaks + if (!document.getElementById('canvas-size-dialog-styles')) { + const style = document.createElement('style'); + style.id = 'canvas-size-dialog-styles'; + style.textContent = ` + .modal-dialog { + width: 600px !important; + height: 500px !important; + max-width: 80% !important; + max-height: 80% !important; + } - .canvas-size-options { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 15px; - margin-bottom: 20px; - } + .modal-body { + max-height: 400px; + overflow-y: auto; + padding-right: 10px; + } - .canvas-size-option { - display: flex; - flex-direction: column; - align-items: center; - padding: 15px; - border: 1px solid var(--panel-border); - background-color: var(--button-bg); - cursor: pointer; - transition: all 0.2s ease; - } + .canvas-size-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; + } - .canvas-size-option:hover { - background-color: var(--button-hover); - transform: translateY(-2px); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - } + .canvas-size-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border: 1px solid var(--panel-border); + background-color: var(--button-bg); + cursor: pointer; + transition: all 0.2s ease; + } - .canvas-size-preview { - position: relative; - background-color: #000; - margin-bottom: 10px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--panel-border); - width: 120px; - height: 120px; - } + .canvas-size-option:hover { + background-color: var(--button-hover); + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + } - .canvas-size-silhouette { - position: absolute; - background-color: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.3); - } + .canvas-size-preview { + position: relative; + background-color: #000; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--panel-border); + width: 120px; + height: 120px; + } - .canvas-size-info { - text-align: center; - width: 100%; - } + .canvas-size-silhouette { + position: absolute; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + } - .canvas-size-name { - font-weight: bold; - margin-bottom: 5px; - color: var(--highlight-color); - text-shadow: var(--text-glow); - } + .canvas-size-info { + text-align: center; + width: 100%; + } - .canvas-size-description { - font-size: 12px; - color: var(--secondary-color); - } + .canvas-size-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--highlight-color); + text-shadow: var(--text-glow); + } - /* Make the modal dialog more square/rectangular */ - #modal-container { - display: flex; - justify-content: center; - align-items: center; - } - `; + .canvas-size-description { + font-size: 12px; + color: var(--secondary-color); + } - document.head.appendChild(style); + /* Make the modal dialog more square/rectangular */ + #modal-container { + display: flex; + justify-content: center; + align-items: center; + } + `; + document.head.appendChild(style); + } } }); diff --git a/src/scripts/canvas/PixelCanvas.js b/src/scripts/canvas/PixelCanvas.js index 3fac938..2a45920 100644 --- a/src/scripts/canvas/PixelCanvas.js +++ b/src/scripts/canvas/PixelCanvas.js @@ -357,19 +357,22 @@ class PixelCanvas { // This avoids the overhead of shift() operations const stack = [{x, y}]; - // Create a visited map to avoid checking the same pixel multiple times - const visited = new Set(); - const getKey = (x, y) => `${x},${y}`; + // Create a visited array for efficient tracking + const visited = new Uint8Array(this.width * this.height); + const index = y * this.width + x; + + // Check starting pixel + if (index < 0 || index >= visited.length) return; while (stack.length > 0) { const {x, y} = stack.pop(); - const key = getKey(x, y); + const index = y * this.width + x; // Skip if already visited - if (visited.has(key)) continue; + if (visited[index]) continue; // Mark as visited - visited.add(key); + visited[index] = 1; // Check if this pixel is the target color if (this.getPixel(x, y) === targetColor) { @@ -426,46 +429,40 @@ class PixelCanvas { this.ctx.fillStyle = '#000000'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - // Use a more efficient rendering approach for larger canvases - if (this.pixelSize * this.zoom === 1) { - // For 1:1 pixel rendering, use ImageData for better performance - const imageData = this.ctx.createImageData(this.width, this.height); - const data = imageData.data; - - for (let i = 0; i < this.pixels.length; i++) { - const color = this.pixels[i]; - // Parse hex color to RGB - const r = parseInt(color.slice(1, 3), 16); - const g = parseInt(color.slice(3, 5), 16); - const b = parseInt(color.slice(5, 7), 16); - - // Set RGBA values (4 bytes per pixel) - const pixelIndex = i * 4; - data[pixelIndex] = r; - data[pixelIndex + 1] = g; - data[pixelIndex + 2] = b; - data[pixelIndex + 3] = 255; // Alpha (fully opaque) - } + // Always use ImageData for rendering for consistent performance + const imageData = this.ctx.createImageData(this.width, this.height); + const data = imageData.data; - this.ctx.putImageData(imageData, 0, 0); - } else { - // For scaled rendering, use the existing approach - for (let y = 0; y < this.height; y++) { - for (let x = 0; x < this.width; x++) { - const index = y * this.width + x; - const color = this.pixels[index]; - - this.ctx.fillStyle = color; - this.ctx.fillRect( - x * this.pixelSize * this.zoom, - y * this.pixelSize * this.zoom, - this.pixelSize * this.zoom, - this.pixelSize * this.zoom - ); - } - } + for (let i = 0; i < this.pixels.length; i++) { + const color = this.pixels[i]; + // Parse hex color to RGB + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + + // Set RGBA values (4 bytes per pixel) + const pixelIndex = i * 4; + data[pixelIndex] = r; + data[pixelIndex + 1] = g; + data[pixelIndex + 2] = b; + data[pixelIndex + 3] = 255; // Alpha (fully opaque) } + // Create temporary canvas for efficient scaling + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.putImageData(imageData, 0, 0); + + // Draw scaled image + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage( + tempCanvas, + 0, 0, this.width, this.height, + 0, 0, this.canvas.width, this.canvas.height + ); + // Draw grid if enabled if (this.showGrid) { this.drawGrid(); From 33f9651eba3a3c91917eef2f127e8a0e6b6a3748 Mon Sep 17 00:00:00 2001 From: "deepsource-io[bot]" <42547082+deepsource-io[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 08:02:23 +0000 Subject: [PATCH 4/5] ci: add .deepsource.toml --- .deepsource.toml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..b89c880 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,31 @@ +version = 1 + +[[analyzers]] +name = "javascript" + + [analyzers.meta] + plugins = [ + "react", + "vue", + "ember", + "meteor", + "angularjs", + "angular" + ] + environment = [ + "nodejs", + "browser", + "jest", + "mocha", + "jasmine", + "vitest", + "cypress", + "mongo", + "jquery" + ] + +[[transformers]] +name = "prettier" + +[[transformers]] +name = "standardjs" \ No newline at end of file From 04da165d7681654b44267eee3300b8bbeb53b47b Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 08:04:31 +0000 Subject: [PATCH 5/5] style: format code with Prettier and StandardJS This commit fixes the style issues introduced in 33f9651 according to the output from Prettier and StandardJS. Details: None --- .history/src/index_20250519130637.html | 562 ++-- .history/src/index_20250519140924.html | 582 ++-- .history/src/index_20250519141013.html | 614 +++-- .history/src/index_20250519141039.html | 622 +++-- .history/src/index_20250519142527.html | 642 +++-- .history/src/index_20250519142541.html | 622 +++-- .history/src/index_20250519173726.html | 622 +++-- .history/src/index_20250519173805.html | 622 +++-- .history/src/index_20250519181803.html | 630 +++-- .history/src/index_20250519193347.html | 636 +++-- .history/src/index_20250519193401.html | 646 +++-- .history/src/index_20250519193438.html | 711 +++-- .history/src/index_20250519193541.html | 724 +++-- .history/src/index_20250519193607.html | 724 +++-- .history/src/index_20250519193626.html | 724 +++-- .history/src/index_20250519193658.html | 733 +++-- .history/src/index_20250519193722.html | 741 +++-- .history/src/index_20250519193743.html | 755 ++++-- .history/src/index_20250519193807.html | 755 ++++-- .history/src/index_20250519193841.html | 759 ++++-- .history/src/index_20250519193903.html | 761 ++++-- .history/src/index_20250519193917.html | 763 ++++-- .history/src/index_20250519194439.html | 763 ++++-- .history/src/index_20250519222641.html | 763 ++++-- .history/src/scripts/app_20250519133340.js | 347 +-- .history/src/scripts/app_20250519140104.js | 257 +- .history/src/scripts/app_20250519140204.js | 305 ++- .history/src/scripts/app_20250519141105.js | 309 ++- .history/src/scripts/app_20250519141133.js | 317 ++- .history/src/scripts/app_20250519142105.js | 321 ++- .history/src/scripts/app_20250519142753.js | 321 ++- .history/src/scripts/app_20250519142822.js | 376 +-- .history/src/scripts/app_20250519143718.js | 376 +-- .history/src/scripts/app_20250519150611.js | 376 +-- .history/src/scripts/app_20250519152152.js | 376 +-- .history/src/scripts/app_20250519152216.js | 376 +-- .history/src/scripts/app_20250519152227.js | 376 +-- .history/src/scripts/app_20250519173906.js | 376 +-- .history/src/scripts/app_20250519181515.js | 376 +-- .history/src/scripts/app_20250519181559.js | 376 +-- .history/src/scripts/app_20250519181852.js | 410 +-- .history/src/scripts/app_20250519182000.js | 416 +-- .history/src/scripts/app_20250519194532.js | 416 +-- .history/src/scripts/app_20250519222947.js | 646 +++-- .history/src/scripts/app_20250519223028.js | 658 +++-- .history/src/scripts/app_20250602183532.js | 658 +++-- .history/src/scripts/app_20250602183633.js | 676 ++--- .history/src/scripts/app_20250605021544.js | 420 +-- .history/src/scripts/app_20250605030646.js | 417 +-- .../canvas/PixelCanvas_20250519133458.js | 345 +-- .../canvas/PixelCanvas_20250519135800.js | 141 +- .../canvas/PixelCanvas_20250519135958.js | 143 +- .../canvas/PixelCanvas_20250519140017.js | 143 +- .../canvas/PixelCanvas_20250519140730.js | 143 +- .../canvas/PixelCanvas_20250519140839.js | 166 +- .../canvas/PixelCanvas_20250519181944.js | 166 +- .../canvas/PixelCanvas_20250519193956.js | 166 +- .../canvas/PixelCanvas_20250519194009.js | 166 +- .../canvas/PixelCanvas_20250519194022.js | 166 +- .../canvas/PixelCanvas_20250519194046.js | 166 +- .../canvas/PixelCanvas_20250519194314.js | 166 +- .../canvas/PixelCanvas_20250519194846.js | 166 +- .../canvas/PixelCanvas_20250519194913.js | 166 +- .../canvas/PixelCanvas_20250519194935.js | 166 +- .../canvas/PixelCanvas_20250519194958.js | 171 +- .../canvas/PixelCanvas_20250519195031.js | 171 +- .../canvas/PixelCanvas_20250519195046.js | 171 +- .../canvas/PixelCanvas_20250519195059.js | 171 +- .../canvas/PixelCanvas_20250519195137.js | 171 +- .../canvas/PixelCanvas_20250519195209.js | 185 +- .../canvas/PixelCanvas_20250519195258.js | 186 +- .../canvas/PixelCanvas_20250519195325.js | 190 +- .../canvas/PixelCanvas_20250605030852.js | 202 +- .../canvas/PixelCanvas_20250605031003.js | 202 +- .../tools/BrushEngine_20250519133811.js | 199 +- .../tools/BrushEngine_20250519140305.js | 103 +- .../tools/BrushEngine_20250519140337.js | 113 +- .../tools/BrushEngine_20250519140547.js | 123 +- .../tools/BrushEngine_20250519140703.js | 158 +- .../tools/BrushEngine_20250519233407.js | 169 +- .../tools/BrushEngine_20250519233455.js | 169 +- .../tools/BrushEngine_20250519233630.js | 180 +- .../tools/BrushEngine_20250602183233.js | 180 +- .../tools/BrushEngine_20250602183334.js | 183 +- .../tools/BrushEngine_20250602183434.js | 191 +- .../tools/BrushEngine_20250602183509.js | 191 +- .../tools/PaletteTool_20250519133932.js | 235 +- .../tools/PaletteTool_20250602183553.js | 235 +- .../scripts/ui/MenuManager_20250605015720.js | 42 +- .../scripts/ui/MenuSystem_20250519200450.js | 146 +- .../scripts/ui/MenuSystem_20250519200503.js | 146 +- .../scripts/ui/MenuSystem_20250519200552.js | 150 +- .../scripts/ui/MenuSystem_20250519200620.js | 150 +- .../scripts/ui/MenuSystem_20250519200704.js | 152 +- .../scripts/ui/MenuSystem_20250519222623.js | 154 +- .../scripts/ui/MenuSystem_20250519222755.js | 156 +- .../scripts/ui/MenuSystem_20250519222812.js | 158 +- .../scripts/ui/MenuSystem_20250519222827.js | 162 +- .../scripts/ui/UIManager_20250519134103.js | 208 +- .../scripts/ui/UIManager_20250519142836.js | 116 +- .../scripts/ui/UIManager_20250519142852.js | 112 +- .../components/menus_20250519132840.css | 28 +- .../components/menus_20250519141705.css | 28 +- .../components/menus_20250519141732.css | 32 +- .../components/menus_20250519142601.css | 32 +- .../components/menus_20250519142613.css | 32 +- .../components/menus_20250519222650.css | 32 +- .../components/menus_20250519222705.css | 32 +- .../components/tools_20250519132814.css | 15 +- .../components/tools_20250519141609.css | 15 +- .../components/tools_20250519141644.css | 15 +- .history/src/styles/main_20250519132719.css | 9 +- .history/src/styles/main_20250519141300.css | 5 +- .history/src/styles/main_20250519141410.css | 15 +- .history/src/styles/main_20250519141453.css | 15 +- .history/src/styles/main_20250519141546.css | 15 +- .history/src/styles/main_20250519141901.css | 19 +- .history/src/styles/main_20250519141924.css | 19 +- .history/src/styles/main_20250519141954.css | 23 +- .history/src/styles/main_20250519142012.css | 39 +- .history/src/styles/main_20250519142549.css | 39 +- .history/src/styles/main_20250519143442.css | 39 +- .history/src/styles/main_20250519143542.css | 47 +- .history/src/styles/main_20250519200408.css | 47 +- .history/src/styles/main_20250519200820.css | 49 +- .history/src/styles/main_20250519222606.css | 47 +- .../themes/lain-dive_20250519132922.css | 6 +- .../themes/lain-dive_20250519142629.css | 4 +- .../themes/lain-dive_20250519142652.css | 4 +- .../themes/lain-dive_20250519142701.css | 4 +- .../themes/lain-dive_20250519142715.css | 4 +- main.js | 122 +- preload.js | 12 +- scripts/animation/gif-exporter.js | 240 +- scripts/animation/timeline.js | 914 ++++--- scripts/app.js | 1296 ++++----- scripts/canvas/dithering.js | 556 ++-- scripts/canvas/effects.js | 586 ++-- scripts/canvas/pixel-canvas.js | 1479 +++++----- scripts/tools/palette-tool.js | 381 +-- scripts/tools/tools.js | 966 +++---- scripts/ui/ui-manager.js | 2392 +++++++++-------- src/index.html | 763 ++++-- src/main.css | 120 +- src/scripts/animation/Frame.js | 424 +-- src/scripts/animation/GifExporter.js | 154 +- src/scripts/animation/Timeline.js | 499 ++-- src/scripts/app.js | 673 ++--- src/scripts/canvas/PixelCanvas.js | 768 +++--- src/scripts/canvas/dithering.js | 423 +-- src/scripts/canvas/effects.js | 348 +-- src/scripts/lib/gif.js | 141 +- src/scripts/lib/gif.worker.js | 16 +- src/scripts/tools/BrushEngine.js | 597 ++-- src/scripts/tools/GlitchTool.js | 502 ++-- src/scripts/tools/PaletteTool.js | 395 +-- src/scripts/tools/SymmetryTools.js | 124 +- src/scripts/ui/MenuManager.js | 108 +- src/scripts/ui/MenuSystem.js | 292 +- src/scripts/ui/ThemeManager.js | 122 +- src/scripts/ui/UIManager.js | 222 +- src/styles/components/canvas.css | 6 +- src/styles/components/menus.css | 32 +- src/styles/components/timeline.css | 2 +- src/styles/components/tools.css | 15 +- src/styles/main.css | 47 +- src/styles/themes/lain-dive.css | 4 +- src/styles/themes/monolith.css | 6 +- src/styles/themes/morrowind-glyph.css | 8 +- src/themes/lain-dive.css | 31 +- src/themes/monolith.css | 236 +- src/themes/morrowind-glyph.css | 262 +- usage-guide.md | 5 + 173 files changed, 29496 insertions(+), 22933 deletions(-) diff --git a/.history/src/index_20250519130637.html b/.history/src/index_20250519130637.html index efed6e0..2195834 100644 --- a/.history/src/index_20250519130637.html +++ b/.history/src/index_20250519130637.html @@ -1,284 +1,328 @@ - + - - - - VOIDSKETCH - - - - - - - - -
- -
-
VOIDSKETCH
-
- - - + + + + VOIDSKETCH + + + + + + + + +
+ +
+
VOIDSKETCH
+
+ + + +
-
- - -
- -
-
-
- TOOLS -
- -
-
BRUSH
-
- - - - - - - - + + +
+ +
+
+
+ TOOLS
-
- -
-
SYMMETRY
-
- - - - - + +
+
BRUSH
+
+ + + + + + + + +
-
- -
-
PALETTE
-
-
- - MONO + +
+
SYMMETRY
+
+ + + + +
-
- - LAIN +
+ +
+
PALETTE
+
+
+ + MONO +
+
+ + LAIN +
+
+ + RED +
+
+ + GREEN +
-
- - RED +
+ +
+
EFFECTS
+
+ + + +
-
- - GREEN +
+ Intensity +
- -
-
EFFECTS
-
- - - - -
-
- Intensity - -
-
-
-
- - -
-
- - - -
-
-
- - 100% - -
-
64x64
-
X: 0 Y: 0
-
- - -
-
-
- FRAMES + + +
+
+ + +
- -
- - - -
- -
- +
+
+ + 100% + +
+
64x64
+
X: 0 Y: 0
- -
-
ANIMATION
-
-
+ + +
+
+
+ FRAMES +
+ +
+ - -
-
- - + +
+
-
- - + +
+
ANIMATION
+
+ + + +
+
+ + +
+
+ + +
-
- - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/.history/src/index_20250519140924.html b/.history/src/index_20250519140924.html index 7192bd6..3c1b006 100644 --- a/.history/src/index_20250519140924.html +++ b/.history/src/index_20250519140924.html @@ -1,307 +1,357 @@ - + - - - - VOIDSKETCH - - - - - - - - -
- -
-
VOIDSKETCH
-
- - - + + + + VOIDSKETCH + + + + + + + + +
+ +
+
VOIDSKETCH
+
+ + + +
-
- -
- -
-
-
- TOOLS -
- -
-
BRUSH
-
- - - - - - - - - - - - - + +
+ +
+
+
+ TOOLS
-
-
- Brush Size - 1 +
+
BRUSH
+
+ + + + + + + + + + + + +
- -
-
-
-
SYMMETRY
-
- - - - - +
+
+ Brush Size + 1 +
+ +
-
-
-
PALETTE
-
-
- - MONO -
-
- - LAIN -
-
- - RED -
-
- - GREEN +
+
SYMMETRY
+
+ + + + +
-
-
-
EFFECTS
-
- - - - -
-
- Intensity - +
+
PALETTE
+
+
+ + MONO +
+
+ + LAIN +
+
+ + RED +
+
+ + GREEN +
+
-
-
-
- -
-
- - - -
-
-
- - 100% - +
+
EFFECTS
+
+ + + + +
+
+ Intensity + +
+
-
64x64
-
X: 0 Y: 0
-
- -
-
-
- FRAMES + +
+
+ + +
- -
- - - +
+
+ + 100% + +
+
64x64
+
X: 0 Y: 0
+
-
- -
+ +
+
+
+ FRAMES +
-
-
ANIMATION
-
- - -
-
- - + +
+
-
- - + +
+
ANIMATION
+
+ + + +
+
+ + +
+
+ + +
-
- - + +
+
- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/.history/src/index_20250519141013.html b/.history/src/index_20250519141013.html index 252c5b3..70121b2 100644 --- a/.history/src/index_20250519141013.html +++ b/.history/src/index_20250519141013.html @@ -1,323 +1,373 @@ - + - - - - VOIDSKETCH - - - - - - - - -
- -
-
VOIDSKETCH
-
- - - + + + + VOIDSKETCH + + + + + + + + +
+ +
+
VOIDSKETCH
+
+ + + +
-
- -
- -
-
-
- TOOLS -
- -
-
BRUSH
-
- - - - - - - - - - - - - + +
+ +
+
+
+ TOOLS
-
-
- Brush Size - 1 +
+
BRUSH
+
+ + + + + + + + + + + + +
- -
-
-
-
SYMMETRY
-
- - - - - +
+
+ Brush Size + 1 +
+ +
-
-
-
PALETTE
-
-
- - MONO -
-
- - LAIN -
-
- - RED -
-
- - GREEN +
+
SYMMETRY
+
+ + + + +
-
-
-
EFFECTS
-
- - - - - - - - -
-
- Intensity - +
+
PALETTE
+
+
+ + MONO +
+
+ + LAIN +
+
+ + RED +
+
+ + GREEN +
+
-
-
-
- -
-
- - - -
-
-
- - 100% - +
+
EFFECTS
+
+ + + + + + + + +
+
+ Intensity + +
+
-
64x64
-
X: 0 Y: 0
-
- -
-
-
- FRAMES + +
+
+ + +
- -
- - - +
+
+ + 100% + +
+
64x64
+
X: 0 Y: 0
+
-
- -
+ +
+
+
+ FRAMES +
-
-
ANIMATION
-
- - -
-
- - + +
+
-
- - + +
+
ANIMATION
+
+ + + +
+
+ + +
+
+ + +
-
- - + +
+
- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/.history/src/index_20250519141039.html b/.history/src/index_20250519141039.html index dce75ca..3e9b0c1 100644 --- a/.history/src/index_20250519141039.html +++ b/.history/src/index_20250519141039.html @@ -1,325 +1,379 @@ - + - - - - VOIDSKETCH - - - - - - - - -
- -
-
VOIDSKETCH
-
- - - + + + + VOIDSKETCH + + + + + + + + +
+ +
+
VOIDSKETCH
+
+ + + +
-
- -
- -
-
-
- TOOLS -
- -
-
BRUSH
-
- - - - - - - - - - - - - + +
+ +
+
+
+ TOOLS
-
-
- Brush Size - 1 +
+
BRUSH
+
+ + + + + + + + + + + + +
- -
-
-
-
SYMMETRY
-
- - - - - +
+
+ Brush Size + 1 +
+ +
-
-
-
PALETTE
-
-
- - MONO -
-
- - LAIN -
-
- - RED -
-
- - GREEN +
+
SYMMETRY
+
+ + + + +
-
-
-
EFFECTS
-
- - - - - - - - -
-
- Intensity - +
+
PALETTE
+
+
+ + MONO +
+
+ + LAIN +
+
+ + RED +
+
+ + GREEN +
+
-
-
-
- -
-
- - - -
-
-
- - 100% - +
+
EFFECTS
+
+ + + + + + + + +
+
+ Intensity + +
+
-
64x64
-
X: 0 Y: 0
-
- -
-
-
- FRAMES + +
+
+ + +
- -
- - - +
+
+ + 100% + +
+
64x64
+
X: 0 Y: 0
+
-
- -
+ +
+
+
+ FRAMES +
-
-
ANIMATION
-
- - -
-
- - + +
+
-
- - + +
+
ANIMATION
+
+ + + +
+
+ + +
+
+ + +
-
- - + +
+
- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/.history/src/index_20250519142527.html b/.history/src/index_20250519142527.html index 83f6f05..78b6e27 100644 --- a/.history/src/index_20250519142527.html +++ b/.history/src/index_20250519142527.html @@ -1,337 +1,391 @@ - + - - - - VOIDSKETCH - - - - - - - - -
- -
-
VOIDSKETCH
-
- - - + + + + VOIDSKETCH + + + + + + + + +
+ +
+
VOIDSKETCH
+
+ + + +
-
- -