diff --git a/cool.js b/cool.js new file mode 100644 index 0000000..9ef76a2 --- /dev/null +++ b/cool.js @@ -0,0 +1,1045 @@ +const commandBarElement = document.getElementById('menu-bar'); +const toolBarElement = document.getElementById('tool-bar'); +const layerBarElement = document.getElementById('layer-bar'); +const studioElement = document.getElementById('studio'); +const infoBarElement = document.getElementById('info-bar'); +const easelElement = document.getElementById('easel'); +const brushPreviewElement = document.getElementById('brush-preview'); + +const dZoom = 0.001; +const dBrushSize = 0.5; +const initialWidth = 800; +const initialHeight = 600; +const maxBrushSize = 500; + +let brushColor = 'rgb(0, 0, 0)'; +let brushShape = 'square' +let brushSize = 1; +let zoom = 1; +let currentTool = 'brush' +let prevTool = 'brush' + +let startX = 0; +let startY = 0; +let endX = 0; +let endY = 0; +let dX = 0; +let dY = 0; +let canvasStartX = 0; +let canvasStartY = 0; +let canvasEndX = 0; +let canvasEndY = 0; +let canvasDX = 0; +let canvasDY = 0; + +let isKeyDown = false; +let isMouseDown = false; + +// HELPERS {{{ + +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 colorsMatch(a, b) { + return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; +} + +function makeButtonElement({icon, name, clickFunction, key}) { + if (!icon) throw new Error('No icon provided'); + if (!name) throw new Error('No name provided'); + if (!clickFunction) throw new Error('No click function provided'); + if (!key) throw new Error('No key provided'); + + const buttonElement = document.createElement('div'); + buttonElement.className = 'button'; + buttonElement.innerHTML = icon; + buttonElement.title = name; + buttonElement.addEventListener('click', clickFunction); + if (key) { + const keyHint = document.createElement('span'); + keyHint.className = 'key-hint'; + keyHint.innerHTML = key; + buttonElement.appendChild(keyHint); + } + return buttonElement; +} + +// }}} + +// LAYERS {{{ + +// factory {{{ + +function makeCanvas({height=600, width=800}) { // {{{ + const canvas = document.createElement('canvas'); + easelElement.appendChild(canvas); + canvas.style.imageRendering = 'pixelated'; + canvas.ctx = canvas.getContext('2d'); + + canvas.tempCanvas = document.createElement('canvas'); + canvas.tempCtx = canvas.tempCanvas.getContext('2d'); + + canvas.disableImageSmoothing = function(ctx) { + ctx.imageSmoothingEnabled = false; + if (ctx.imageSmoothingEnabled !== false) { + ctx.mozImageSmoothingEnabled = false; + ctx.webkitImageSmoothingEnabled = false; + ctx.msImageSmoothingEnabled = false; + } + }; + + canvas.saveTempCanvas = function() { + canvas.ctx.save(); + canvas.tempCanvas.width = canvas.width; + canvas.tempCanvas.height = canvas.height; + canvas.disableTempImageSmoothing(canvas.tempCtx); + canvas.tempCtx.drawImage(canvas, 0, 0); + } + + canvas.restoreTempCanvas = function() { + canvas.ctx.clearRect(0, 0, canvas.width, canvas.height); + canvas.ctx.drawImage(canvas.tempCanvas, 0, 0); + canvas.ctx.restore(); + } + + canvas.setHeight = function(height) { + canvas.height = height; + canvas.disableImageSmoothing(canvas.ctx); + }; + + canvas.setWidth = function(width) { + canvas.width = width; + canvas.disableImageSmoothing(canvas.ctx); + }; + + canvas.getPositionOnCanvas = function(e) { + const rect = canvas.getBoundingClientRect(); + return { + x: Math.round((e.clientX - rect.left) / zoom), + y: Math.round((e.clientY - rect.top) / zoom), + }; + } + + canvas.drawPixel = function(x, y, color) { + console.log({x, y, color}); + canvas.ctx.fillStyle = color; + canvas.ctx.fillRect(x, y, 1, 1); + } + + canvas.drawLineWithPixels = function(x1, y1, x2, y2, color) { + const dx = Math.abs(x2 - x1); + const dy = Math.abs(y2 - y1); + const sx = x1 < x2 ? 1 : -1; + const sy = y1 < y2 ? 1 : -1; + let err = dx - dy; + while (true) { + canvas.drawPixel(x1, y1, color); // Draw each pixel along the line + if (x1 === x2 && y1 === y2) break; + const e2 = err * 2; + if (e2 > -dy) { err -= dy; x1 += sx; } + if (e2 < dx) { err += dx; y1 += sy; } + } + } + + + canvas.drawShape = function(x, y, shape, size, color) { + if (size == 1) { + canvas.drawPixel(x, y, color); + return; + } + canvas.ctx.fillStyle = color; + if (shape === 'square') { + canvas.ctx.fillRect(x - Math.floor(size / 2), y - Math.floor(size / 2), size, size); + } + } + + canvas.getColorAtPixel = function(data, x, y) { + const index = (y * canvas.width + x) * 4; + return [data[index], data[index + 1], data[index + 2], data[index + 3]]; + } + + canvas.setColorAtPixel = function(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; + } + + + canvas.fill = function(color) { + canvas.ctx.fillStyle = color; + canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + canvas.floodFill = function(x, y, color) { + const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + const targetColor = canvas.getColorAtPixel(data, x, y); + const fillColorArray = hexToRgbArray(color); + + if (colorsMatch(targetColor, fillColorArray)) { + return; + } + + const stack = [{x, y}]; + + while (stack.length > 0) { + const {x, y} = stack.pop(); + const currentColor = canvas.getColorAtPixel(data, x, y); + + if (colorsMatch(currentColor, targetColor)) { + canvas.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}); + } + } + + canvas.ctx.putImageData(imageData, 0, 0); + } + + canvas.toDataUrl = function() { + const dataURL = canvas.toDataURL(); + const dimensions = `${canvas.width}x${canvas.height}`; + return {dataURL, dimensions}; + } + + canvas.fromDataUrl = function(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'; + canvas.ctx.drawImage(img, 0, 0); + } + } + + canvas.deleteCanvas = function() { + canvas.remove(); + } + + canvas.setWidth(width); + canvas.setHeight(height); + + return canvas; + +} // }}} + +function makeLayer({height=600, width=800}) { // {{{ + const layer = {} + layer.canvas = makeCanvas({height, width}); + layer.active = false; + layer.opacity = 1; + layer.controllerElement = document.createElement('div'); + layer.controllerElement.className = 'layer-controller'; + layer.controllerElement.addEventListener('click', () => { + layers.setActive(layer); + }); + + layer.activate = function() { + layer.active = true; + layer.controllerElement.classList.add('active'); + } + + layer.deactivate = function() { + layer.active = false; + layer.controllerElement.classList.remove('active'); + } + + return layer; +} // }}} + +function makeLayers({height=600, width=800}) { // {{{ + const layers = []; + layers.height = height; + layers.width = width; + + layers.setHeight = function(height) { + layers.height = height; + easelElement.style.height = height + 2 + 'px'; + } + + layers.setHeight(height); + + layers.setWidth = function(width) { + layers.width = width; + easelElement.style.width = width + 2 + 'px'; + } + + layers.setWidth(width); + + layers.resetPosition = function() { + const studioRect = studioElement.getBoundingClientRect(); + easelElement.style.left = `${studioRect.left}px`; + easelElement.style.top = `${studioRect.top}px`; + } + + layers.updateControllers = function() { + layerBarElement.innerHTML = ''; + layers.forEach(layer => { + layerBarElement.appendChild(layer.controllerElement); + }); + } + + layers.add = function() { + const layer = makeLayer({ + height: layers.height, + width: layers.width, + }); + layers.push(layer); + layer.activate(); + layers.updateControllers(); + } + + layers.delete = function(layer) { + layer.canvas.deleteCanvas(); + layers.splice(layers.indexOf(layer), 1); + layers.updateControllers(); + } + + layers.deleteAll = function() { + layers.forEach(layer => layer.deleteCanvas()); + // TODO + } + + layers.move = function(layer, index) { + layers.splice(layers.indexOf(layer), 1); + layers.splice(index, 0, layer); + } + + layers.setActive = function(layer) { + layers.forEach(layer => layer.deactivate()); + layer.activate(); + } + + layers.getActive = function() { + return layers.find(layer => layer.active); + } + + return layers; +} // }}} + +// }}} + +const layers = makeLayers({height: initialHeight, width: initialWidth}); +layers.add(); +layers.add(); +layers[0].canvas.fill('rgb(255, 255, 255)'); +layers.setActive(layers[1]); + +// }}} + +// COLOR PREVIEW {{{ + +function makeColorPreview() { + const colorPreview = {} + colorPreview.element = document.createElement('div'); + colorPreview.element.id = 'color-preview'; + colorPreview.element.className = 'puck'; + colorPreview.element.style.backgroundColor = brushColor; + commandBarElement.appendChild(colorPreview.element); + colorPreview.update = function() { + colorPreview.element.style.backgroundColor = brushColor; + } +} + +const colorPreview = makeColorPreview(); + +// }}} + +// COMMANDS {{{ + +// factory {{{ + +function makeCommand({name, key, icon, clickFunction}) { + if (!name) throw new Error('No name provided'); + if (!icon) throw new Error('No icon provided'); + if (!clickFunction) throw new Error('No click function provided'); + if (!key) throw new Error('No key provided'); + + const command = {}; + command.name = name; + command.key = key; + command.buttonElement = makeButtonElement({icon, name, clickFunction, key}); + commandBarElement.appendChild(command.buttonElement); + + return command +} + +function makeCommands() { + const commands = []; + + commands.add = function({name, key, icon, clickFunction}) { + const command = makeCommand({name, key, icon, clickFunction}); + commands.push(command); + } + + return commands; +} + + +// }}} + +const commands = makeCommands(); + +commands.add({ // flip-horizontally {{{ + name: 'flip-horizontally', + key: 'f', + icon: '', + clickFunction: function flipCanvasHorizontally() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + ctx.save(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.scale(-1, 1); + ctx.translate(-canvas.width, 0); + canvas.restoreTempCanvas(); + ctx.restore(); + } +}); // }}} + +commands.add({ // flip-vertically {{{ + name: 'flip-vertically', + key: 'v', + icon: '', + clickFunction: function flipCanvasVertically() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + ctx.save(); + canvas.saveTempCanvas(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.scale(1, -1); + ctx.translate(0, -canvas.height); + canvas.restoreTempCanvas(); + ctx.restore(); + } +}); // }}} + +commands.add({ // export {{{ + name: 'export', + key: 'e', + icon: '', + clickFunction: function exportCanvas() { + const canvas = layers.getActive().canvas; + const link = document.createElement('a'); + link.download = 'canvas.png'; + link.href = canvas.toDataURL(); + link.click(); + } +}); // }}} + +commands.add({ // import {{{ + name: 'import', + key: 'i', + icon: '', + clickFunction: function importCanvas() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + 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(); + } +}); // }}} + +commands.add({ // clear {{{ + name: 'clear', + key: 'c', + icon: '', + clickFunction: function clearCanvas() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + canvas.saveTempCanvas(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } +}); // }}} + +commands.add({ // reset {{{ + name: 'reset', + key: 't', + icon: '', + clickFunction: function resetZoom() { + layers.resetPosition(); + // zoom = 1; + // canvas.style.width = canvas.width * zoom + 'px'; + // canvas.style.height = canvas.height * zoom + 'px'; + // TODO + + } +}); // }}} + +// }}} + +// TOOLS {{{ + +// factory {{{ + +function makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}) { + if (!name) throw new Error('No name provided'); + if (!key) throw new Error('No key provided'); + if (!icon) throw new Error('No icon provided'); + + const tool = {}; + tool.name = name; + tool.key = key; + tool.icon = icon; + tool.mouseDown = mouseDown; + tool.mouseMove = mouseMove; + tool.mouseUp = mouseUp; + tool.active = false; + + tool.buttonElement = makeButtonElement({ + icon: tool.icon, + name: tool.name, + key: tool.key, + clickFunction: function() { + tools.activate(tool); + tool.buttonElement.classList.add('active'); + } + }); + toolBarElement.appendChild(tool.buttonElement); + + tool.activate = function() { + currentTool = tool.name; + tool.active = true; + } + + tool.deactivate = function() { + tool.active = false; + tool.buttonElement.classList.remove('active'); + } + + + return tool; +} + +function makeTools() { + const tools = []; + + tools.add = function({name, key, icon, mouseDown, mouseMove, mouseUp}) { + const tool = makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}); + tools.push(tool); + } + + tools.activate = function(tool) { + tools.forEach(tool => tool.deactivate()); + tool.activate(); + } + + return tools; +} + +// }}} + +const tools = makeTools(); + +tools.add({ // brush {{{ + name: 'brush', + key: 'b', + icon: '', + mouseDown: function(e) { + const canvas = layers.getActive().canvas; + if (brushSize == 1) { + canvas.drawPixel(canvasStartX, canvasStartY, brushColor); + } else { + canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, brushColor); + } + }, + mouseMove: function(e) { + const canvas = layers.getActive().canvas; + if (brushSize == 1) { + canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushColor); + return; + } else { + canvas.drawShape(canvasEndX, canvasEndY, brushShape, brushSize, brushColor); + } + canvasStartX = canvasEndX; + canvasStartY = canvasEndY; + }, +}); // }}} + +tools.add({ // content-move {{{ + name: 'content-move', + key: 'h', + icon: '', + mouseMove: function(e) { + const canvas = layers.getActive().canvas; + canvas.ctx.clearRect(0, 0, canvas.width, canvas.height); + canvas.ctx.fillStyle = 'white'; + canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); + canvas.restoreTempCanvas(); + }, +}); // }}} + +tools.add({ // move {{{ + name: 'move', + key: 'm', + icon: '', + mouseDown: function(e) { + startX = e.clientX - easelElement.offsetLeft; + startY = e.clientY - easelElement.offsetTop; + }, + mouseMove: function(e) { + easelElement.style.left = dX + 'px'; + easelElement.style.top = dY + 'px'; + }, +}); // }}} + +tools.add({ // zoom {{{ + name: 'zoom', + key: 'z', + icon: '', + mouseMove: function(e) { + // TODO all canvases + // const canvas = layers.getActive().canvas; + zoom += dX * dZoom; + if (zoom < 0.1) zoom = 0.1; + // canvas.style.height = canvasHeight * zoom + 'px'; + // canvas.style.width = canvasWidth * zoom + 'px'; + startX = endX; + } +}); // }}} + +tools.add({ // bucket-fill {{{ + name: 'bucket-fill', + key: 'k', + icon: '', + mouseDown: function(e) { + // canvas = layers.getActive().canvas; + // canvas.floodFill(canvasStartX, canvasStartY, brushColor); + } +}); // }}} + +tools.add({ // color-picker {{{ + name: 'color-picker', + key: 'a', + icon: '', + mouseDown: function(e) { + const canvas = layers.getActive().canvas; + const imageData = canvas.ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data; + const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; + brushColor = pickedColor; + colorPreview.update(); + } +}); // }}} + +tools.add({ // brush-size {{{ + name: 'brush-size', + key: 'd', + icon: '', + mouseMove: function(e) { + brushSize += dX * dBrushSize; + if (brushSize < 1) brushSize = 1; + if (brushSize > maxBrushSize) brushSize = maxBrushSize; + startX = endX; + } +}); // }}} + +tools.add({ // resize {{{ + name: 'resize', + key: 'r', + icon: '', + mouseMove: function(e) { + // const canvas = layers.getActive().canvas; + // let newWidth = canvasWidth + dX / zoom; + // let newHeight = canvasHeight + dY / zoom; + // if (newWidth > 0 && newHeight > 0) { + // canvas.setWidth(newWidth); + // canvas.setHeight(newHeight); + // canvas.style.width = newWidth * zoom + 'px'; + // canvas.style.height = newHeight * zoom + 'px'; + // canvas.ctx.clearRect(0, 0, canvas.width, canvas.height); + // canvas.ctx.fillStyle = backgroundColor; + // canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); + // canvas.ctx.drawImage(tempCanvas, 0, 0); + // } + } +}); // }}} + +tools.add({ // color-mix {{{ + name: 'color-mix', + key: 'x', + icon: '', + mouseMove: function(e) { + // const canvas = layers.getActive().canvas; + 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(brushColor, canvasColor, t); + + brushColor = mixedColor; + + startX = e.clientX; + startY = e.clientY; + } +}); // }}} + +// }}} + +// PUCKS {{{ + +// factory {{{ + +function createPuck({puckColor, key, editable=true}) { + if (!puckColor) throw new Error('No puck color provided'); + + const puck = {} + puck.element = document.createElement('div'); + puck.element.style.backgroundColor = puckColor; + puck.element.className = 'puck'; + + if (editable) { + const deleteHandle = document.createElement('div'); + deleteHandle.className = 'delete-handle'; + deleteHandle.innerHTML = ''; + puck.element.appendChild(deleteHandle); + deleteHandle.addEventListener('click', () => { + puck.element.remove(); + }); + } + + if (key) { + const keyHint = document.createElement('div'); + keyHint.className = 'key-hint'; + keyHint.innerHTML = key; + puck.element.appendChild(keyHint); + } + + function mixx(startTime) { + var interval = setInterval(() => { + const elapsedTime = Date.now() - startTime; + const t = Math.min(1, elapsedTime / 10000); + const mixedColor = mixbox.lerp(brushColor, puck.style.backgroundColor, t); + brushColor = mixedColor; + colorPreview.update(); + infos.update(); + }, 50); + return interval; + } + + puck.element.addEventListener('mousedown', () => { + const startTime = Date.now(); + var interval = mixx(startTime); + function onMouseUp() { + clearInterval(interval); + document.removeEventListener('mouseup', onMouseUp); + } + document.addEventListener('mouseup', onMouseUp); + }); + + puck.keydown = function(e) { + if (e.key == key) { + const startTime = Date.now(); + var interval = mixx(startTime); + function onKeyUp() { + clearInterval(interval); + document.removeEventListener('keyup', onKeyUp); + } + document.addEventListener('keyup', onKeyUp); + } + } + + commandBarElement.appendChild(puck.element); +} + +function makePucks() { + const pucks = []; + + pucks.add = function({puckColor, key, editable}) { + const puck = createPuck({puckColor, key, editable}); + pucks.push(puck); + } + + return pucks; +} + +// }}} + +const pucks = makePucks(); + +pucks.add({ + puckColor: 'rgb(0, 0, 0)', + key: '1', + editable: false, +}); + +pucks.add({ + puckColor: 'rgb(255, 255, 255)', + key: '2', + editable: false, +}); + +pucks.add({ + puckColor: 'rgb(255, 0, 0)', + key: '3', + editable: false, +}); + +pucks.add({ + puckColor: 'rgb(0, 255, 0)', + key: '4', + editable: false, +}); + +pucks.add({ + puckColor: 'rgb(0, 0, 255)', + key: '5', + editable: false, +}); + +// }}} + +// INFO {{{ + +function makeInfo({name, updateFunction}) { + if (!name) throw new Error('No name provided'); + if (!updateFunction) throw new Error('No update function provided'); + + const info = {}; + info.name = name; + info.updateFunction = updateFunction; + + info.element = document.createElement('span'); + info.element.className = 'info'; + + const key = document.createElement('span'); + key.className = 'key'; + key.innerHTML = info.name + ':'; + + const value = document.createElement('span'); + value.className = 'value'; + value.innerHTML = '0'; + + info.element.appendChild(key); + info.element.appendChild(value); + + infoBarElement.appendChild(info.element); + + info.update = function() { + let v = updateFunction(); + if (v === undefined) v = '?'; + value.innerHTML = v; + } + + return info; +} + +function makeInfos() { + const infos = [] + infos.add = function({name, updateFunction}) { + const info = makeInfo({name, updateFunction}); + infos.push(info); + } + infos.update = function() { + infos.forEach(function(info){ + info.update(); + }); + } + return infos; +} + +const infos = makeInfos(); + +infos.add({ + name: 'zoom', + updateFunction: function() { + var percent = zoom * 100; + return percent.toFixed(0) + '%'; + } +}); + +infos.add({ + name: 'brush', + updateFunction: function() { + return brushSize; + } +}); + + +infos.add({ + name: 'x', + updateFunction: function() { + return canvasEndX; + } +}); + + +infos.add({ + name: 'y', + updateFunction: function() { + return canvasEndY; + } +}); + +infos.add({ + name: 'color', + updateFunction: function() { + return brushColor; + } +}); + +infos.add({ + name: 'width', + updateFunction: function() { + return "width"; + } +}); + +infos.add({ + name: 'height', + updateFunction: function() { + return "height"; + } +}); + +// }}} + +// MOUSE EVENT LISTENERS {{{ + +studioElement.addEventListener('mousedown', (e) => { + const canvas = layers.getActive().canvas; + isMouseDown = true; + startX = e.clientX; + startY = e.clientY; + canvasStartX = canvas.getPositionOnCanvas(e).x; + canvasStartY = canvas.getPositionOnCanvas(e).y; + + for (var i = 0; i < tools.length; i++) { + var tool = tools[i]; + if (tool.name === currentTool) { + if (tool.mouseDown) { + tool.mouseDown(e); + break; + } + } + } + + infos.update(); + +}); + +studioElement.addEventListener('mousemove', (e) => { + const canvas = layers.getActive().canvas; + endX = e.clientX; + endY = e.clientY; + dX = endX - startX; + dY = endY - startY; + canvasEndX = canvas.getPositionOnCanvas(e).x; + canvasEndY = canvas.getPositionOnCanvas(e).y; + canvasDX = canvasEndX - canvasStartX; + canvasDY = canvasEndY - canvasStartY; + + if (currentTool == 'brush-size') { + brushPreviewElement.style.display = 'block'; + brushPreviewElement.style.width = brushSize + 'px'; + brushPreviewElement.style.height = brushSize + 'px'; + brushPreviewElement.style.left = e.clientX - brushSize / 2 + 'px'; + brushPreviewElement.style.top = e.clientY - brushSize / 2 + 'px'; + } + + if (isMouseDown) { + for (var i = 0; i < tools.length; i++) { + var tool = tools[i]; + if (tool.name === currentTool) { + if (tool.mouseMove) { + tool.mouseMove(e); + break; + } + } + } + } + + infos.update(); + +}); + +studioElement.addEventListener('mouseup', () => { + isMouseDown = false; + infos.update(); +}); + +studioElement.addEventListener('mouseleave', () => { + isMouseDown = false; + brushPreviewElement.style.display = 'none'; + infos.update(); +}); + +// }}} + +// KEYBINDINGS {{{ + +document.addEventListener('keydown', (e) => { + if (isKeyDown) return; + + tools.forEach(tool => { + if (tool.key.toLowerCase() === e.key.toLowerCase()) { + prevTool = currentTool; + currentTool = tool.name; + } + }); + + commands.forEach(command => { + if (command.key.toLowerCase() === e.key.toLowerCase()) { + command.clickFunction(); + } + }); + + pucks.filter(puck => puck.key).forEach(puck => { + if (puck.key.toLowerCase() === e.key.toLowerCase()) { + puck.keydown(e); + } + }); + + isKeyDown = true; + +}); + +document.addEventListener('keyup', (e) => { + tools.forEach(tool => { + if (tool.key.toLowerCase() === e.key) { + currentTool = prevTool; + } + }); + + isKeyDown = false; + +}); + +// }}} + +layers.resetPosition(); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..565ae09 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,9 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, +]; \ No newline at end of file diff --git a/history.js b/history.js new file mode 100644 index 0000000..7f7ddb4 --- /dev/null +++ b/history.js @@ -0,0 +1,77 @@ +function clearFromLocalStorage() { + localStorage.removeItem(LOCAL_STORAGE_CANVAS_NAME); + localStorage.removeItem(LOCAL_STORAGE_DIMENSIONS_NAME); +} + +// }}} + +// history {{{ + +function saveState(undoStack, redoStack, maxHistory) { + if (undoStack.length >= maxHistory) { + undoStack.shift(); + } + undoStack.push({ + imageData: canvas.toDataURL(), + width: canvas.width, + height: canvas.height + }); + redoStack = []; +} + +function undo(canvas, ctx, undoStack, redoStack) { + 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); + }; + } +} + +// }}} + diff --git a/index.html b/index.html index 6831226..2642c6f 100644 --- a/index.html +++ b/index.html @@ -12,12 +12,13 @@
-
-
- +
+
-
+
+
+
diff --git a/package-lock.json b/package-lock.json index 74887f4..627b9d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { - "name": "mix2", + "name": "mixx", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "mix2", + "name": "mixx", "version": "1.0.0", - "license": "ISC", "devDependencies": { + "@eslint/js": "^9.9.0", "electron": "^31.3.1", - "nodeman": "^1.1.2", + "eslint": "^9.9.0", + "globals": "^15.9.0", "nodemon": "^3.1.4" } }, @@ -35,6 +36,170 @@ "global-agent": "^3.0.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -114,6 +279,67 @@ "@types/node": "*" } }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -127,6 +353,12 @@ "node": ">= 8" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -210,6 +442,52 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -246,21 +524,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=0.1.90" + "node": ">=7.0.0" } }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -305,6 +606,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -428,7 +735,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "optional": true, "engines": { "node": ">=10" }, @@ -436,6 +742,164 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -456,6 +920,33 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -465,6 +956,18 @@ "pend": "~1.2.0" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -477,6 +980,41 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -593,6 +1131,18 @@ "node": ">=10" } }, + "node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -734,12 +1284,46 @@ "node": ">=10.19.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -782,12 +1366,51 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -813,6 +1436,40 @@ "json-buffer": "3.0.1" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -862,21 +1519,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/nodeman": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/nodeman/-/nodeman-1.1.2.tgz", - "integrity": "sha512-RpRpDnMI4TN/EPX6+my7f02585I14AQO8YrHTkO1aTiC65+sMa5G3NwQplN6kwue1njxWiaSz2UwCnDbwkLgeQ==", - "dev": true, - "dependencies": { - "colors": "*" - }, - "bin": { - "_nodeman": "bin/_nodeman", - "nodeman": "bin/nodeman" - }, - "engines": { - "node": "*" - } + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/nodemon": { "version": "3.1.4", @@ -958,6 +1605,23 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -967,6 +1631,66 @@ "node": ">=8" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -985,6 +1709,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -1010,6 +1743,35 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -1040,6 +1802,15 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -1052,6 +1823,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -1070,6 +1851,29 @@ "node": ">=8.0" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1102,6 +1906,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1133,6 +1958,30 @@ "dev": true, "optional": true }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -1157,6 +2006,12 @@ "node": ">=4" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1178,6 +2033,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -1212,6 +2079,39 @@ "node": ">= 4.0.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1227,6 +2127,18 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 79e57d4..bf5e490 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "start": "nodemon --exec electron ." }, "devDependencies": { + "@eslint/js": "^9.9.0", "electron": "^31.3.1", + "eslint": "^9.9.0", + "globals": "^15.9.0", "nodemon": "^3.1.4" } } diff --git a/render.js b/render.js index aadbb71..87ba792 100644 --- a/render.js +++ b/render.js @@ -1,758 +1,1222 @@ -// 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; +// CONSTANTS {{{ -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; +const commandBarElement = document.getElementById('menu-bar'); +const toolBarElement = document.getElementById('tool-bar'); +const layerControllersElement = document.getElementById('layer-controllers'); +const studioElement = document.getElementById('studio'); +const infoBarElement = document.getElementById('info-bar'); +const easelElement = document.getElementById('easel'); +const brushPreviewElement = document.getElementById('brush-preview'); -let isMouseDown = false; +const dZoom = 0.001; +const dBrushSize = 0.5; +const initialWidth = 800; +const initialHeight = 600; +const maxBrushSize = 500; +const tolerance = 1; +const shapes = ['circle', 'square']; -const colorPreview = document.createElement('div'); -colorPreview.id = 'color-preview'; -colorPreview.className = 'puck'; -colorPreview.style.backgroundColor = color; +// }}} -menuBar.appendChild(colorPreview); +// VARS {{{ +let brushColor = 'rgb(0, 0, 0)'; +let brushShape = 'circle' +let brushSize = 15; +let zoom = 1; +let currentTool; +let prevTool = 'brush'; + +let startX = 0; +let startY = 0; +let endX = 0; +let endY = 0; +let dX = 0; +let dY = 0; +let canvasStartX = 0; +let canvasStartY = 0; +let canvasEndX = 0; +let canvasEndY = 0; +let canvasDX = 0; +let canvasDY = 0; + +let isKeyDown = false; +let isMouseDown = false; // }}} -// helpers {{{ - -function saveState() { - if (undoStack.length >= maxHistory) { - undoStack.shift(); // Remove the oldest state if the stack exceeds the limit - } +// HELPERS {{{ - // 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 disableImageSmoothing(ctx) { + ctx.imageSmoothingEnabled = false; + if (ctx.imageSmoothingEnabled !== false) { + ctx.mozImageSmoothingEnabled = false; + ctx.webkitImageSmoothingEnabled = false; + ctx.msImageSmoothingEnabled = false; + } +}; -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 hexToRgbArray(hex) { + if (hex.startsWith('#')) { + hex = hex.slice(1); } -} -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); - }; + 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 getPositionOnCanvas(e) { - const rect = canvas.getBoundingClientRect(); - return { - x: Math.round((e.clientX - rect.left) / zoom), - y: Math.round((e.clientY - rect.top) / zoom), - }; +function colorsMatch(color1, color2, tolerance = 0) { + return Math.abs(color1[0] - color2[0]) <= tolerance && + Math.abs(color1[1] - color2[1]) <= tolerance && + Math.abs(color1[2] - color2[2]) <= tolerance; } -function drawCircle(x, y) { - ctx.beginPath(); - ctx.arc(x, y, brushSize / 2, 0, 2 * Math.PI, false); - ctx.fillStyle = color; - ctx.fill(); +function makeIconElement(htmlString) { + const parentElement = document.createElement('div'); + parentElement.innerHTML = htmlString; + const iconElement = parentElement.firstChild; + return iconElement; } -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)); +function makeButtonElement({icon, name, func, key}) { + if (!icon) throw new Error('No icon provided'); + if (!name) throw new Error('No name provided'); + if (!func) throw new Error('No click function provided'); + if (!key) throw new Error('No key provided'); + + const button = {}; + button.name = name; + button.key = key; + button.icon = icon; + button.element = document.createElement('div'); + button.element.className = 'button'; + + button.element.addEventListener('click', func); + + button.refresh = function() { + button.element.innerHTML = ''; + const iconElement = makeIconElement(button.icon); + button.element.appendChild(iconElement); + button.element.title = button.name; + if (button.key) { + const keyHint = document.createElement('span'); + keyHint.className = 'key-hint'; + keyHint.innerHTML = key; + button.element.appendChild(keyHint); + } + } - for (let i = 0; i <= steps; i++) { - const x = x1 + (dx * i) / steps; - const y = y1 + (dy * i) / steps; - drawCircle(x, y); - } -} + button.refresh(); -function saveCanvasContents() { - tempCanvas = document.createElement('canvas'); - tempCanvas.width = canvas.width; - tempCanvas.height = canvas.height; - const tempCtx = tempCanvas.getContext('2d'); - tempCtx.drawImage(canvas, 0, 0); + return button; } -function updateColorPreview() { - colorPreview.style.backgroundColor = color; -} +// }}} -function hexToRgbArray(hex) { - if (hex.startsWith('#')) { - hex = hex.slice(1); - } +// LAYERS {{{ - if (hex.length === 3) { - hex = hex.split('').map(char => char + char).join(''); - } +// FACTORY {{{ - const bigint = parseInt(hex, 16); - return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; -} +function makeCanvas({height=600, width=800}) { // {{{ + const canvas = document.createElement('canvas'); + canvas.style.imageRendering = 'pixelated'; + canvas.ctx = canvas.getContext('2d'); -function floodFill(x, y, fillColor) { - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; + canvas.tempCanvas = document.createElement('canvas'); + canvas.tempCtx = canvas.tempCanvas.getContext('2d'); - const targetColor = getColorAtPixel(data, x, y); - const fillColorArray = hexToRgbArray(fillColor); + canvas.saveCanvas = function() { + canvas.ctx.save(); + canvas.tempCanvas.width = canvas.width; + canvas.tempCanvas.height = canvas.height; + canvas.tempCtx.clearRect(0, 0, canvas.width, canvas.height); + disableImageSmoothing(canvas.tempCtx); + canvas.tempCtx.drawImage(canvas, 0, 0); + } - if (colorsMatch(targetColor, fillColorArray)) { - return; // The clicked point is already the fill color - } + canvas.clearCanvas = function() { + canvas.ctx.clearRect(0, 0, canvas.width, canvas.height); + } - const stack = [{x, y}]; + canvas.restoreCanvas = function(x=0, y=0) { + canvas.ctx.drawImage(canvas.tempCanvas, x, y); + } - while (stack.length > 0) { - const {x, y} = stack.pop(); - const currentColor = getColorAtPixel(data, x, y); + canvas.setHeight = function(height) { + canvas.height = height; + disableImageSmoothing(canvas.ctx); + }; - if (colorsMatch(currentColor, targetColor)) { - setColorAtPixel(data, x, y, fillColorArray); + canvas.setWidth = function(width) { + canvas.width = width; + disableImageSmoothing(canvas.ctx); + }; - 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}); - } - } + canvas.resize = function(width, height) { + canvas.width = width; + canvas.height = height; + disableImageSmoothing(canvas.ctx); + } - ctx.putImageData(imageData, 0, 0); -} + canvas.getPositionOnCanvas = function(e) { + const rect = canvas.getBoundingClientRect(); + return { + x: Math.round((e.clientX - rect.left) / zoom), + y: Math.round((e.clientY - rect.top) / zoom), + }; + } -function getColorAtPixel(data, x, y) { - const index = (y * canvas.width + x) * 4; - return [data[index], data[index + 1], data[index + 2], data[index + 3]]; -} + canvas.drawPixel = function(x, y, color) { + canvas.ctx.fillStyle = color; + canvas.ctx.fillRect(x, y, 1, 1); + } -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 -} + canvas.drawLineWithPixels = function(x1, y1, x2, y2, color) { + const dx = Math.abs(x2 - x1); + const dy = Math.abs(y2 - y1); + const sx = x1 < x2 ? 1 : -1; + const sy = y1 < y2 ? 1 : -1; + let err = dx - dy; + while (true) { + canvas.drawPixel(x1, y1, color); // Draw each pixel along the line + if (x1 === x2 && y1 === y2) break; + const e2 = err * 2; + if (e2 > -dy) { err -= dy; x1 += sx; } + if (e2 < dx) { err += dx; y1 += sy; } + } + } -function colorsMatch(a, b) { - return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; -} + canvas.drawShape = function(x, y, shape, size, color) { + x = Math.round(x); + y = Math.round(y); -// }}} + if (size === 1) { + canvas.drawPixel(x, y, color); + return; + } + canvas.ctx.fillStyle = color; + + if (shape === 'square') { + canvas.ctx.fillRect(x - Math.floor(size / 2), y - Math.floor(size / 2), size, size); + } else if (shape === 'circle') { + let radius = Math.floor(size / 2); + let radiusSquared = radius * radius; + + for (let y1 = -radius; y1 <= radius; y1++) { + for (let x1 = -radius; x1 <= radius; x1++) { + // Adjust the condition to avoid the outcrop + if ((x1 * x1 + y1 * y1) <= radiusSquared - radius) { + canvas.ctx.fillRect(x + x1, y + y1, 1, 1); + } + } + } + } else if (shape === 'empty-circle') { + let radius = Math.floor(size / 2); + let x1 = radius; + let y1 = 0; + let radiusError = 1 - x1; + + while (x1 >= y1) { + // Draw the 8 octants of the circle + canvas.ctx.fillRect(x + x1, y + y1, 1, 1); + canvas.ctx.fillRect(x + y1, y + x1, 1, 1); + canvas.ctx.fillRect(x - y1, y + x1, 1, 1); + canvas.ctx.fillRect(x - x1, y + y1, 1, 1); + canvas.ctx.fillRect(x - x1, y - y1, 1, 1); + canvas.ctx.fillRect(x - y1, y - x1, 1, 1); + canvas.ctx.fillRect(x + y1, y - x1, 1, 1); + canvas.ctx.fillRect(x + x1, y - y1, 1, 1); + + y1++; + if (radiusError < 0) { + radiusError += 2 * y1 + 1; + } else { + x1--; + radiusError += 2 * (y1 - x1 + 1); + } + } + } + } -// mousedown {{{ + canvas.drawLineWithShape = function(x1, y1, x2, y2, shape, size, color) { + const dx = x2 - x1; + const dy = y2 - y1; + const distance = Math.sqrt(dx * dx + dy * dy); + const steps = Math.ceil(distance / (size / 2)); -canvasArea.addEventListener('mousedown', (e) => { - if (e.target.closest('.puck')) return; + for (let i = 0; i <= steps; i++) { + const x = Math.round(x1 + (dx * i) / steps); + const y = Math.round(y1 + (dy * i) / steps); + canvas.drawShape(x, y, shape, size, color); + } + } - startX = e.clientX; - startY = e.clientY; - canvasStartX = getPositionOnCanvas(e).x; - canvasStartY = getPositionOnCanvas(e).y; - saveCanvasContents(); - isMouseDown = true; + canvas.fill = function(color) { + canvas.ctx.fillStyle = color; + canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); + } - 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; + canvas.getColorAtPixel = function(data, x, y) { + const index = (y * canvas.width + x) * 4; + return [data[index], data[index + 1], data[index + 2], data[index + 3]]; } -}); -// }}} + canvas.setColorAtPixel = function(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; + } -// mousemove {{{ + canvas.floodFill = function(x, y, color) { + console.log('flood fill'); + const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; -canvasArea.addEventListener('mousemove', (e) => { + const targetColor = canvas.getColorAtPixel(data, x, y); + const fillColorArray = hexToRgbArray(color); - endX = e.clientX; - endY = e.clientY; - dX = endX - startX; - dY = endY - startY; + if (colorsMatch(targetColor, fillColorArray, tolerance)) { + return; + } - canvasEndX = getPositionOnCanvas(e).x; - canvasEndY = getPositionOnCanvas(e).y; - canvasDX = canvasEndX - canvasStartX; - canvasDY = canvasEndY - canvasStartY; + const stack = [{x, y}]; - 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'; - } + while (stack.length > 0) { + const {x, y} = stack.pop(); + const currentColor = canvas.getColorAtPixel(data, x, y); - if (isMouseDown) { + if (colorsMatch(currentColor, targetColor, tolerance)) { + canvas.setColorAtPixel(data, x, y, fillColorArray); - 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); + 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}); } - } 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); + canvas.ctx.putImageData(imageData, 0, 0); + } - const mixedColor = mixbox.lerp(color, canvasColor, t); + canvas.toDataUrl = function() { + const dataURL = canvas.toDataURL(); + const dimensions = `${canvas.width}x${canvas.height}`; + return {dataURL, dimensions}; + } - color = mixedColor; + canvas.fromDataUrl = function(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'; + canvas.ctx.drawImage(img, 0, 0); + } + } - startX = e.clientX; - startY = e.clientY; + canvas.deleteCanvas = function() { + canvas.remove(); + } - } + canvas.setWidth(width); + canvas.setHeight(height); - } - - updateInfos(); - updateColorPreview(); -}); + return canvas; -// }}} +} // }}} -// mouseup {{{ +function makeLayer({height=600, width=800}) { // {{{ + const layer = {} + layer.canvas = makeCanvas({height, width}); + layer.active = false; + layer.opacity = 1; -canvasArea.addEventListener('mouseup', (e) => { - isMouseDown = false; - if (tool === 'brush') { - ctx.closePath(); - } else if (tool === 'resize') { - canvasWidth = canvas.width; - canvasHeight = canvas.height; + layer.controllerElement = document.createElement('div'); + layer.controllerElement.className = 'layer-controller'; + layer.controllerElement.innerHTML = ''; + + const moveUpHandle = document.createElement('div'); + moveUpHandle.classList.add('handle'); + moveUpHandle.classList.add('top-right'); + moveUpHandle.innerHTML = ''; + moveUpHandle.addEventListener('click', () => { + const index = layers.indexOf(layer); + if (index > 0) { + layers.move(layer, index - 1); + } + }); + layer.controllerElement.appendChild(moveUpHandle); + + const moveDownHandle = document.createElement('div'); + moveDownHandle.classList.add('handle'); + moveDownHandle.classList.add('bottom-right'); + moveDownHandle.innerHTML = ''; + moveDownHandle.addEventListener('click', () => { + const index = layers.indexOf(layer); + if (index < layers.length - 1) { + layers.move(layer, index + 1); + } + }); + layer.controllerElement.appendChild(moveDownHandle); + + layer.controllerElement.addEventListener('click', () => { + layers.setActive(layer); + }); + + layer.activate = function() { + layer.active = true; + layer.controllerElement.classList.add('active'); } - updateColorPreview(); -}); + layer.deactivate = function() { + layer.active = false; + layer.controllerElement.classList.remove('active'); + } -// }}} + return layer; +} // }}} -// mouseleave {{{ +function makeLayers({height=600, width=800}) { // {{{ + const layers = []; + layers.height = height; + layers.width = width; -canvasArea.addEventListener('mouseleave', (e) => { - isMouseDown = false; - brushPreview.style.display = 'none'; -}); + layers.addButton = document.createElement('div'); + layers.addButton.className = 'layer-add-button'; + layers.addButton.innerHTML = ''; + layers.addButton.addEventListener('click', () => { + layers.add(); + }); -// }}} + layers.setHeight = function(height) { + layers.height = height; + easelElement.style.height = height + 2 + 'px'; + } -// tools {{{ + layers.setHeight(height); -var toolButtons = []; + layers.setWidth = function(width) { + layers.width = width; + easelElement.style.width = width + 2 + 'px'; + } -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(); -} + layers.setWidth(width); -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); + layers.resetPosition = function() { + const studioRect = studioElement.getBoundingClientRect(); + easelElement.style.left = `${studioRect.left}px`; + easelElement.style.top = `${studioRect.top}px`; } - if (temporaryKey) { - const temporaryKeyHint = document.createElement('span'); - temporaryKeyHint.className = 'temporary-key-hint'; - temporaryKeyHint.innerHTML = temporaryKey; - button.appendChild(temporaryKeyHint); + + layers.refreshControllers = function() { + layerControllersElement.innerHTML = ''; + layers.forEach(layer => { + layerControllersElement.appendChild(layer.controllerElement); + }); + layerControllersElement.appendChild(layers.addButton); } - toolBar.appendChild(button); - return button; -} + layers.refreshLayers = function() { + easelElement.innerHTML = ''; + layers.forEach(layer => { + easelElement.appendChild(layer.canvas); + }); + } + + layers.refresh = function() { + layers.refreshControllers(); + layers.refreshLayers(); + } -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)}); + layers.add = function() { + const layer = makeLayer({ + height: layers.height, + width: layers.width, + }); + layers.push(layer); + layer.activate(); + layers.refresh(); + } + + layers.delete = function(layer) { + layer.canvas.deleteCanvas(); + layers.splice(layers.indexOf(layer), 1); + layers.refresh(); + } + + layers.deleteAll = function() { + layers.forEach(layer => layer.deleteCanvas()); + // TODO + } + + layers.move = function(layer, index) { + layers.splice(layers.indexOf(layer), 1); + layers.splice(index, 0, layer); + } + + layers.setActive = function(layer) { + layers.forEach(layer => layer.deactivate()); + layer.activate(); + } + + layers.getActive = function() { + return layers.find(layer => layer.active); + } + + return layers; +} // }}} // }}} -// 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(); +const layers = makeLayers({height: initialHeight, width: initialWidth}); +layers.add(); +layers.add(); +layers[0].canvas.fill('rgb(255, 255, 255)'); +layers.setActive(layers[1]); + +// }}} + +// COLOR PREVIEW {{{ + +function makeColorPreview() { + const colorPreview = {} + colorPreview.element = document.createElement('div'); + colorPreview.element.id = 'color-preview'; + colorPreview.element.className = 'puck'; + colorPreview.element.style.backgroundColor = brushColor; + commandBarElement.appendChild(colorPreview.element); + colorPreview.update = function() { + colorPreview.element.style.backgroundColor = brushColor; + } } -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(); +const colorPreview = makeColorPreview(); + +// }}} + +// COMMANDS {{{ + +// FACTORY {{{ + +function makeCommand({name, key, icon, func}) { + if (!name) throw new Error('No name provided'); + if (!icon) throw new Error('No icon provided'); + if (!func) throw new Error('No click function provided'); + if (!key) throw new Error('No key provided'); + + const command = {}; + command.name = name; + command.key = key; + command.func = function() { + func(); + infos.update(); + } + command.button = makeButtonElement({ + icon: icon, + name: name, + key: key, + func: command.func, + }); + commandBarElement.appendChild(command.button.element); + + return command } -function saveCanvas(e) { - const link = document.createElement('a'); - link.download = 'canvas.png'; - link.href = canvas.toDataURL(); - link.click(); +function makeCommands() { + const commands = []; + + commands.add = function({name, key, icon, func}) { + const command = makeCommand({name, key, icon, func}); + commands.push(command); + } + + commands.get = function(name) { + return commands.find(command => command.name === name); + } + + commands.click = function(name) { + const command = commands.get(name); + command.func(); + } + + return commands; } -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); + +// }}} + +const commands = makeCommands(); + +commands.add({ // flip-horizontally {{{ + name: 'flip-horizontally', + key: 'f', + icon: '', + func: function flipCanvasHorizontally() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + ctx.save(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.scale(-1, 1); + ctx.translate(-canvas.width, 0); + canvas.restoreCanvas(); + ctx.restore(); + } +}); // }}} + +commands.add({ // flip-vertically {{{ + name: 'flip-vertically', + key: 'v', + icon: '', + func: function flipCanvasVertically() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + ctx.save(); + canvas.saveCanvas(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.scale(1, -1); + ctx.translate(0, -canvas.height); + canvas.restoreCanvas(); + ctx.restore(); + } +}); // }}} + +commands.add({ // export {{{ + name: 'export', + key: 'e', + icon: '', + func: function exportCanvas() { + const canvas = layers.getActive().canvas; + const link = document.createElement('a'); + link.download = 'canvas.png'; + link.href = canvas.toDataURL(); + link.click(); + } +}); // }}} + +commands.add({ // import {{{ + name: 'import', + key: 'i', + icon: '', + func: function importCanvas() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + 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; } - img.src = e.target.result; + reader.readAsDataURL(file); } - reader.readAsDataURL(file); + input.click(); } - input.click(); -} +}); // }}} + +commands.add({ // clear {{{ + name: 'clear', + key: 'c', + icon: '', + func: function clearCanvas() { + const canvas = layers.getActive().canvas; + const ctx = canvas.ctx; + canvas.saveCanvas(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } +}); // }}} + +commands.add({ // change-shape {{{ + name: 'change-shape', + key: 's', + icon: ``, + func: function changeShape() { + const currentIndex = shapes.indexOf(brushShape); + brushShape = shapes[(currentIndex + 1) % shapes.length]; + } +}); // }}} -function clearCanvas(e) { - saveState(); - ctx.clearRect(0, 0, canvas.width, canvas.height); -} +// }}} + +// TOOLS {{{ + +// FACTORY {{{ -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; +function makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}) { + if (!name) throw new Error('No name provided'); + if (!key) throw new Error('No key provided'); + if (!icon) throw new Error('No icon provided'); - canvasAreaRect = canvasArea.getBoundingClientRect(); + const tool = {}; + tool.name = name; + tool.key = key; + tool.icon = icon; + tool.mouseDown = mouseDown; + tool.mouseMove = mouseMove; + tool.mouseUp = mouseUp; + tool.active = false; + + tool.activate = function() { + currentTool = tool.name; + tool.active = true; + tool.button.element.classList.add('active'); + } - canvasContainer.style.left = `${canvasAreaRect.left}px`; - canvasContainer.style.top = `${canvasAreaRect.top}px`; + tool.deactivate = function() { + tool.active = false; + tool.button.element.classList.remove('active'); + } + + tool.button = makeButtonElement({ + icon: tool.icon, + name: tool.name, + key: tool.key, + func: function() { + tools.activate(tool.name); + } + }); + + toolBarElement.appendChild(tool.button.element); + + return tool; } -// }}} +function makeTools() { + const tools = []; -// menu {{{ + tools.add = function({name, key, icon, mouseDown, mouseMove, mouseUp}) { + const tool = makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}); + tools.push(tool); + } -var menuButtons = []; + tools.get = function(name) { + return tools.find(tool => tool.name === name); + } -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(); - }); + tools.activate = function(name) { + const tool = tools.get(name); + tools.forEach(tool => tool.deactivate()); + tool.activate(); } - 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)); + return tools; +} // }}} -// pucks {{{ - -function createPuck(c, editable=true) { - if (c === undefined) { - c = color; +const tools = makeTools(); + +tools.add({ // brush {{{ + name: 'brush', + key: 'b', + icon: '', + mouseDown: function(e) { + const canvas = layers.getActive().canvas; + if (brushSize === 1) { + canvas.drawPixel(canvasStartX, canvasStartY, brushColor); + } else { + canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, brushColor); + } + }, + mouseMove: function(e) { + const canvas = layers.getActive().canvas; + if (brushSize === 1) { + canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushColor); + } else { + canvas.drawLineWithShape(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushShape, brushSize, brushColor); + } + canvasStartX = canvasEndX; + canvasStartY = canvasEndY; + }, +}); // }}} + +tools.add({ // content-move {{{ + name: 'content-move', + key: 'h', + icon: '', + mouseDown: function(e) { + const canvas = layers.getActive().canvas; + canvas.saveCanvas(); + }, + mouseMove: function(e) { + const canvas = layers.getActive().canvas; + canvas.clearCanvas(); + canvas.restoreCanvas(dX, dY); + }, +}); // }}} + +tools.add({ // move {{{ + name: 'move', + key: 'm', + icon: '', + mouseDown: function(e) { + startX = e.clientX - easelElement.offsetLeft; + startY = e.clientY - easelElement.offsetTop; + }, + mouseMove: function(e) { + easelElement.style.left = dX + 'px'; + easelElement.style.top = dY + 'px'; + }, +}); // }}} + +tools.add({ // zoom {{{ + name: 'zoom', + key: 'z', + icon: '', + mouseMove: function(e) { + // TODO all canvases + // const canvas = layers.getActive().canvas; + zoom += dX * dZoom; + if (zoom < 0.1) zoom = 0.1; + // canvas.style.height = canvasHeight * zoom + 'px'; + // canvas.style.width = canvasWidth * zoom + 'px'; + startX = endX; + } +}); // }}} + +tools.add({ // bucket-fill {{{ + name: 'bucket-fill', + key: 'k', + icon: '', + mouseDown: function(e) { + const canvas = layers.getActive().canvas; + canvas.floodFill(canvasStartX, canvasStartY, brushColor); + } +}); // }}} + +tools.add({ // color-picker {{{ + name: 'color-picker', + key: 'a', + icon: '', + mouseDown: function(e) { + const canvas = layers.getActive().canvas; + const imageData = canvas.ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data; + const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; + brushColor = pickedColor; + colorPreview.update(); + } +}); // }}} + +tools.add({ // brush-size {{{ + name: 'brush-size', + key: 'd', + icon: '', + mouseMove: function(e) { + brushSize += dX * dBrushSize; + if (brushSize < 1) brushSize = 1; + if (brushSize > maxBrushSize) brushSize = maxBrushSize; + startX = endX; } +}); // }}} + +tools.add({ // resize {{{ + name: 'resize', + key: 'r', + icon: '', + mouseMove: function(e) { + // const canvas = layers.getActive().canvas; + // let newWidth = canvasWidth + dX / zoom; + // let newHeight = canvasHeight + dY / zoom; + // if (newWidth > 0 && newHeight > 0) { + // canvas.setWidth(newWidth); + // canvas.setHeight(newHeight); + // canvas.style.width = newWidth * zoom + 'px'; + // canvas.style.height = newHeight * zoom + 'px'; + // canvas.ctx.clearRect(0, 0, canvas.width, canvas.height); + // canvas.ctx.fillStyle = backgroundColor; + // canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); + // canvas.ctx.drawImage(tempCanvas, 0, 0); + // } + } +}); // }}} - const puck = document.createElement('div'); - puck.className = 'puck'; - puck.style.backgroundColor = c; +tools.add({ // color-mix {{{ + name: 'color-mix', + key: 'x', + icon: '', + mouseMove: function(e) { + // const canvas = layers.getActive().canvas; + const imageData = ctx.getImageData(canvasEndX, canvasEndY, 1, 1).data; + const canvasColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; - const selectHandle = document.createElement('div'); - selectHandle.className = 'select-handle'; - selectHandle.innerHTML = ''; - puck.appendChild(selectHandle); + const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); + const t = Math.min(1, distance / 300); - selectHandle.addEventListener('click', () => { - color = puck.style.backgroundColor; - updateColorPreview(); - updateInfos(); - }); + const mixedColor = mixbox.lerp(brushColor, canvasColor, t); - if (editable) { - const updateHandle = document.createElement('div'); - updateHandle.className = 'update-handle'; - updateHandle.innerHTML = ''; - puck.appendChild(updateHandle); + brushColor = mixedColor; - updateHandle.addEventListener('click', () => { - puck.style.backgroundColor = color; - }); + startX = e.clientX; + startY = e.clientY; + } +}); // }}} + +// }}} + +// PUCKS {{{ + +// FACTORY {{{ +function makePuck({puckColor, key, editable=true}) { + if (!puckColor) throw new Error('No puck color provided'); + + + const puck = {} + puck.element = document.createElement('div'); + puck.element.style.backgroundColor = puckColor; + puck.element.className = 'puck'; + + if (editable) { const deleteHandle = document.createElement('div'); deleteHandle.className = 'delete-handle'; deleteHandle.innerHTML = ''; - puck.appendChild(deleteHandle); - + puck.element.appendChild(deleteHandle); deleteHandle.addEventListener('click', () => { - console.log("test"); - puck.remove(); + puck.element.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 + if (key) { + puck.key = key; + const keyHint = document.createElement('div'); + keyHint.className = 'key-hint'; + keyHint.innerHTML = key; + puck.element.appendChild(keyHint); + } - document.addEventListener('mouseup', onMouseUp); + function mixx(startTime) { + var interval = setInterval(() => { + const elapsedTime = Date.now() - startTime; + const t = Math.min(1, elapsedTime / 10000); + const mixedColor = mixbox.lerp(brushColor, puck.style.backgroundColor, t); + brushColor = mixedColor; + colorPreview.update(); + infos.update(); + }, 50); + return interval; + } + puck.element.addEventListener('mousedown', () => { + const startTime = Date.now(); + var interval = mixx(startTime); function onMouseUp() { - isMixing = false; - clearInterval(interval); // Stop the interval when the mouse is released + clearInterval(interval); document.removeEventListener('mouseup', onMouseUp); } + document.addEventListener('mouseup', onMouseUp); }); - // puck.addEventListener('mousedown', (e) => { - // let isMixing = true; - // let startX = e.clientX; - // let startY = e.clientY; + puck.keydown = function(e) { + if (e.key == key) { + const startTime = Date.now(); + var interval = mixx(startTime); + function onKeyUp() { + clearInterval(interval); + document.removeEventListener('keyup', onKeyUp); + } + document.addEventListener('keyup', onKeyUp); + } + } + + commandBarElement.appendChild(puck.element); + + return puck; +} - // document.addEventListener('mousemove', onMouseMove); - // document.addEventListener('mouseup', onMouseUp); +function makePucks() { + const pucks = []; - // function onMouseMove(e) { - // if (isMixing) { - // const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); + pucks.add = function({puckColor, key, editable}) { + const puck = makePuck({puckColor, key, editable}); + pucks.push(puck); + } - // const t = Math.min(1, distance / 300); + return pucks; +} - // const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t); +// }}} - // color = mixedColor; +const pucks = makePucks(); - // startX = e.clientX; - // startY = e.clientY; - // updateColorPreview(); - // updateInfos(); - // } - // } - // function onMouseUp() { - // isMixing = false; - // document.removeEventListener('mousemove', onMouseMove); - // document.removeEventListener('mouseup', onMouseUp); - // } - // }); +pucks.add({ + puckColor: 'rgb(0, 0, 0)', + key: '1', + editable: false, +}); +pucks.add({ + puckColor: 'rgb(255, 255, 255)', + key: '2', + editable: false, +}); - menuBar.appendChild(puck); -} +pucks.add({ + puckColor: 'rgb(255, 0, 0)', + key: '3', + editable: false, +}); -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); +pucks.add({ + puckColor: 'rgb(0, 255, 0)', + key: '4', + editable: false, +}); + +pucks.add({ + puckColor: 'rgb(0, 0, 255)', + key: '5', + editable: false, +}); // }}} -// info {{{ +// INFO {{{ + +// FACTORY {{{ + +function makeInfo({name, updateFunction}) { + if (!name) throw new Error('No name provided'); + if (!updateFunction) throw new Error('No update function provided'); -var infos = []; + const info = {}; + info.name = name; + info.updateFunction = updateFunction; + + info.element = document.createElement('span'); + info.element.className = 'info'; -function createInfo(name, updateFunction) { - const info = document.createElement('span'); - info.className = 'info'; const key = document.createElement('span'); key.className = 'key'; - key.innerHTML = name + ':'; + key.innerHTML = info.name + ':'; + const value = document.createElement('span'); value.className = 'value'; value.innerHTML = '0'; - info.appendChild(key); - info.appendChild(value); - infoBar.appendChild(info); - function update() { + + info.element.appendChild(key); + info.element.appendChild(value); + + infoBarElement.appendChild(info.element); + + info.update = function() { let v = updateFunction(); if (v === undefined) v = '?'; value.innerHTML = v; } - return update; + + return info; } -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()); +function makeInfos() { + const infos = [] + infos.add = function({name, updateFunction}) { + const info = makeInfo({name, updateFunction}); + infos.push(info); + } + infos.update = function() { + infos.forEach(function(info){ + info.update(); + }); + } + return infos; } // }}} -// keybindings {{{ +const infos = makeInfos(); -let keyDown = false; -let oldTool = tool; +infos.add({ + name: 'zoom', + updateFunction: function() { + var percent = zoom * 100; + return percent.toFixed(0) + '%'; + } +}); -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}, -] +infos.add({ + name: 'brush', + updateFunction: function() { + return brushSize; + } +}); -const functionBindings = [ - {'key': 'u', 'function': undo}, - {'key': 'y', 'function': redo}, - {'key': 'backspace', 'function': clearCanvas}, -] -document.addEventListener('keydown', (e) => { - if (keyDown) return; +infos.add({ + name: 'x', + updateFunction: function() { + return canvasEndX; + } +}); - if (toolBindings.map(b => b.key).includes(e.key)) { - oldTool = tool; - keyDown = true; - changeTool(toolBindings.find(b => b.key === e.key).tool); - return; + +infos.add({ + name: 'y', + updateFunction: function() { + return canvasEndY; } +}); - if (functionBindings.map(b => b.key).includes(e.key)) { - functionBindings.find(b => b.key === e.key).function(); +infos.add({ + name: 'color', + updateFunction: function() { + return brushColor; } }); -document.addEventListener('keyup', (e) => { - keyDown = false; - if (toolBindings.filter(b => !b.persistent).map(b => b.key).includes(e.key)) { - changeTool(oldTool); +infos.add({ + name: 'width', + updateFunction: function() { + return "width"; + } +}); + +infos.add({ + name: 'height', + updateFunction: function() { + return "height"; + } +}); + +infos.add({ + name: 'shape', + updateFunction: function() { + return brushShape; } }); // }}} -// start {{{ +// MOUSE EVENT LISTENERS {{{ + +studioElement.addEventListener('mousedown', (e) => { + const canvas = layers.getActive().canvas; + isMouseDown = true; + startX = e.clientX; + startY = e.clientY; + canvasStartX = canvas.getPositionOnCanvas(e).x; + canvasStartY = canvas.getPositionOnCanvas(e).y; + + for (var i = 0; i < tools.length; i++) { + var tool = tools[i]; + if (tool.name === currentTool) { + if (tool.mouseDown) { + tool.mouseDown(e); + break; + } + } + } + + infos.update(); + +}); + +studioElement.addEventListener('mousemove', (e) => { + const canvas = layers.getActive().canvas; + endX = e.clientX; + endY = e.clientY; + dX = endX - startX; + dY = endY - startY; + canvasEndX = canvas.getPositionOnCanvas(e).x; + canvasEndY = canvas.getPositionOnCanvas(e).y; + canvasDX = canvasEndX - canvasStartX; + canvasDY = canvasEndY - canvasStartY; + + if (currentTool == 'brush-size') { + brushPreviewElement.style.display = 'block'; + brushPreviewElement.style.width = brushSize + 'px'; + brushPreviewElement.style.height = brushSize + 'px'; + brushPreviewElement.style.left = e.clientX - brushSize / 2 + 'px'; + brushPreviewElement.style.top = e.clientY - brushSize / 2 + 'px'; + } -ctx.fillStyle = backgroundColor; -ctx.fillRect(0, 0, canvas.width, canvas.height); -updateInfos(); -toolButtons[0]['button'].click(); -resetZoom(); + if (isMouseDown) { + for (var i = 0; i < tools.length; i++) { + var tool = tools[i]; + if (tool.name === currentTool) { + if (tool.mouseMove) { + tool.mouseMove(e); + break; + } + } + } + } + + infos.update(); + +}); + +studioElement.addEventListener('mouseup', () => { + isMouseDown = false; + infos.update(); +}); + +studioElement.addEventListener('mouseleave', () => { + isMouseDown = false; + brushPreviewElement.style.display = 'none'; + infos.update(); +}); + +// }}} + +// KEYBINDINGS {{{ + +document.addEventListener('keydown', (e) => { + if (isKeyDown) return; + + tools.forEach(tool => { + if (tool.key.toLowerCase() === e.key.toLowerCase()) { + prevTool = currentTool; + currentTool = tool.name; + } + }); + + commands.forEach(command => { + if (command.key.toLowerCase() === e.key.toLowerCase()) { + command.func(); + } + }); + + pucks.filter(puck => puck.key !== undefined).forEach(puck => { + if (puck.key.toLowerCase() === e.key.toLowerCase()) { + puck.keydown(e); + } + }); + + isKeyDown = true; + +}); + +document.addEventListener('keyup', (e) => { + tools.forEach(tool => { + if (tool.key.toLowerCase() === e.key) { + currentTool = prevTool; + } + }); + + isKeyDown = false; + +}); // }}} +layers.resetPosition(); +tools.activate('brush'); diff --git a/style.css b/style.css index 0662bbb..4e326e6 100644 --- a/style.css +++ b/style.css @@ -58,6 +58,7 @@ body { display: flex; padding: 10px; padding-top: 0; + padding-bottom: 0; background-color: #ddd; gap: 10px; height: 100%; @@ -65,22 +66,24 @@ body { justify-content: flex-start; } -#canvas-area { +#studio { flex-grow: 1; height: 100%; border: 1px solid; cursor: crosshair; } -#canvas-container { +#easel { position: absolute; border: 1px solid; z-index: -1; } -#canvas { - display: block; +canvas { + position: absolute; + top: 0; + left: 0; } .button { @@ -126,11 +129,11 @@ body { cursor: pointer; } -.update-handle { +.delete-handle { position: absolute; top: 0; right: 0; - font-size: .5em; + font-size: .6em; background-color: black; color: white; padding: 1px 0 3px 4px; @@ -144,33 +147,16 @@ body { cursor: pointer; } -.delete-handle { - position: absolute; - top: 0; - left: 0; - font-size: .5em; - background-color: black; - color: white; - padding: 1px 4px 3px 0; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - border-radius: 0 0 10px 0; - border-right: 1px solid white; - border-bottom: 1px solid white; - cursor: pointer; -} -.jump-key-hint { +.key-hint { display: block; - height: 12px; - width: 8px; + height: 13px; + width: 9px; line-height: 8px; /* width: 12px; */ position: absolute; - top: 0; - left: 0; + top: 2px; + left: 1px; font-size: 1em; /* padding: 2px 2px 2px 2px; */ /* display: flex; */ @@ -180,8 +166,99 @@ body { border-radius: 0 0 5px 0; /* border-right: 1px solid; */ /* border-bottom: 1px solid; */ + text-shadow: + 1px 1px 0 #000, + -1px 1px 0 #000, + -1px -1px 0 #000, + 1px -1px 0 #000; + color: white; + pointer-events: none; } + .button.active, .button:active { background-color: darkgray; } + +#layer-controllers { + background-color: darkgrey; + border: 1px solid; + height: 100%; +} + +.layer-controller { + background-color: lightgray; + height: 30px; + width: 60px; + border: 1px solid; + border-radius: 2px; + padding: 5px; + position: relative; + margin: 1px; +} + +.layer-controller > i { + color: darkgray; + cursor: pointer; +} + +.layer-controller.active > i { + color: black; +} + +.handle { + display: block; + font-size: 9px; + height: 11px; + width: 24px; + position: absolute; + justify-content: center; + text-align: center; + cursor: pointer; + border-radius: 2px; + border: 1px solid; + margin: 2px; +} + +.layer-add-button { + background-color: lightgray; + height: 12px; + width: 60px; + border: 1px solid; + border-radius: 2px; + position: relative; + margin: 1px; + text-align: center; + font-size: 10px; + cursor: pointer; +} + +.layer-add-button:hover { + background-color: darkgray; +} + + + +.handle:hover { + background-color: darkgray; +} + +.top-left { + top: 0; + left: 0; +} + +.bottom-right { + bottom: 0; + right: 0; +} + +.top-right { + top: 0; + right: 0; +} + +.bottom-left { + bottom: 0; + left: 0; +} diff --git a/temp.js b/temp.js new file mode 100644 index 0000000..fdb075f --- /dev/null +++ b/temp.js @@ -0,0 +1,724 @@ +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(); + +// }}}