// 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'); // canvas.style.imageRendering = 'pixelated'; const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; ctx.webkitImageSmoothingEnabled = false; ctx.mozImageSmoothingEnabled = false; 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 backgroundColor = 'rgb(255, 255, 255)'; 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 === 'brush' || tool === 'content-move' || tool === 'resize' || tool === 'zoom' || tool === 'bucket-fill' ) { saveState(); } if (tool === 'brush') { 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 === 'brush') { drawLineWithCircles(canvasStartX, canvasStartY, canvasEndX, canvasEndY); canvasStartX = canvasEndX; canvasStartY = canvasEndY; } else if (tool === 'content-move') { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = backgroundColor; ctx.fillRect(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.fillStyle = backgroundColor; ctx.fillRect(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 === 'brush') { ctx.closePath(); } else if (tool === 'resize') { canvasWidth = canvas.width; canvasHeight = canvas.height; } updateColorPreview(); }); // }}} // mouseleave {{{ canvasArea.addEventListener('mouseleave', (e) => { isMouseDown = false; brushPreview.style.display = 'none'; }); // }}} // tools {{{ var toolButtons = []; function changeTool(toolName) { toolButtons.forEach(button => button.button.classList.remove('active')); toolButtons.find(button => button.name === toolName).button.classList.add('active'); tool = toolName; brushPreview.style.display = 'none'; updateInfos(); } function createToolButton(displayName, icon, toolName, jumpKey=undefined, temporaryKey=undefined) { const button = document.createElement('div'); button.classList.add('button'); button.classList.add('tool'); button.innerHTML = icon; button.title = displayName; button.addEventListener('click', () => { changeTool(toolName); }); if (jumpKey) { const jumpKeyHint = document.createElement('span'); jumpKeyHint.className = 'jump-key-hint'; jumpKeyHint.innerHTML = jumpKey; button.appendChild(jumpKeyHint); } if (temporaryKey) { const temporaryKeyHint = document.createElement('span'); temporaryKeyHint.className = 'temporary-key-hint'; temporaryKeyHint.innerHTML = temporaryKey; button.appendChild(temporaryKeyHint); } toolBar.appendChild(button); return button; } toolButtons.push({'name': 'brush', 'button': createToolButton('Brush', '', 'brush', 'e', undefined)}); toolButtons.push({'name': 'content-move', 'button': createToolButton('Move Content', '', 'content-move', 'h', undefined)}); toolButtons.push({'name': 'move', 'button': createToolButton('Move Canvas', '', 'move', 'm', undefined)}); toolButtons.push({'name': 'zoom', 'button': createToolButton('Zoom', '', 'zoom', 'z', undefined)}); toolButtons.push({'name': 'resize', 'button': createToolButton('Resize', '', 'resize', 'r', undefined)}); toolButtons.push({'name': 'color-picker', 'button': createToolButton('Color Picker', '', 'color-picker', 'a', undefined)}); toolButtons.push({'name': 'color-mix', 'button': createToolButton('Color Mix', '', 'color-mix', 's', undefined)}); toolButtons.push({'name': 'brush-size', 'button': createToolButton('Brush Size', '', 'brush-size', 'd', undefined)}); toolButtons.push({'name': 'bucket-fill', 'button': createToolButton('Bucket Fill', '', 'bucket-fill', 'f', undefined)}); // }}} // 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', () => { clickFunction() 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', resetZoom)); menuButtons.push(createMenuButton('', 'Add Color', createPuck)); // }}} // pucks {{{ function createPuck(c, editable=true) { if (c === undefined) { c = color; } const puck = document.createElement('div'); puck.className = 'puck'; puck.style.backgroundColor = c; const selectHandle = document.createElement('div'); selectHandle.className = 'select-handle'; selectHandle.innerHTML = ''; puck.appendChild(selectHandle); selectHandle.addEventListener('click', () => { color = puck.style.backgroundColor; updateColorPreview(); updateInfos(); }); if (editable) { const updateHandle = document.createElement('div'); updateHandle.className = 'update-handle'; updateHandle.innerHTML = ''; puck.appendChild(updateHandle); updateHandle.addEventListener('click', () => { puck.style.backgroundColor = color; }); const deleteHandle = document.createElement('div'); deleteHandle.className = 'delete-handle'; deleteHandle.innerHTML = ''; puck.appendChild(deleteHandle); deleteHandle.addEventListener('click', () => { console.log("test"); puck.remove(); }); } puck.addEventListener('mousedown', (e) => { let isMixing = true; const startTime = Date.now(); // Record the time when the mouse is pressed // Interval to update the color based on time const interval = setInterval(() => { if (isMixing) { const elapsedTime = Date.now() - startTime; const t = Math.min(1, elapsedTime / 10000); const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t); color = mixedColor; updateColorPreview(); updateInfos(); } }, 50); // Update every 50ms document.addEventListener('mouseup', onMouseUp); function onMouseUp() { isMixing = false; clearInterval(interval); // Stop the interval when the mouse is released document.removeEventListener('mouseup', onMouseUp); } }); // puck.addEventListener('mousedown', (e) => { // let isMixing = true; // let startX = e.clientX; // let startY = e.clientY; // document.addEventListener('mousemove', onMouseMove); // document.addEventListener('mouseup', onMouseUp); // function onMouseMove(e) { // if (isMixing) { // 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, puck.style.backgroundColor, t); // color = mixedColor; // startX = e.clientX; // startY = e.clientY; // updateColorPreview(); // updateInfos(); // } // } // function onMouseUp() { // isMixing = false; // document.removeEventListener('mousemove', onMouseMove); // document.removeEventListener('mouseup', onMouseUp); // } // }); menuBar.appendChild(puck); } createPuck(c='rgb(0, 0, 0)', editable=false); createPuck(c='rgb(255, 255, 255)', editale=false); createPuck(c='rgb(0, 255, 0)', editale=false); createPuck(c='rgb(0, 0, 255)', editale=false); createPuck(c='rgb(255, 255, 0)', editale=false); createPuck(c='rgb(255, 0, 0)', editale=false); createPuck(c='rgb(255, 0, 255)', editale=false); createPuck(c='rgb(0, 255, 255)', editale=false); // }}} // 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('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()); } // }}} // keybindings {{{ let keyDown = false; let oldTool = tool; const toolBindings = [ {'key': 'e', 'tool': 'brush', 'persistent': true}, {'key': 'h', 'tool': 'content-move', 'persistent': true}, {'key': 'm', 'tool': 'move', 'persistent': true}, {'key': 'z', 'tool': 'zoom', 'persistent': true}, {'key': 'r', 'tool': 'resize', 'persistent': true}, {'key': 'a', 'tool': 'color-picker', 'persistent': false}, {'key': 's', 'tool': 'color-mix', 'persistent': false}, {'key': 'd', 'tool': 'brush-size', 'persistent': false}, ] const functionBindings = [ {'key': 'u', 'function': undo}, {'key': 'y', 'function': redo}, {'key': 'backspace', 'function': clearCanvas}, ] document.addEventListener('keydown', (e) => { if (keyDown) return; if (toolBindings.map(b => b.key).includes(e.key)) { oldTool = tool; keyDown = true; changeTool(toolBindings.find(b => b.key === e.key).tool); return; } if (functionBindings.map(b => b.key).includes(e.key)) { functionBindings.find(b => b.key === e.key).function(); } }); document.addEventListener('keyup', (e) => { keyDown = false; if (toolBindings.filter(b => !b.persistent).map(b => b.key).includes(e.key)) { changeTool(oldTool); } }); // }}} // start {{{ ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, canvas.width, canvas.height); updateInfos(); toolButtons[0]['button'].click(); resetZoom(); // }}}