diff --git a/render.js b/render.js index 87ba792..2534771 100644 --- a/render.js +++ b/render.js @@ -20,12 +20,9 @@ const shapes = ['circle', 'square']; // VARS {{{ -let brushColor = 'rgb(0, 0, 0)'; let brushShape = 'circle' -let brushSize = 15; +let brushSize = 10; let zoom = 1; -let currentTool; -let prevTool = 'brush'; let startX = 0; let startY = 0; @@ -43,6 +40,8 @@ let canvasDY = 0; let isKeyDown = false; let isMouseDown = false; +let interval; + // }}} // HELPERS {{{ @@ -57,22 +56,19 @@ function disableImageSmoothing(ctx) { }; function hexToRgbArray(hex) { - if (hex.startsWith('#')) { - hex = hex.slice(1); - } - - if (hex.length === 3) { - hex = hex.split('').map(char => char + char).join(''); - } - + hex = hex.replace(/^#/, ''); const bigint = parseInt(hex, 16); - return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return [r, g, b, 255]; // Add 255 for full opacity } 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; + Math.abs(color1[2] - color2[2]) <= tolerance && + Math.abs(color1[3] - color2[3]) <= tolerance; // Include alpha comparison } function makeIconElement(htmlString) { @@ -117,6 +113,38 @@ function makeButtonElement({icon, name, func, key}) { // }}} +// COLOR {{{ + +function makeColor(rgb) { + const color = {}; + + color.color = rgb; + + color.toRgb = function() { + return color.rgb; + } + + color.toHex = function() { + color.r = parseInt(rgb[0]); + color.g = parseInt(rgb[1]); + color.b = parseInt(rgb[2]); + return `#${color.r.toString(16)}${color.g.toString(16)}${color.b.toString(16)}`; + } + + color.mix = function(color2, t) { + const color1 = color.color; + const newColor = mixbox.lerp(color1, color2, t); + color.color = newColor; + colorPreview.update(); + } + + return color; +} + +const color = makeColor('rgb(0, 0, 0)'); + +// }}} + // LAYERS {{{ // FACTORY {{{ @@ -203,7 +231,7 @@ function makeCanvas({height=600, width=800}) { // {{{ 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 radius = Math.floor(size / 1); let radiusSquared = radius * radius; for (let y1 = -radius; y1 <= radius; y1++) { @@ -260,25 +288,25 @@ function makeCanvas({height=600, width=800}) { // {{{ canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); } - canvas.getColorAtPixel = function(data, x, y) { + canvas.getColorAtPixelData = function(x, y, data) { const index = (y * canvas.width + x) * 4; - return [data[index], data[index + 1], data[index + 2], data[index + 3]]; + const color = [data[index], data[index + 1], data[index + 2], data[index + 3]]; + return color; } - canvas.setColorAtPixel = function(data, x, y, color) { + canvas.setColorAtPixelData = function(x, y, color, data) { 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; + data[index + 3] = color[3]; } 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; - const targetColor = canvas.getColorAtPixel(data, x, y); + const targetColor = canvas.getColorAtPixelData(x, y, data); const fillColorArray = hexToRgbArray(color); if (colorsMatch(targetColor, fillColorArray, tolerance)) { @@ -289,10 +317,10 @@ function makeCanvas({height=600, width=800}) { // {{{ while (stack.length > 0) { const {x, y} = stack.pop(); - const currentColor = canvas.getColorAtPixel(data, x, y); + const currentColor = canvas.getColorAtPixelData(x, y, data); if (colorsMatch(currentColor, targetColor, tolerance)) { - canvas.setColorAtPixel(data, x, y, fillColorArray); + canvas.setColorAtPixelData(x, y, fillColorArray, data); if (x > 0) stack.push({x: x - 1, y}); if (x < canvas.width - 1) stack.push({x: x + 1, y}); @@ -304,6 +332,19 @@ function makeCanvas({height=600, width=800}) { // {{{ canvas.ctx.putImageData(imageData, 0, 0); } + + canvas.getColorAtPixel = function(x, y) { + const data = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height).data; + return canvas.getColorAtPixelData(x, y, data); + } + + canvas.setColorAtPixel = function(x, y, color) { + const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + canvas.setColorAtPixelData(x, y, color, data); + canvas.ctx.putImageData(new ImageData(data, canvas.width, canvas.height), 0, 0); + } + canvas.toDataUrl = function() { const dataURL = canvas.toDataURL(); const dimensions = `${canvas.width}x${canvas.height}`; @@ -491,11 +532,13 @@ function makeColorPreview() { colorPreview.element = document.createElement('div'); colorPreview.element.id = 'color-preview'; colorPreview.element.className = 'puck'; - colorPreview.element.style.backgroundColor = brushColor; + colorPreview.element.style.backgroundColor = color.color; commandBarElement.appendChild(colorPreview.element); colorPreview.update = function() { - colorPreview.element.style.backgroundColor = brushColor; + colorPreview.element.style.backgroundColor = color.color; } + + return colorPreview; } const colorPreview = makeColorPreview(); @@ -674,7 +717,6 @@ function makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}) { tool.active = false; tool.activate = function() { - currentTool = tool.name; tool.active = true; tool.button.element.classList.add('active'); } @@ -701,6 +743,8 @@ function makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}) { function makeTools() { const tools = []; + tools.prevToolName = 'na'; + tools.add = function({name, key, icon, mouseDown, mouseMove, mouseUp}) { const tool = makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}); tools.push(tool); @@ -710,8 +754,22 @@ function makeTools() { return tools.find(tool => tool.name === name); } + tools.getActive = function() { + return tools.find(tool => tool.active); + } + tools.activate = function(name) { const tool = tools.get(name); + if (tool.active) return; + if (tools.getActive()) { + tools.prevToolName = tools.getActive().name; + tools.forEach(tool => tool.deactivate()); + } + tool.activate(); + } + + tools.restore = function() { + const tool = tools.get(tools.prevToolName); tools.forEach(tool => tool.deactivate()); tool.activate(); } @@ -730,17 +788,17 @@ tools.add({ // brush {{{ mouseDown: function(e) { const canvas = layers.getActive().canvas; if (brushSize === 1) { - canvas.drawPixel(canvasStartX, canvasStartY, brushColor); + canvas.drawPixel(canvasStartX, canvasStartY, color.color); } else { - canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, brushColor); + canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, color.color); } }, mouseMove: function(e) { const canvas = layers.getActive().canvas; if (brushSize === 1) { - canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushColor); + canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, color.color); } else { - canvas.drawLineWithShape(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushShape, brushSize, brushColor); + canvas.drawLineWithShape(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushShape, brushSize, color.color); } canvasStartX = canvasEndX; canvasStartY = canvasEndY; @@ -797,7 +855,7 @@ tools.add({ // bucket-fill {{{ icon: '', mouseDown: function(e) { const canvas = layers.getActive().canvas; - canvas.floodFill(canvasStartX, canvasStartY, brushColor); + canvas.floodFill(canvasStartX, canvasStartY, color.color); } }); // }}} @@ -809,7 +867,7 @@ tools.add({ // color-picker {{{ 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; + color.color = pickedColor; colorPreview.update(); } }); // }}} @@ -847,25 +905,29 @@ tools.add({ // resize {{{ } }); // }}} + 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; + mouseDown: function(e) { + const startTime = Date.now(); + const canvas = layers.getActive().canvas; + interval = setInterval(() => { + let canvasColor = canvas.getColorAtPixel(canvasEndX, canvasEndX); + console.log({canvasEndX, canvasEndY, canvasColor}); + const elapsedTime = Date.now() - startTime; + const t = Math.min(1, elapsedTime / 10000); + color.mix(canvasColor, t); + }, 50); + }, + mouseUp: function(e) { + clearInterval(interval); + }, + mouseLeave: function(e) { + clearInterval(interval); + } - startX = e.clientX; - startY = e.clientY; - } }); // }}} // }}} @@ -901,38 +963,32 @@ function makePuck({puckColor, key, editable=true}) { puck.element.appendChild(keyHint); } - function mixx(startTime) { - var interval = setInterval(() => { + + puck.element.addEventListener('mousedown', (e) => { + const startTime = Date.now(); + 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(); + color.mix(puck.element.style.backgroundColor, t); }, 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.element.addEventListener('mouseup', (e) => { + clearInterval(interval); }); 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); + const startTime = Date.now(); + var interval = setInterval(() => { + const elapsedTime = Date.now() - startTime; + const t = Math.min(1, elapsedTime / 10000); + color.mix(puck.element.style.backgroundColor, t); + }, 50); + function onKeyUp() { + clearInterval(interval); + document.removeEventListener('keyup', onKeyUp); } + document.addEventListener('keyup', onKeyUp); } commandBarElement.appendChild(puck.element); @@ -1078,7 +1134,7 @@ infos.add({ infos.add({ name: 'color', updateFunction: function() { - return brushColor; + return color.color; } }); @@ -1103,6 +1159,13 @@ infos.add({ } }); +infos.add({ + name: 'tool', + updateFunction: function() { + return tools.getActive().name; + } +}); + // }}} // MOUSE EVENT LISTENERS {{{ @@ -1114,10 +1177,12 @@ studioElement.addEventListener('mousedown', (e) => { startY = e.clientY; canvasStartX = canvas.getPositionOnCanvas(e).x; canvasStartY = canvas.getPositionOnCanvas(e).y; + canvasEndX = canvas.getPositionOnCanvas(e).x; + canvasEndX = canvas.getPositionOnCanvas(e).y; for (var i = 0; i < tools.length; i++) { var tool = tools[i]; - if (tool.name === currentTool) { + if (tool.active) { if (tool.mouseDown) { tool.mouseDown(e); break; @@ -1140,7 +1205,7 @@ studioElement.addEventListener('mousemove', (e) => { canvasDX = canvasEndX - canvasStartX; canvasDY = canvasEndY - canvasStartY; - if (currentTool == 'brush-size') { + if (tools.getActive().name === 'brush-size') { brushPreviewElement.style.display = 'block'; brushPreviewElement.style.width = brushSize + 'px'; brushPreviewElement.style.height = brushSize + 'px'; @@ -1151,7 +1216,7 @@ studioElement.addEventListener('mousemove', (e) => { if (isMouseDown) { for (var i = 0; i < tools.length; i++) { var tool = tools[i]; - if (tool.name === currentTool) { + if (tool.active) { if (tool.mouseMove) { tool.mouseMove(e); break; @@ -1166,11 +1231,33 @@ studioElement.addEventListener('mousemove', (e) => { studioElement.addEventListener('mouseup', () => { isMouseDown = false; + + for (var i = 0; i < tools.length; i++) { + var tool = tools[i]; + if (tool.active) { + if (tool.mouseUp) { + tool.mouseUp(); + break; + } + } + } + infos.update(); }); studioElement.addEventListener('mouseleave', () => { isMouseDown = false; + + for (var i = 0; i < tools.length; i++) { + var tool = tools[i]; + if (tool.active) { + if (tool.mouseLeave) { + tool.mouseLeave(); + break; + } + } + } + brushPreviewElement.style.display = 'none'; infos.update(); }); @@ -1184,8 +1271,7 @@ document.addEventListener('keydown', (e) => { tools.forEach(tool => { if (tool.key.toLowerCase() === e.key.toLowerCase()) { - prevTool = currentTool; - currentTool = tool.name; + tools.activate(tool.name); } }); @@ -1208,7 +1294,7 @@ document.addEventListener('keydown', (e) => { document.addEventListener('keyup', (e) => { tools.forEach(tool => { if (tool.key.toLowerCase() === e.key) { - currentTool = prevTool; + tools.restore(); } }); diff --git a/temp2.js b/temp2.js new file mode 100644 index 0000000..1fcc457 --- /dev/null +++ b/temp2.js @@ -0,0 +1,1282 @@ +// CONSTANTS {{{ + +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'); + +const dZoom = 0.001; +const dBrushSize = 0.5; +const initialWidth = 800; +const initialHeight = 600; +const maxBrushSize = 500; +const tolerance = 1; +const shapes = ['circle', 'square']; + +// }}} + +// VARS {{{ + +let brushShape = 'circle' +let brushSize = 15; +let zoom = 1; + +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 disableImageSmoothing(ctx) { + ctx.imageSmoothingEnabled = false; + if (ctx.imageSmoothingEnabled !== false) { + ctx.mozImageSmoothingEnabled = false; + ctx.webkitImageSmoothingEnabled = false; + ctx.msImageSmoothingEnabled = false; + } +}; + +function hexToRgbArray(hex) { + hex = hex.replace(/^#/, ''); + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return [r, g, b, 255]; // Add 255 for full opacity +} + +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 && + Math.abs(color1[3] - color2[3]) <= tolerance; // Include alpha comparison +} + +function makeIconElement(htmlString) { + const parentElement = document.createElement('div'); + parentElement.innerHTML = htmlString; + const iconElement = parentElement.firstChild; + return iconElement; +} + +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); + } + } + + button.refresh(); + + return button; +} + +// }}} + +// COLOR {{{ + +function makeColor(rgb) { + const color = {}; + + color.color = rgb; + + color.toRgb = function() { + return color.rgb; + } + + color.toHex = function() { + color.r = parseInt(rgb[0]); + color.g = parseInt(rgb[1]); + color.b = parseInt(rgb[2]); + return `#${color.r.toString(16)}${color.g.toString(16)}${color.b.toString(16)}`; + } + + color.mix = function(color2, t) { + const color1 = color.color; + const newColor = mixbox.lerp(color1, color2, t); + color.color = newColor; + console.log(color.color); + colorPreview.update(); + } + + return color; +} + +const color = makeColor({r: 0, g: 0, b: 0}); + +// }}} + +// LAYERS {{{ + +// FACTORY {{{ + +function makeCanvas({height=600, width=800}) { // {{{ + const canvas = document.createElement('canvas'); + canvas.style.imageRendering = 'pixelated'; + canvas.ctx = canvas.getContext('2d'); + + canvas.tempCanvas = document.createElement('canvas'); + canvas.tempCtx = canvas.tempCanvas.getContext('2d'); + + 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); + } + + canvas.clearCanvas = function() { + canvas.ctx.clearRect(0, 0, canvas.width, canvas.height); + } + + canvas.restoreCanvas = function(x=0, y=0) { + canvas.ctx.drawImage(canvas.tempCanvas, x, y); + } + + canvas.setHeight = function(height) { + canvas.height = height; + disableImageSmoothing(canvas.ctx); + }; + + canvas.setWidth = function(width) { + canvas.width = width; + disableImageSmoothing(canvas.ctx); + }; + + canvas.resize = function(width, height) { + canvas.width = width; + canvas.height = height; + 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) { + 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) { + 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 / 1); + 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); + } + } + } + } + + 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)); + + 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); + } + } + + canvas.fill = function(color) { + canvas.ctx.fillStyle = color; + canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + canvas.getColorAtPixelData = function(x, y, data) { + const index = (y * canvas.width + x) * 4; + const color = [data[index], data[index + 1], data[index + 2], data[index + 3]]; + return color; + } + + canvas.setColorAtPixelData = function(x, y, color, data) { + const index = (y * canvas.width + x) * 4; + data[index] = color[0]; + data[index + 1] = color[1]; + data[index + 2] = color[2]; + data[index + 3] = color[3]; + } + + 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; + + const targetColor = canvas.getColorAtPixelData(x, y, data); + const fillColorArray = hexToRgbArray(color); + + if (colorsMatch(targetColor, fillColorArray, tolerance)) { + return; + } + + const stack = [{x, y}]; + + while (stack.length > 0) { + const {x, y} = stack.pop(); + const currentColor = canvas.getColorAtPixelData(x, y, data); + + if (colorsMatch(currentColor, targetColor, tolerance)) { + canvas.setColorAtPixelData(x, y, fillColorArray, data); + + 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.getColorAtPixel = function(x, y) { + const data = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height).data; + return canvas.getColorAtPixelData(x, y, data); + } + + canvas.setColorAtPixel = function(x, y, color) { + const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + canvas.setColorAtPixelData(x, y, color, data); + canvas.ctx.putImageData(new ImageData(data, canvas.width, canvas.height), 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.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'); + } + + 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.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'; + } + + 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.refreshControllers = function() { + layerControllersElement.innerHTML = ''; + layers.forEach(layer => { + layerControllersElement.appendChild(layer.controllerElement); + }); + layerControllersElement.appendChild(layers.addButton); + } + + layers.refreshLayers = function() { + easelElement.innerHTML = ''; + layers.forEach(layer => { + easelElement.appendChild(layer.canvas); + }); + } + + layers.refresh = function() { + layers.refreshControllers(); + layers.refreshLayers(); + } + + 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; +} // }}} + +// }}} + +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 = color.color; + commandBarElement.appendChild(colorPreview.element); + colorPreview.update = function() { + colorPreview.element.style.backgroundColor = color.color; + } + + return colorPreview; +} + +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 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; +} + + +// }}} + +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; + } + reader.readAsDataURL(file); + } + 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]; + } +}); // }}} + +// }}} + +// 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.activate = function() { + tool.active = true; + tool.button.element.classList.add('active'); + } + + 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 = []; + + tools.prevToolName = 'na'; + + tools.add = function({name, key, icon, mouseDown, mouseMove, mouseUp}) { + const tool = makeTool({name, key, icon, mouseDown, mouseMove, mouseUp}); + tools.push(tool); + } + + tools.get = function(name) { + return tools.find(tool => tool.name === name); + } + + tools.getActive = function() { + return tools.find(tool => tool.active); + } + + tools.activate = function(name) { + const tool = tools.get(name); + if (tool.active) return; + if (tools.getActive()) { + tools.prevToolName = tools.getActive().name; + tools.forEach(tool => tool.deactivate()); + } + tool.activate(); + } + + tools.restore = function() { + const tool = tools.get(tools.prevToolName); + 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, color.color); + } else { + canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, color.color); + } + }, + mouseMove: function(e) { + const canvas = layers.getActive().canvas; + if (brushSize === 1) { + canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, color.color); + } else { + canvas.drawLineWithShape(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushShape, brushSize, color.color); + } + 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, color.color); + } +}); // }}} + +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]})`; + color.color = 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 canvasColor = canvas.getColorAtPixel(canvasStartX, canvasStartY); + const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); + const t = Math.min(1, distance / 300); + color.mix(canvasColor, t); + startX = endX; + startY = endY; + } +}); // }}} + +// }}} + +// 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.element.appendChild(deleteHandle); + deleteHandle.addEventListener('click', () => { + puck.element.remove(); + }); + } + + if (key) { + puck.key = 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 puckColor = puck.element.style.backgroundColor; + // const mixedColor = mixbox.lerp(color.color, puck.element.style.backgroundColor, t); + // color.color = 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); + + return puck; +} + +function makePucks() { + const pucks = []; + + pucks.add = function({puckColor, key, editable}) { + const puck = makePuck({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 {{{ + +// FACTORY {{{ + +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 color.color; + } +}); + +infos.add({ + name: 'width', + updateFunction: function() { + return "width"; + } +}); + +infos.add({ + name: 'height', + updateFunction: function() { + return "height"; + } +}); + +infos.add({ + name: 'shape', + updateFunction: function() { + return brushShape; + } +}); + +infos.add({ + name: 'tool', + updateFunction: function() { + return tools.getActive().name; + } +}); + +// }}} + +// 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.active) { + 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 (tools.getActive().name === '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.active) { + 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; + console.log(e.key); + + tools.forEach(tool => { + if (tool.key.toLowerCase() === e.key.toLowerCase()) { + tools.activate(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) { + tools.restore(); + } + }); + + isKeyDown = false; + +}); + +// }}} + +layers.resetPosition(); +tools.activate('brush');