about summary refs log tree commit diff stats
path: root/js/pixel-art
diff options
context:
space:
mode:
Diffstat (limited to 'js/pixel-art')
-rw-r--r--js/pixel-art/pixel/app.js517
-rw-r--r--js/pixel-art/pixel/index.html187
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>