const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const defaultGridWidth = 16; const defaultGridHeight = 16; let gridWidth = defaultGridWidth; let gridHeight = defaultGridHeight; let cellSize = 16; let colorHistory = []; 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; 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.5)); document.getElementById('zoomOutBtn').addEventListener('click', () => handleZoom(0.666)); 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); resizeCanvas(); loadFromLocalStorage(); 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(); } function initializeGrid() { if (canvases.length > 0) { canvases[currentCanvasIndex].grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); } } function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; centerGrid(); drawGrid(); } function centerGrid() { 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); 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) { if (colorHistory.includes(color)) return; if (colorHistory.length >= 10) colorHistory.shift(); colorHistory.push(color); renderColorHistory(); } function renderColorHistory() { const historyDiv = document.getElementById('colorHistory'); historyDiv.innerHTML = ''; colorHistory.forEach(color => { const colorDiv = document.createElement('div'); colorDiv.style.backgroundColor = color; colorDiv.addEventListener('click', () => { currentColor = color; document.getElementById('colorPicker').value = color; }); historyDiv.appendChild(colorDiv); }); } function handleColorChange() { currentColor = document.getElementById('colorPicker').value; addToColorHistory(currentColor); saveToLocalStorage(); } function handleReset() { 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(); // Clear canvases and add a fresh one canvases = []; addNewCanvas(); // Enable inputs for the fresh canvas document.getElementById('gridWidth').disabled = false; document.getElementById('gridHeight').disabled = false; document.getElementById('gridWidth').value = gridWidth; document.getElementById('gridHeight').value = gridHeight; // Clear localStorage 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 (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() { const newWidth = parseInt(document.getElementById('gridWidth').value); const newHeight = parseInt(document.getElementById('gridHeight').value); // Validate input if (newWidth <= 0 || newHeight <= 0) 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(); } function saveToLocalStorage() { const gridData = { gridWidth, gridHeight, cellSize, colorHistory, currentColor, canvases, isPaletteVisible, globalOffsetX, globalOffsetY }; localStorage.setItem('pixelArtConfig', JSON.stringify(gridData)); } function loadFromLocalStorage() { const savedData = localStorage.getItem('pixelArtConfig'); if (savedData) { const gridData = JSON.parse(savedData); gridWidth = gridData.gridWidth || defaultGridWidth; gridHeight = gridData.gridHeight || defaultGridHeight; cellSize = gridData.cellSize || 16; colorHistory = gridData.colorHistory || []; currentColor = gridData.currentColor || '#000000'; canvases = gridData.canvases || []; globalOffsetX = gridData.globalOffsetX || 0; globalOffsetY = gridData.globalOffsetY || 0; document.getElementById('gridWidth').value = gridWidth; document.getElementById('gridHeight').value = gridHeight; document.getElementById('colorPicker').value = currentColor; isPaletteVisible = gridData.isPaletteVisible ?? true; if (!isPaletteVisible) { palette.classList.add('hidden'); paletteToggle.classList.add('hidden'); paletteToggle.innerHTML = '🎨'; } } else { // 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(); } function exportToPNG() { // Prompt for filename const filename = prompt("Enter a name for your file(s)", "pixel-art"); if (!filename) return; // Cancelled canvases.forEach((canvasData, index) => { 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 = canvasData.grid[x][y] || 'transparent'; tempCtx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } } const paddedNumber = String(index + 1).padStart(2, '0'); const finalFilename = canvases.length > 1 ? `${filename}-${paddedNumber}.png` : `${filename}.png`; tempCanvas.toBlob(blob => { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = finalFilename; link.click(); }); }); } 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(); } } 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 getInputCoordinates(e) { const rect = canvas.getBoundingClientRect(); 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(); } }