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 = [
'#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;
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);
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();
}
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();
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 (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 || 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();
}
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;
// Set input values
document.getElementById('gridWidth').value = gridWidth;
document.getElementById('gridHeight').value = gridHeight;
document.getElementById('colorPicker').value = currentColor;
// 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 {
// 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() {
// 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();
}
}
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();
}