diff options
Diffstat (limited to 'js/pixel-art')
-rw-r--r-- | js/pixel-art/pixel/app.js | 517 | ||||
-rw-r--r-- | js/pixel-art/pixel/index.html | 187 |
2 files changed, 603 insertions, 101 deletions
diff --git a/js/pixel-art/pixel/app.js b/js/pixel-art/pixel/app.js index 087801c..2d83997 100644 --- a/js/pixel-art/pixel/app.js +++ b/js/pixel-art/pixel/app.js @@ -5,28 +5,81 @@ const defaultGridHeight = 16; let gridWidth = defaultGridWidth; let gridHeight = defaultGridHeight; let cellSize = 16; -let colorHistory = []; +let colorHistory = [ + '#000000', + '#ae8ce2', + '#2d5d9e', + '#43bef2', + '#99b213', + '#e5b42e', + '#c00f68', + '#ffffff' +]; let currentColor = '#000000'; let grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); let offsetX = 0; let offsetY = 0; +const paletteToggle = document.getElementById('palette-toggle'); +const palette = document.getElementById('palette'); +let isPaletteVisible = true; +let isDrawing = false; +let lastX = null; +let lastY = null; +let lastCell = null; +const MIN_CELL_SIZE = 4; +const MAX_CELL_SIZE = 64; +let canvases = []; +let currentCanvasIndex = 0; +let globalOffsetX = 0; +let globalOffsetY = 0; -// Event Listeners -canvas.addEventListener('click', handleCanvasClick); +canvas.addEventListener('mousedown', handleInputStart); +canvas.addEventListener('mousemove', handleInputMove); +canvas.addEventListener('mouseup', handleInputEnd); +canvas.addEventListener('touchstart', handleInputStart); +canvas.addEventListener('touchmove', handleInputMove); +canvas.addEventListener('touchend', handleInputEnd); document.getElementById('colorPicker').addEventListener('input', handleColorChange); document.getElementById('gridWidth').addEventListener('change', updateGridSize); document.getElementById('gridHeight').addEventListener('change', updateGridSize); document.getElementById('resetBtn').addEventListener('click', handleReset); document.getElementById('exportBtn').addEventListener('click', exportToPNG); window.addEventListener('keydown', handlePan); +paletteToggle.addEventListener('click', togglePalette); +document.getElementById('zoomInBtn').addEventListener('click', () => handleZoom(1.25)); +document.getElementById('zoomOutBtn').addEventListener('click', () => handleZoom(0.75)); +document.getElementById('panUpBtn').addEventListener('click', () => handlePanButton('up')); +document.getElementById('panDownBtn').addEventListener('click', () => handlePanButton('down')); +document.getElementById('panLeftBtn').addEventListener('click', () => handlePanButton('left')); +document.getElementById('panRightBtn').addEventListener('click', () => handlePanButton('right')); +document.getElementById('centerViewBtn').addEventListener('click', resetView); +document.getElementById('newCanvasBtn').addEventListener('click', addNewCanvas); +document.getElementById('saveProjectBtn').addEventListener('click', saveProject); +document.getElementById('loadProjectBtn').addEventListener('click', loadProject); + -// Initialization resizeCanvas(); loadFromLocalStorage(); +renderColorHistory(); + + +function addNewCanvas() { + canvases.push({ + grid: Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)), + offsetX: 0, + offsetY: 0, + hasPixels: false + }); + currentCanvasIndex = canvases.length - 1; + centerGrid(); + drawGrid(); + saveToLocalStorage(); +} -// Functions function initializeGrid() { - grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); + if (canvases.length > 0) { + canvases[currentCanvasIndex].grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); + } } function resizeCanvas() { @@ -37,22 +90,52 @@ function resizeCanvas() { } function centerGrid() { - offsetX = Math.max((canvas.width - (gridWidth * cellSize)) / 2, 0); - offsetY = Math.max((canvas.height - (gridHeight * cellSize)) / 2, 0); + if (canvases.length === 0) return; + + canvases.forEach((canvasData, index) => { + canvasData.offsetY = Math.max((canvas.height - (gridHeight * cellSize)) / 2, 0); + if (index === 0) { + canvasData.offsetX = Math.max((canvas.width - (gridWidth * cellSize * canvases.length) - (cellSize * (canvases.length - 1))) / 2, 0); + } else { + // Position each canvas one cell width apart + const previousCanvas = canvases[index - 1]; + canvasData.offsetX = previousCanvas.offsetX + (gridWidth * cellSize) + cellSize; + } + }); } function drawGrid() { ctx.fillStyle = 'teal'; ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.strokeStyle = '#888888'; - for (let x = 0; x < gridWidth; x++) { - for (let y = 0; y < gridHeight; y++) { - ctx.fillStyle = grid[x][y] || '#f7f7f7'; - ctx.fillRect(x * cellSize + offsetX, y * cellSize + offsetY, cellSize, cellSize); - ctx.strokeRect(x * cellSize + offsetX, y * cellSize + offsetY, cellSize, cellSize); + canvases.forEach((canvasData, index) => { + const xOffset = canvasData.offsetX + globalOffsetX; + + for (let x = 0; x < gridWidth; x++) { + for (let y = 0; y < gridHeight; y++) { + const cellX = x * cellSize + xOffset; + const cellY = y * cellSize + canvasData.offsetY + globalOffsetY; + + // Fill cell background + ctx.fillStyle = canvasData.grid[x][y] || '#f7f7f7'; + ctx.fillRect(cellX, cellY, cellSize, cellSize); + + // Draw cell border + ctx.strokeStyle = '#888888'; + ctx.strokeRect(cellX, cellY, cellSize, cellSize); + + // Draw diagonal line for empty cells + if (!canvasData.grid[x][y]) { + ctx.beginPath(); + ctx.strokeStyle = '#bfbfbf'; + ctx.moveTo(cellX, cellY); + ctx.lineTo(cellX + cellSize, cellY + cellSize); + ctx.stroke(); + ctx.strokeStyle = '#888888'; + } + } } - } + }); } function addToColorHistory(color) { @@ -83,32 +166,69 @@ function handleColorChange() { } function handleReset() { - gridWidth = defaultGridWidth; - gridHeight = defaultGridHeight; - initializeGrid(); - centerGrid(); - drawGrid(); - localStorage.removeItem('pixelArtConfig'); - colorHistory = []; - renderColorHistory(); - document.getElementById('gridWidth').value = gridWidth; - document.getElementById('gridHeight').value = gridHeight; - alert("Grid reset, color history cleared, and local storage cleared."); + const confirmReset = confirm("Are you sure you want to reset? This will clear your drawing and settings."); + + if (confirmReset) { + gridWidth = defaultGridWidth; + gridHeight = defaultGridHeight; + cellSize = 16; + globalOffsetX = 0; + globalOffsetY = 0; + colorHistory = []; + renderColorHistory(); + + canvases = []; + addNewCanvas(); + + document.getElementById('gridWidth').disabled = false; + document.getElementById('gridHeight').disabled = false; + document.getElementById('gridWidth').value = gridWidth; + document.getElementById('gridHeight').value = gridHeight; + + localStorage.removeItem('pixelArtConfig'); + + alert("Reset complete. You can now adjust the grid size until you place your first pixel."); + } } function handlePan(e) { const step = cellSize; - if (e.key === 'ArrowUp') offsetY += step; - if (e.key === 'ArrowDown') offsetY -= step; - if (e.key === 'ArrowLeft') offsetX += step; - if (e.key === 'ArrowRight') offsetX -= step; + if (canvases.length === 0) return; + + if (e.key === 'ArrowUp') globalOffsetY += step; + if (e.key === 'ArrowDown') globalOffsetY -= step; + if (e.key === 'ArrowLeft') globalOffsetX += step; + if (e.key === 'ArrowRight') globalOffsetX -= step; drawGrid(); } function updateGridSize() { - gridWidth = parseInt(document.getElementById('gridWidth').value); - gridHeight = parseInt(document.getElementById('gridHeight').value); - initializeGrid(); + const newWidth = parseInt(document.getElementById('gridWidth').value); + const newHeight = parseInt(document.getElementById('gridHeight').value); + + // Validate input + if (newWidth <= 0 || newHeight <= 0 || newWidth > 100 || newHeight > 100) return; + + gridWidth = newWidth; + gridHeight = newHeight; + + // Update all existing canvases with new dimensions + canvases.forEach(canvasData => { + const newGrid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); + + // Preserve existing pixel data where possible + const minWidth = Math.min(canvasData.grid.length, gridWidth); + const minHeight = Math.min(canvasData.grid[0].length, gridHeight); + + for (let x = 0; x < minWidth; x++) { + for (let y = 0; y < minHeight; y++) { + newGrid[x][y] = canvasData.grid[x][y]; + } + } + + canvasData.grid = newGrid; + }); + centerGrid(); drawGrid(); saveToLocalStorage(); @@ -116,12 +236,15 @@ function updateGridSize() { function saveToLocalStorage() { const gridData = { - gridWidth: gridWidth, - gridHeight: gridHeight, - cellSize: cellSize, - colorHistory: colorHistory, - currentColor: currentColor, - grid: grid, + gridWidth, + gridHeight, + cellSize, + colorHistory, + currentColor, + canvases, + isPaletteVisible, + globalOffsetX, + globalOffsetY }; localStorage.setItem('pixelArtConfig', JSON.stringify(gridData)); } @@ -130,57 +253,309 @@ function loadFromLocalStorage() { const savedData = localStorage.getItem('pixelArtConfig'); if (savedData) { const gridData = JSON.parse(savedData); - gridWidth = gridData.gridWidth || 10; - gridHeight = gridData.gridHeight || 10; + gridWidth = gridData.gridWidth || defaultGridWidth; + gridHeight = gridData.gridHeight || defaultGridHeight; cellSize = gridData.cellSize || 16; colorHistory = gridData.colorHistory || []; currentColor = gridData.currentColor || '#000000'; - grid = gridData.grid || Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); + canvases = gridData.canvases || []; + globalOffsetX = gridData.globalOffsetX || 0; + globalOffsetY = gridData.globalOffsetY || 0; + + // Set input values document.getElementById('gridWidth').value = gridWidth; document.getElementById('gridHeight').value = gridHeight; document.getElementById('colorPicker').value = currentColor; - centerGrid(); - drawGrid(); + + // Disable grid size inputs if there's saved data + document.getElementById('gridWidth').disabled = true; + document.getElementById('gridHeight').disabled = true; + + isPaletteVisible = gridData.isPaletteVisible ?? true; + if (!isPaletteVisible) { + palette.classList.add('hidden'); + paletteToggle.classList.add('hidden'); + paletteToggle.innerHTML = '🎨'; + } } else { - initializeGrid(); - centerGrid(); - drawGrid(); + // No saved data, create default canvas + gridWidth = defaultGridWidth; + gridHeight = defaultGridHeight; + addNewCanvas(); } + + // Ensure there's at least one canvas + if (canvases.length === 0) { + addNewCanvas(); + } + + centerGrid(); + drawGrid(); + renderColorHistory(); } function exportToPNG() { - const tempCanvas = document.createElement('canvas'); - const tempCtx = tempCanvas.getContext('2d'); - tempCanvas.width = gridWidth * cellSize; - tempCanvas.height = gridHeight * cellSize; - - for (let x = 0; x < gridWidth; x++) { - for (let y = 0; y < gridHeight; y++) { - tempCtx.fillStyle = grid[x][y] || 'transparent'; - tempCtx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + // Prompt for filename + const filename = prompt("Enter a name for your file(s)", "pixel-art"); + if (!filename) return; // Cancelled + + // An array of promises for each canvas + const exportPromises = canvases.map((canvasData, index) => { + return new Promise(resolve => { + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = gridWidth * cellSize; + tempCanvas.height = gridHeight * cellSize; + + // Draw the canvas content + for (let x = 0; x < gridWidth; x++) { + for (let y = 0; y < gridHeight; y++) { + tempCtx.fillStyle = canvasData.grid[x][y] || 'transparent'; + tempCtx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + } + + // Convert to data URL (trying to work around a webkit bug where blobs don't work so well) + const dataURL = tempCanvas.toDataURL('image/png'); + const paddedNumber = String(index + 1).padStart(2, '0'); + const finalFilename = canvases.length > 1 + ? `${filename}-${paddedNumber}.png` + : `${filename}.png`; + + resolve({ dataURL, filename: finalFilename }); + }); + }); + + // Process exports sequentially with delay + Promise.all(exportPromises).then(exports => { + exports.forEach((exportData, index) => { + setTimeout(() => { + const link = document.createElement('a'); + link.href = exportData.dataURL; + link.download = exportData.filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, index * 1000); // 1 second delay between each download + }); + }); +} + +function handleInputStart(e) { + e.preventDefault(); + isDrawing = true; + const coords = getInputCoordinates(e); + const cell = getCellFromCoords(coords); + + if (cell) { + lastCell = cell; + currentCanvasIndex = cell.canvasIndex; + + // Lock grid size on first pixel placement + if (!document.getElementById('gridWidth').disabled) { + document.getElementById('gridWidth').disabled = true; + document.getElementById('gridHeight').disabled = true; + } + + if (canvases[currentCanvasIndex].grid[cell.x][cell.y]) { + canvases[currentCanvasIndex].grid[cell.x][cell.y] = null; + } else { + canvases[currentCanvasIndex].grid[cell.x][cell.y] = currentColor; } + drawGrid(); + saveToLocalStorage(); } +} - tempCanvas.toBlob(blob => { - const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = 'pixel-art.png'; - link.click(); - }); +function handleInputMove(e) { + e.preventDefault(); + if (!isDrawing) return; + + const coords = getInputCoordinates(e); + const cell = getCellFromCoords(coords); + + if (cell && (!lastCell || cell.x !== lastCell.x || cell.y !== lastCell.y)) { + lastCell = cell; + // When dragging, always draw (don't erase) + canvases[currentCanvasIndex].grid[cell.x][cell.y] = currentColor; + drawGrid(); + saveToLocalStorage(); + } +} + +function handleInputEnd(e) { + e.preventDefault(); + isDrawing = false; + lastCell = null; } -function handleCanvasClick(e) { +function getInputCoordinates(e) { const rect = canvas.getBoundingClientRect(); - const x = Math.floor((e.clientX - rect.left - offsetX) / cellSize); - const y = Math.floor((e.clientY - rect.top - offsetY) / cellSize); - - if (x >= 0 && x < gridWidth && y >= 0 && y < gridHeight) { - if (e.detail === 2) { // Double-click resets the cell - grid[x][y] = null; - } else { // Single-click paints the cell with the current color - grid[x][y] = currentColor; + if (e.touches) { + // Touch event + return { + x: e.touches[0].clientX - rect.left, + y: e.touches[0].clientY - rect.top + }; + } else { + // Mouse event + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; + } +} + +function getCellFromCoords(coords) { + if (canvases.length === 0) return null; + + for (let i = 0; i < canvases.length; i++) { + const canvasData = canvases[i]; + const canvasLeft = canvasData.offsetX + globalOffsetX; + const canvasRight = canvasLeft + (gridWidth * cellSize); + + if (coords.x >= canvasLeft && coords.x < canvasRight) { + const x = Math.floor((coords.x - canvasLeft) / cellSize); + const y = Math.floor((coords.y - (canvasData.offsetY + globalOffsetY)) / cellSize); + + if (x >= 0 && x < gridWidth && y >= 0 && y < gridHeight) { + return { x, y, canvasIndex: i }; + } } + } + return null; +} + +function togglePalette() { + isPaletteVisible = !isPaletteVisible; + + if (isPaletteVisible) { + palette.classList.remove('hidden'); + paletteToggle.classList.remove('hidden'); + paletteToggle.innerHTML = '☰'; + } else { + palette.classList.add('hidden'); + paletteToggle.classList.add('hidden'); + paletteToggle.innerHTML = '🎨'; + } +} + +function handleZoom(factor) { + const newCellSize = Math.max(MIN_CELL_SIZE, Math.min(MAX_CELL_SIZE, cellSize * factor)); + + if (newCellSize === cellSize) return; + + cellSize = newCellSize; + + centerGrid(); + + drawGrid(); + saveToLocalStorage(); +} + +function handlePanButton(direction) { + if (canvases.length === 0) return; + + const step = cellSize; + switch(direction) { + case 'up': + globalOffsetY += step; + break; + case 'down': + globalOffsetY -= step; + break; + case 'left': + globalOffsetX += step; + break; + case 'right': + globalOffsetX -= step; + break; + } + drawGrid(); +} + +function resetView() { + cellSize = 16; // Reset to default zoom + globalOffsetX = 0; + globalOffsetY = 0; + if (canvases.length > 0) { + centerGrid(); drawGrid(); saveToLocalStorage(); } +} + +function saveProject() { + const now = new Date(); + const formattedDate = now.toISOString().slice(0, 16).replace('T', '-').replace(':', '-'); + + const projectName = prompt("Enter a name for your project", formattedDate); + if (!projectName) return; // User cancelled + + // First save to localStorage to ensure all current state is saved + saveToLocalStorage(); + + // Get the data from localStorage and add our special header + const projectData = JSON.parse(localStorage.getItem('pixelArtConfig')); + const exportData = { + __projectHeader: "pppppp_v1", // Add special header + timestamp: new Date().toISOString(), + data: projectData + }; + + // Create and trigger download + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${projectName}.json`; + link.click(); + URL.revokeObjectURL(link.href); +} + +function loadProject() { + // AAAAH! Data loss! + const confirmLoad = confirm("Loading a project will replace your current work. Are you sure you want to proceed?"); + if (!confirmLoad) return; + + // Create file input + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.json'; + + fileInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const importedData = JSON.parse(e.target.result); + + // Check for our super special header + if (!importedData.__projectHeader || + importedData.__projectHeader !== "pppppp_v1") { + throw new Error('This file is not a valid Pixel Art Project file'); + } + + const projectData = importedData.data; + + // Validate the data has expected properties + if (!projectData.gridWidth || !projectData.gridHeight || !projectData.canvases) { + throw new Error('Invalid project file format'); + } + + // Save to localStorage + localStorage.setItem('pixelArtConfig', JSON.stringify(projectData)); + + // Reload the page to apply changes + window.location.reload(); + + } catch (error) { + alert('Error loading project file: ' + error.message); + } + }; + reader.readAsText(file); + }); + + fileInput.click(); } \ No newline at end of file diff --git a/js/pixel-art/pixel/index.html b/js/pixel-art/pixel/index.html index 91f1813..7e3df56 100644 --- a/js/pixel-art/pixel/index.html +++ b/js/pixel-art/pixel/index.html @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>Pixel Pixel Pixel Pixel Pixel Pixel</title> <meta name="description" content="Draw me a goblin, please."> <style> @@ -12,74 +12,201 @@ overflow: hidden; background-color: teal; } + button { - padding: 4px 10px; - font-size: 16px; + padding: 0.5rem 0.75rem; + font-size: 1rem; cursor: pointer; + min-height: 2.75rem; } + + button.reset-btn { + cursor: pointer; + margin: 0; + padding: 0.25rem 0.5rem; + border: none; + background-color: tomato; + color: #fafafa; + min-height: 2rem; + font-size: 0.875rem; + border-radius: 0.25rem; + margin-top: 0.5rem; + } + #canvas { display: block; } + #palette { - position: absolute; - top: 10px; - right: 10px; + position: fixed; + top: 0.625rem; + right: 0.625rem; background-color: beige; - padding: 10px; + padding: 0.5rem; border: 1px solid #ccc; - border-radius: 5px; - width: 150px; + border-radius: 0.25rem; + width: 10.625rem; + transition: transform 0.3s ease-out; + max-height: 90vh; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + box-sizing: border-box; + } + + #palette.hidden { + transform: translateX(calc(100% + 1.25rem)); + } + + #palette-toggle { + position: fixed; + top: 1.025rem; + right: 0.025rem; + background-color: beige; + border: 1px solid #ccc; + border-radius: 0.25rem 0 0 0.25rem; + padding: 0.75rem; + cursor: pointer; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease-out; + min-width: 2.75rem; + min-height: 2.75rem; + font-size: 1.125rem; + } + + #palette-toggle:not(.hidden) { + transform: translateX(-11.25rem); } - #palette label, #palette input, #palette button { display: block; - margin-bottom: 10px; + margin-bottom: 0.75rem; width: 100%; } .color-history { display: flex; flex-wrap: wrap; + margin-top: 0.625rem; } .color-history div { - width: 20px; - height: 20px; + width: 1.25rem; + height: 1.25rem; border: 1px solid #000; cursor: pointer; - margin-right: 5px; - margin-bottom: 5px; + margin-right: 0.3125rem; + margin-bottom: 0.3125rem; } - .color-history { + .zoom-controls { display: flex; - margin-top: 10px; + gap: 0.3125rem; + margin-top: 0.625rem; + justify-content: space-between; + width: 100%; } - .color-history div { - width: 20px; - height: 20px; - border: 1px solid #000; - cursor: pointer; - margin-right: 5px; + + .zoom-controls button { + flex: 1; + padding: 0.25rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + min-height: unset; + width: unset; + } + + .pan-controls { + display: flex; + flex-direction: column; + gap: 0.3125rem; + margin-top: 0.625rem; + align-items: center; + } + + .pan-middle { + display: flex; + gap: 0.3125rem; + width: 100%; + } + + .pan-controls button { + padding: 0.25rem; + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + min-height: unset; + } + + #centerViewBtn { + margin-top: 0.625rem; + } + + @media (max-width: 48rem) { + #palette { + width: 12.5rem; + top: auto; + bottom: 0.625rem; + } + + #palette-toggle { + top: auto; + right: 0.625rem; + bottom: 0.625rem; + } + + #palette-toggle:not(.hidden) { + transform: translateX(-12.5rem); + } } </style> </head> <body> <canvas id="canvas"></canvas> + <button id="palette-toggle">☰</button> <div id="palette"> - <label for="gridWidth">Grid Width:</label> - <input type="number" id="gridWidth" value="16" min="1"> - <label for="gridHeight">Grid Height:</label> - <input type="number" id="gridHeight" value="16" min="1"> - <br> + <div style="display: flex; gap: 5px; margin-bottom: 10px;"> + <div style="flex: 1;"> + <label for="gridWidth">Width:</label> + <input type="number" id="gridWidth" value="16" min="1" max="100" style="width: 90%;"> + </div> + <div style="flex: 1;"> + <label for="gridHeight">Height:</label> + <input type="number" id="gridHeight" value="16" min="1" max="100" style="width: 90%;"> + </div> + </div> <label for="colorPicker">Select Color:</label> <input type="color" id="colorPicker" value="#000000"> <div id="colorHistory" class="color-history"></div> + <div class="zoom-controls"> + <button id="zoomInBtn">🔍+</button> + <button id="zoomOutBtn">🔍-</button> + </div> + <div class="pan-controls"> + <button id="panUpBtn">⬆️</button> + <div class="pan-middle"> + <button id="panLeftBtn">⬅️</button> + <button id="panRightBtn">➡️</button> + </div> + <button id="panDownBtn">⬇️</button> + </div> + <button id="centerViewBtn">🎯 Center View</button> + <button id="newCanvasBtn">➕ New Canvas</button> + <hr> <button id="exportBtn">Export as PNG</button> - <button id="resetBtn">Reset</button> - <!--<input type="file" id="importBtn" accept="image/png">--> + <button id="saveProjectBtn">Save Project</button> + <button id="loadProjectBtn">Load Project</button> + <button id="resetBtn" class="reset-btn">Reset</button> </div> <script src="app.js"></script> </body> |