// init {{{ const menuBar = document.getElementById('menu-bar'); const toolBar = document.getElementById('tool-bar'); const layerBar = document.getElementById('layer-bar'); const canvasArea = document.getElementById('canvas-area'); const infoBar = document.getElementById('info-bar'); const canvasContainer = document.getElementById('canvas-container'); const brushPreview = document.getElementById('brush-preview'); const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; ctx.webkitImageSmoothingEnabled = false; ctx.mozImageSmoothingEnabled = false; ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); canvas.width = 800; canvas.height = 600; let canvasWidth = canvas.width; let canvasHeight = canvas.height; let undoStack = []; let redoStack = []; let maxHistory = 30; const dZoom = 0.001; let zoom = 1; let brushSize = 5; let dBrushSize = 0.5; let maxBrushSize = 500; let color = 'rgb(0, 0, 0)'; let tool let tempCanvas; let startX, startY; let endX, endY; let dX, dY; let canvasStartX let canvasStartY; let canvasEndX; let canvasEndY; let canvasDX let canvasDY; let isMouseDown = false; const colorPreview = document.createElement('div'); colorPreview.id = 'color-preview'; colorPreview.className = 'puck'; colorPreview.style.backgroundColor = color; menuBar.appendChild(colorPreview); // }}} // helpers {{{ function saveState() { if (undoStack.length >= maxHistory) { undoStack.shift(); // Remove the oldest state if the stack exceeds the limit } // Save the current canvas content and dimensions undoStack.push({ imageData: canvas.toDataURL(), width: canvas.width, height: canvas.height }); redoStack = []; // Clear the redo stack whenever a new action is performed } function undo() { if (undoStack.length > 0) { const currentState = { imageData: canvas.toDataURL(), width: canvas.width, height: canvas.height }; redoStack.push(currentState); // Save current state to the redo stack const lastState = undoStack.pop(); // Get the last state from the undo stack // Restore the canvas dimensions canvas.width = lastState.width; canvas.height = lastState.height; canvas.style.width = canvas.width * zoom + 'px'; canvas.style.height = canvas.height * zoom + 'px'; canvasWidth = canvas.width; canvasHeight = canvas.height; // Restore the canvas content const img = new Image(); img.src = lastState.imageData; img.onload = function() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); }; } } function redo() { if (redoStack.length > 0) { const currentState = { imageData: canvas.toDataURL(), width: canvas.width, height: canvas.height }; undoStack.push(currentState); // Save current state to the undo stack const nextState = redoStack.pop(); // Get the last state from the redo stack // Restore the canvas dimensions canvas.width = nextState.width; canvas.height = nextState.height; canvas.style.width = canvas.width * zoom + 'px'; canvas.style.height = canvas.height * zoom + 'px'; canvasWidth = canvas.width; canvasHeight = canvas.height; // Restore the canvas content const img = new Image(); img.src = nextState.imageData; img.onload = function() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); }; } } function getPositionOnCanvas(e) { const rect = canvas.getBoundingClientRect(); return { x: Math.round((e.clientX - rect.left) / zoom), y: Math.round((e.clientY - rect.top) / zoom), }; } function drawCircle(x, y) { ctx.beginPath(); ctx.arc(x, y, brushSize / 2, 0, 2 * Math.PI, false); ctx.fillStyle = color; ctx.fill(); } function drawLineWithCircles(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; const distance = Math.sqrt(dx * dx + dy * dy); const steps = Math.ceil(distance / (brushSize / 5)); for (let i = 0; i <= steps; i++) { const x = x1 + (dx * i) / steps; const y = y1 + (dy * i) / steps; drawCircle(x, y); } } function saveCanvasContents() { tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const tempCtx = tempCanvas.getContext('2d'); tempCtx.drawImage(canvas, 0, 0); } function updateColorPreview() { colorPreview.style.backgroundColor = color; } function hexToRgbArray(hex) { if (hex.startsWith('#')) { hex = hex.slice(1); } if (hex.length === 3) { hex = hex.split('').map(char => char + char).join(''); } const bigint = parseInt(hex, 16); return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; } function floodFill(x, y, fillColor) { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const targetColor = getColorAtPixel(data, x, y); const fillColorArray = hexToRgbArray(fillColor); if (colorsMatch(targetColor, fillColorArray)) { return; // The clicked point is already the fill color } const stack = [{x, y}]; while (stack.length > 0) { const {x, y} = stack.pop(); const currentColor = getColorAtPixel(data, x, y); if (colorsMatch(currentColor, targetColor)) { setColorAtPixel(data, x, y, fillColorArray); if (x > 0) stack.push({x: x - 1, y}); if (x < canvas.width - 1) stack.push({x: x + 1, y}); if (y > 0) stack.push({x, y: y - 1}); if (y < canvas.height - 1) stack.push({x, y: y + 1}); } } ctx.putImageData(imageData, 0, 0); } function getColorAtPixel(data, x, y) { const index = (y * canvas.width + x) * 4; return [data[index], data[index + 1], data[index + 2], data[index + 3]]; } function setColorAtPixel(data, x, y, color) { const index = (y * canvas.width + x) * 4; data[index] = color[0]; data[index + 1] = color[1]; data[index + 2] = color[2]; data[index + 3] = 255; // Set alpha to fully opaque } function colorsMatch(a, b) { return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; } // }}} // mousedown {{{ canvasArea.addEventListener('mousedown', (e) => { if (e.target.closest('.puck')) return; startX = e.clientX; startY = e.clientY; canvasStartX = getPositionOnCanvas(e).x; canvasStartY = getPositionOnCanvas(e).y; saveCanvasContents(); isMouseDown = true; if ( tool === 'draw' || tool === 'content-move' || tool === 'resize' || tool === 'zoom' || tool === 'bucket-fill' ) { saveState(); } if (tool === 'draw') { drawCircle(canvasStartX, canvasStartY); } else if (tool === 'bucket-fill') { floodFill(canvasStartX, canvasStartY, color); return; } else if (tool === 'move') { startX = e.clientX - canvasContainer.offsetLeft; startY = e.clientY - canvasContainer.offsetTop; } else if (tool === 'color-picker') { const imageData = ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data; const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; color = pickedColor; console.log('Picked Color:', pickedColor); updateColorPreview(); return; } }); // }}} // mousemove {{{ canvasArea.addEventListener('mousemove', (e) => { endX = e.clientX; endY = e.clientY; dX = endX - startX; dY = endY - startY; canvasEndX = getPositionOnCanvas(e).x; canvasEndY = getPositionOnCanvas(e).y; canvasDX = canvasEndX - canvasStartX; canvasDY = canvasEndY - canvasStartY; if (tool == 'brush-size') { brushPreview.style.display = 'block'; brushPreview.style.width = brushSize + 'px'; brushPreview.style.height = brushSize + 'px'; brushPreview.style.left = e.clientX - brushSize / 2 + 'px'; brushPreview.style.top = e.clientY - brushSize / 2 + 'px'; } if (isMouseDown) { if (tool === 'brush-size') { brushSize += dX * dBrushSize; if (brushSize < 1) brushSize = 1; if (brushSize > maxBrushSize) brushSize = maxBrushSize; startX = endX; } else if (tool === 'draw') { drawLineWithCircles(canvasStartX, canvasStartY, canvasEndX, canvasEndY); canvasStartX = canvasEndX; canvasStartY = canvasEndY; } else if (tool === 'content-move') { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(tempCanvas, dX, dY); } else if (tool === 'move') { canvasContainer.style.left = dX + 'px'; canvasContainer.style.top = dY + 'px'; } else if (tool === 'zoom') { zoom += dX * dZoom; if (zoom < 0.1) zoom = 0.1; canvas.style.height = canvasHeight * zoom + 'px'; canvas.style.width = canvasWidth * zoom + 'px'; startX = endX; } else if (tool === 'resize') { let newWidth = canvasWidth + dX / zoom; let newHeight = canvasHeight + dY / zoom; if (newWidth > 0 && newHeight > 0) { canvas.width = newWidth; canvas.height = newHeight; canvas.style.width = newWidth * zoom + 'px'; canvas.style.height = newHeight * zoom + 'px'; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(tempCanvas, 0, 0); } } else if (tool === 'color-mix') { const imageData = ctx.getImageData(canvasEndX, canvasEndY, 1, 1).data; const canvasColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); const t = Math.min(1, distance / 300); const mixedColor = mixbox.lerp(color, canvasColor, t); color = mixedColor; startX = e.clientX; startY = e.clientY; } } updateInfos(); updateColorPreview(); }); // }}} // mouseup {{{ canvasArea.addEventListener('mouseup', (e) => { isMouseDown = false; if (tool === 'draw') { ctx.closePath(); } else if (tool === 'resize') { canvasWidth = canvas.width; canvasHeight = canvas.height; } updateColorPreview(); }); // }}} // mouseleave {{{ canvasArea.addEventListener('mouseleave', (e) => { brushPreview.style.display = 'none'; }); // }}} // tools {{{ var toolButtons = []; function createToolButton(icon, buttonTool) { const button = document.createElement('div'); button.classList.add('button'); button.classList.add('tool'); button.innerHTML = icon; button.title = buttonTool; button.addEventListener('click', () => { buttons = document.getElementsByClassName('tool-button'); toolButtons.forEach(button => button.button.classList.remove('active')); button.classList.add('active'); tool = buttonTool; updateInfos(); }); toolBar.appendChild(button); return button; } toolButtons.push({'name': 'draw', 'button': createToolButton('', 'draw')}); toolButtons.push({'name': 'content-move', 'button': createToolButton('', 'content-move')}); toolButtons.push({'name': 'move', 'button': createToolButton('', 'move')}); toolButtons.push({'name': 'zoom', 'button': createToolButton('', 'zoom')}); toolButtons.push({'name': 'resize', 'button': createToolButton('', 'resize')}); toolButtons.push({'name': 'color-picker', 'button': createToolButton('', 'color-picker')}); toolButtons.push({'name': 'color-mix', 'button': createToolButton('', 'color-mix')}); toolButtons.push({'name': 'brush-size', 'button': createToolButton('', 'brush-size')}); toolButtons.push({'name': 'bucket-fill', 'button': createToolButton('', 'bucket-fill')}); // }}} // menu functons {{{ function flipCanvasHorizontally(e) { saveState(); ctx.save(); saveCanvasContents(); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.scale(-1, 1); ctx.translate(-canvas.width, 0); ctx.drawImage(tempCanvas, 0, 0); ctx.restore(); } function flipCanvasVertically(e) { saveState(); ctx.save(); saveCanvasContents(); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.scale(1, -1); ctx.translate(0, -canvas.height); ctx.drawImage(tempCanvas, 0, 0); ctx.restore(); } function saveCanvas(e) { const link = document.createElement('a'); link.download = 'canvas.png'; link.href = canvas.toDataURL(); link.click(); } function openCanvas(e) { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = (e) => { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); } img.src = e.target.result; } reader.readAsDataURL(file); } input.click(); } function clearCanvas(e) { saveState(); ctx.clearRect(0, 0, canvas.width, canvas.height); } function resetZoom(e) { zoom = 1; canvas.style.width = canvas.width * zoom + 'px'; canvas.style.height = canvas.height * zoom + 'px'; canvasWidth = canvas.width; canvasHeight = canvas.height; canvasAreaRect = canvasArea.getBoundingClientRect(); canvasContainer.style.left = `${canvasAreaRect.left}px`; canvasContainer.style.top = `${canvasAreaRect.top}px`; } // }}} // menu {{{ var menuButtons = []; function createMenuButton(icon, name, clickFunction) { const button = document.createElement('div'); button.className = 'button'; button.innerHTML = icon; button.title = name; if (clickFunction) { button.addEventListener('click', (e) => { clickFunction(e) updateInfos(); }); } menuBar.appendChild(button); return button; } menuButtons.push(createMenuButton('', 'Open', openCanvas)); menuButtons.push(createMenuButton('', 'Save', saveCanvas)); menuButtons.push(createMenuButton('', 'Flip Horizontally', flipCanvasHorizontally)); menuButtons.push(createMenuButton('', 'Flip Vertically', flipCanvasVertically)); menuButtons.push(createMenuButton('', 'Undo', undo)); menuButtons.push(createMenuButton('', 'Redo', redo)); menuButtons.push(createMenuButton('', 'Clear', clearCanvas)); menuButtons.push(createMenuButton('', 'Reset Zoom', resetZoom)); // }}} // pucks {{{ function createPuck(c) { const puck = document.createElement('div'); puck.className = 'puck'; puck.style.backgroundColor = c; puck.addEventListener('click', () => { color = c; updateColorPreview(); updateInfos(); }); menuBar.appendChild(puck); } createPuck('rgb(255, 0, 0)'); createPuck('rgb(0, 255, 0)'); createPuck('rgb(0, 0, 255)'); createPuck('rgb(255, 255, 0)'); createPuck('rgb(255, 0, 255)'); createPuck('rgb(0, 255, 255)'); createPuck('rgb(0, 0, 0)'); createPuck('rgb(255, 255, 255)'); // }}} // info {{{ var infos = []; function createInfo(name, updateFunction) { const info = document.createElement('span'); info.className = 'info'; const key = document.createElement('span'); key.className = 'key'; key.innerHTML = name + ':'; const value = document.createElement('span'); value.className = 'value'; value.innerHTML = '0'; info.appendChild(key); info.appendChild(value); infoBar.appendChild(info); function update() { let v = updateFunction(); if (v === undefined) v = '?'; value.innerHTML = v; } return update; } infos.push(createInfo('tool', function() { return tool; })); infos.push(createInfo('zoom', function() { var percent = zoom * 100; return percent.toFixed(0) + '%'; })); infos.push(createInfo('brush', function() { return brushSize; })); infos.push(createInfo('x', function() { return canvasEndX; })); infos.push(createInfo('y', function() { return canvasEndY; })); infos.push(createInfo('color', function() { return color; })); infos.push(createInfo('width', function() { return canvas.width; })); infos.push(createInfo('height', function() { return canvas.height; })); function updateInfos() { infos.forEach(info => info()); } // }}} // start {{{ updateInfos(); toolButtons[0]['button'].click(); // }}}