const colorPreview = document.createElement('div'); colorPreview.id = 'color-preview'; colorPreview.className = 'puck'; colorPreview.style.backgroundColor = color; menuBar.appendChild(colorPreview); // }}} // helpers {{{ function saveCanvas() { const dataURL = canvas.toDataURL(); const dimensions = `${canvas.width}x${canvas.height}`; localStorage.setItem('mixxCanvas', dataURL); localStorage.setItem('mixxDimensions', dimensions); console.log('Canvas saved'); } function loadCanvas() { const dataURL = localStorage.getItem('mixxCanvas'); const dimensions = localStorage.getItem('mixxDimensions'); if (dataURL && dimensions) { const img = new Image(); img.src = dataURL; img.onload = function() { canvas.width = dimensions.split('x')[0]; canvas.height = dimensions.split('x')[1]; canvas.style.width = canvas.width * zoom + 'px'; canvas.style.height = canvas.height * zoom + 'px'; canvasWidth = canvas.width; canvasHeight = canvas.height; ctx.drawImage(img, 0, 0); } } else { console.log('No saved canvas found'); } } function clearCanvasFromLocalStorage() { localStorage.removeItem('savedCanvas'); localStorage.removeItem('canvasDimensions'); } function saveState() { if (undoStack.length >= maxHistory) { undoStack.shift(); // Remove the oldest state if the stack exceeds the limit } 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 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; 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 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; 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') { console.log('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'; }); // }}} // keybindings {{{ const toolKeyBindings = {} const functionKeyBindings = { } document.addEventListener('keydown', (e) => { if (keyDown) return; const newTool = toolKeyBindings[e.key.toLowerCase()] if (newTool) { prevTool = tool; keyDown = true; changeTool(newTool); return; } const func = functionKeyBindings[e.key]; if (func) { func(); return; } }); document.addEventListener('keyup', (e) => { const currentTool = toolKeyBindings[e.key.toLowerCase()] if (currentTool) { keyDown = false; if (e.key == e.key.toLowerCase()) { changeTool(prevTool); return; } } }); // }}} // 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(toolName, displayName, icon, key=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 (key) { const keyHint = document.createElement('span'); keyHint.className = 'key-hint'; keyHint.innerHTML = key; button.appendChild(keyHint); toolKeyBindings[key] = toolName; } toolBar.appendChild(button); toolButtons.push({'name': toolName, 'button': button}); return button; } createToolButton('brush', 'Brush', '', 'b'); createToolButton('content-move', 'Move Content', '', 'h'); createToolButton('move', 'Move Canvas', '', 'm'); createToolButton('zoom', 'Zoom', '', 'z'); createToolButton('resize', 'Resize', '', 'r'); createToolButton('color-picker', 'Color Picker', '', 'a'); createToolButton('color-mix', 'Color Mix', '', 'x'); createToolButton('brush-size', 'Brush Size', '', 'd'); createToolButton('bucket-fill', 'Bucket Fill', '', 'k'); // }}} // 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 exportCanvas(e) { const link = document.createElement('a'); link.download = 'canvas.png'; link.href = canvas.toDataURL(); link.click(); } function importCanvas(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); ctx.fillStyle = backgroundColor; ctx.fillRect(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, key=undefined) { const button = document.createElement('div'); button.className = 'button'; button.innerHTML = icon; button.title = name; if (clickFunction) { button.addEventListener('click', () => { clickFunction() updateInfos(); }); } menuBar.appendChild(button); if (key) { const keyHint = document.createElement('span'); keyHint.className = 'key-hint'; keyHint.innerHTML = key; button.appendChild(keyHint); functionKeyBindings[key] = clickFunction; } return button; } menuButtons.push(createMenuButton('', 'Save', saveCanvas, 's')); menuButtons.push(createMenuButton('', 'Load', loadCanvas)); menuButtons.push(createMenuButton('', 'Clear', clearCanvas)); menuButtons.push(createMenuButton('', 'Export', exportCanvas)); menuButtons.push(createMenuButton('', 'Import', importCanvas)); menuButtons.push(createMenuButton('', 'Flip Horizontally', flipCanvasHorizontally, 'f')); menuButtons.push(createMenuButton('', 'Flip Vertically', flipCanvasVertically, 'v')); menuButtons.push(createMenuButton('', 'Undo', undo, 'u')); menuButtons.push(createMenuButton('', 'Redo', redo, 'y')); menuButtons.push(createMenuButton('', 'Clear', clearCanvas, 'c')); menuButtons.push(createMenuButton('', 'Reset', resetZoom, 't')); menuButtons.push(createMenuButton('', 'Add Color', createPuck)); // }}} // pucks {{{ function createPuck(c, editable=true, key=undefined) { 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(); }); } if (key) { const keyHint = document.createElement('div'); keyHint.className = 'key-hint'; keyHint.innerHTML = key; puck.appendChild(keyHint); } function mixx(startTime) { var interval = setInterval(() => { 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); return interval; } puck.addEventListener('mousedown', (e) => { const startTime = Date.now(); var interval = mixx(startTime); function onMouseUp() { clearInterval(interval); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mouseup', onMouseUp); }); document.addEventListener('keydown', (e) => { if (e.key == key) { console.log(e.key); const startTime = Date.now(); var interval = mixx(startTime); function onKeyUp() { clearInterval(interval); document.removeEventListener('keyup', onKeyUp); } document.addEventListener('keyup', onKeyUp); } }); menuBar.appendChild(puck); } createPuck(c='rgb(0, 0, 0)', editable=false, key='1'); createPuck(c='rgb(255, 0, 0)', editale=false, key='2'); createPuck(c='rgb(0, 0, 255)', editale=false, key='3'); createPuck(c='rgb(255, 255, 0)', editale=false, key='4'); createPuck(c='rgb(99, 60, 22)', editale=false, key='5'); createPuck(c='rgb(0, 255, 0)', editale=false, key='6'); createPuck(c='rgb(255, 0, 255)', editale=false, key='7'); createPuck(c='rgb(0, 255, 255)', editale=false, key='8'); createPuck(c='rgb(255, 255, 255)', editale=false, key='9'); // }}} // 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()); } // }}} // start {{{ ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, canvas.width, canvas.height); updateInfos(); toolButtons[0]['button'].click(); // }}}