about summary refs log blame commit diff stats
path: root/js/pixel-art/pixel/app.js
blob: 8a7aceb7ad8ad873ffa5aacf992fb5b4437f6696 (plain) (tree)
1
2
3
4
5
6
7






                                                 









                    



                                                                           


                                                                



                      

                         



                           
 





                                                        





                                                                                    
                                                       

                                                                                        



                                                                                                 
                                                                              
                                                                                

                                                                                 
 
 

                       
                     
 
 



                                                                              

                        




                                             

 
                           


                                                                                                            









                                       






                                                                                                                                                 
                                                        



                                                                                            





                                                    
























                                                                                
             
         
       





























                                                                 




                                                                                                               


                          

                             
        


                       

                                                               


                                                                 


                                                                                                    
     



                          





                                                      



                           



                                                                            
                                                                                     




















                                                                                        






                               








                         







                                                                     

                                                              


                                                          


                                                    
        
                           


                                                                    
        



                                                              





                                                             
            



                                               
     
    
                                         





                                
                         


                        

                                                                          
                                       
    




                                                            
 




                                                                                 
         
 




                                                                


                                                     
                                          

                         


       




                                           
    

                        
                                              






                                                                  

                                                                     
                
                                                                             



                             

 









                                                                                
                                                                         




























                                                
                                           
    












                                                                                               



                











                                                 






                                                                                            





                           


                                     

                                      


                          
                                  

                    
                                  

                    
                                  

                     
                                  


                  



                                           






                              










































































                                                                                                                       
 
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();
}