// 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 dZoom = 0.001; const dBrushSize = 0.5; const dOpacity = 0.001; const initialWidth = 800; const initialHeight = 600; const maxBrushSize = 500; const tolerance = 1; const shapes = ['circle', 'square']; // }}} // VARS {{{ let brushShape = 'circle' let brushSize = 10; 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; let interval; var startTime; // }}} // HELPERS {{{ function disableImageSmoothing(ctx) { ctx.imageSmoothingEnabled = false; if (ctx.imageSmoothingEnabled !== false) { ctx.mozImageSmoothingEnabled = false; ctx.webkitImageSmoothingEnabled = false; ctx.msImageSmoothingEnabled = false; } }; function hexToRgbArray(hex) { const r = parseInt(hex.substring(1, 3), 16); const g = parseInt(hex.substring(3, 5), 16); const b = parseInt(hex.substring(5, 7), 16); return [r, g, b, 255]; } function colorsMatch(color1, color2, tolerance = 0) { return color1[0] === color2[0] && color1[1] === color2[1] && color1[2] === color2[2] && color1[3] === color2[3]; 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 closeRgbArray(color1, color2, tolerance) { return Math.abs(color1[0] - color2[0]) <= tolerance && Math.abs(color1[1] - color2[1]) <= tolerance && Math.abs(color1[2] - color2[2]) <= tolerance; } 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.classList.add('button'); button.element.classList.add('bar-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.fromRgba = function(rgba) { color.r = rgba.split(',')[0].split('(')[1]; color.g = rgba.split(',')[1]; color.b = rgba.split(',')[2]; color.a = rgba.split(',')[3].split(')')[0]; } color.toRgba = function() { return `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`; } color.fromRgb = function(rgb) { color.r = rgb.split(',')[0].split('(')[1]; color.g = rgb.split(',')[1]; color.b = rgb.split(',')[2].split(')')[0]; color.a = 255; } color.toRgb = function() { return `rgb(${color.r}, ${color.g}, ${color.b})`; } color.fromHex = function(hex) { color.r = parseInt(hex.substring(1, 3), 16); color.g = parseInt(hex.substring(3, 5), 16); color.b = parseInt(hex.substring(5, 7), 16); color.a = 255; } 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.fromRgbaArray = function(array) { color.r = array[0]; color.g = array[1]; color.b = array[2]; color.a = array[3]; } color.toRgbaArray = function() { return [color.r, color.g, color.b, color.a]; } color.fromRgbArray = function(array) { color.r = array[0]; color.g = array[1]; color.b = array[2]; color.a = 255; } color.toRgbArray = function() { return [color.r, color.g, color.b]; } color.mixxRgbArray = function(color2RgbArray, t) { const color1RgbArray = color.toRgbArray(); if (color1RgbArray === color2RgbArray) { return; } var newColorRgbArray = color2RgbArray; if (!closeRgbArray(color1RgbArray, color2RgbArray, 2)) { console.log('mixxing'); newColorRgbArray = mixbox.lerp(color1RgbArray, color2RgbArray, t); } color.fromRgbArray(newColorRgbArray); } color.mixxRgb = function(color2Rgb, t) { const result = color2Rgb.match(/rgb\((\d+), (\d+), (\d+)\)/); if (result) { const color2RgbArray = [result[1], result[2], result[3]]; color.mixxRgbArray(color2RgbArray, t); } } color.mixxRgbaArray = function(color2RgbaArray, t) { const color2a = color2RgbaArray[3]; if (color2a !== 225) { return; } const color2RgbArray = color2RgbaArray.slice(0, 3); color.mixxRgbArray(color2RgbArray, t); } color.mixxRgba = function(color2Rgba, t) { const result = color2Rgba.match(/rgba\((\d+), (\d+), (\d+), (\d+)\)/); if (result) { const color2RgbaArray = [result[1], result[2], result[3], result[4]]; color.mixxRgbaArray(color2RgbaArray, t); } } color.mixx = function(color2, t) { if (color2.a === 255) { color.mixxRgbArray(color2.toRgbArray(), t); } } color.isOpaque = function() { return color.a === 255; } color.match = function(color2) { return color.r === color2.r && color.g === color2.g && color.b === color2.b && color.a === color2.a; } color.copy = function(color2) { color.r = color2.r; color.g = color2.g; color.b = color2.b; color.a = color2.a; } color.fromRgb(rgb); return color; } const brushColor = makeColor('rgb(0, 0, 0)'); const canvasColor = makeColor('rgb(0, 0, 0)'); const tempColor = makeColor('rgb(0, 0, 0)'); // }}} // LAYERS {{{ // FACTORY {{{ function makeCanvas({height=600, width=800, background=false}) { // {{{ const canvas = document.createElement('canvas'); canvas.style.imageRendering = 'pixelated'; canvas.ctx = canvas.getContext('2d'); canvas.background = background; 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() { if (!canvas.background) { 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.saveCanvas(); canvas.clearCanvas(); canvas.width = width; canvas.height = height; canvas.style.width = width * zoom + 'px'; canvas.style.height = height * zoom + 'px'; disableImageSmoothing(canvas.ctx); canvas.restoreCanvas(); } 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) { if (!canvas.background) { canvas.ctx.fillStyle = color; canvas.ctx.fillRect(x, y, 1, 1); } } canvas.drawLineWithPixels = function(x1, y1, x2, y2, color) { if (!canvas.background) { 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 (!canvas.background) { 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); } } } } } canvas.drawLineWithShape = function(x1, y1, x2, y2, shape, size, color) { if (!canvas.background) { const dx = x2 - x1; const dy = y2 - y1; const distance = Math.sqrt(dx * dx + dy * dy); const steps = Math.ceil(distance / (size / 3)); 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.getRgbaArrayColorAtPixelData = 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.setRgbaArrayColorAtPixelData = 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, colorRgbaArray) { if (!canvas.background) { const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const targetColor = canvas.getRgbaArrayColorAtPixelData(x, y, data); const fillColorArray = colorRgbaArray; if (colorsMatch(targetColor, fillColorArray, tolerance)) { return; } const stack = [{x, y}]; while (stack.length > 0) { const {x, y} = stack.pop(); const currentColor = canvas.getRgbaArrayColorAtPixelData(x, y, data); if (colorsMatch(currentColor, targetColor, tolerance)) { canvas.setRgbaArrayColorAtPixelData(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.getRgbaColorArrayAtPixel = function(x, y) { const data = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height).data; return canvas.getRgbaArrayColorAtPixelData(x, y, data); } canvas.getRgbColorAtPixel = function(x, y) { const color = canvas.getRgbaColorArrayAtPixel(x, y); return `rgb(${color[0]}, ${color[1]}, ${color[2]})`; } canvas.setColorAtPixel = function(x, y, color) { const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; canvas.setRgbaArrayColorAtPixelData(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() { if (!background) { canvas.remove(); } } canvas.add = function(canvas2) { canvas.ctx.drawImage(canvas2, 0, 0); } canvas.setWidth(width); canvas.setHeight(height); return canvas; } // }}} function makeLayer({height=600, width=800, background=undefined}) { // {{{ const layer = {} layer.canvas = makeCanvas({height, width, background}); layer.active = false; layer.opacity = 1; layer.background = background; layer.controllerElement = document.createElement('div'); layer.controllerElement.className = 'layer-controller'; layer.previewElement = document.createElement('img'); layer.previewElement.className = 'layer-preview'; layer.previewElement.src = layer.canvas.toDataURL(); layer.previewElement.addEventListener('click', () => { layers.setActive(layer); }); layer.controllerElement.appendChild(layer.previewElement); if (!layer.background) { layer.moveButtons = document.createElement('div'); layer.moveButtons.classList.add('button'); layer.moveButtons.classList.add('layer-move-buttons'); layer.moveButtons.className = 'layer-move-buttons'; layer.moveUpButton = document.createElement('div'); layer.moveUpButton.classList.add('button'); layer.moveUpButton.classList.add('layer-move-button'); layer.moveUpButton.innerHTML = ''; layer.moveUpButton.addEventListener('click', () => { layers.moveUp(layer); }); layer.moveButtons.appendChild(layer.moveUpButton); layer.moveDownButton = document.createElement('div'); layer.moveDownButton.classList.add('button'); layer.moveDownButton.classList.add('layer-move-button'); layer.moveDownButton.innerHTML = ''; layer.moveDownButton.addEventListener('click', () => { layers.moveDown(layer); }); layer.moveButtons.appendChild(layer.moveDownButton); layer.controllerElement.appendChild(layer.moveButtons); layer.mergeButtons = document.createElement('div'); layer.mergeButtons.classList.add('button'); layer.mergeButtons.classList.add('layer-merge-buttons'); layer.mergeButtons.className = 'layer-merge-buttons'; layer.mergeUpButton = document.createElement('div'); layer.mergeUpButton.classList.add('button'); layer.mergeUpButton.classList.add('layer-merge-button'); layer.mergeUpButton.innerHTML = ''; layer.mergeUpButton.addEventListener('click', () => { layers.mergeUp(layer); }); layer.mergeButtons.appendChild(layer.mergeUpButton); layer.controllerElement.appendChild(layer.mergeButtons); layer.deleteButton = document.createElement('div'); layer.deleteButton.classList.add('button'); layer.deleteButton.classList.add('layer-delete-button'); layer.deleteButton.innerHTML = ''; layer.deleteButton.addEventListener('click', () => { layers.delete(layer); }); layer.controllerElement.appendChild(layer.deleteButton); } layer.activate = function() { layer.active = true; layer.controllerElement.classList.add('active'); } layer.deactivate = function() { layer.active = false; layer.controllerElement.classList.remove('active'); } layer.refreshPreview = function() { layer.previewElement.src = layer.canvas.toDataURL(); } layer.changeOpacity = function(opacity) { console.log({opacity}); layer.opacity = opacity; layer.canvas.style.opacity = opacity } return layer; } // }}} function makeLayers({height=600, width=800, backgroundColor='rgb(255, 255, 255)'}) { // {{{ const layers = []; layers.height = height; layers.width = width; layers.addButton = document.createElement('div'); layers.addButton.classList.add('button'); layers.addButton.classList.add('layer-add-button'); layers.addButton.innerHTML = ''; layers.addButton.addEventListener('click', () => { layers.add(); }); layers.setHeight = function(height) { layers.height = height; layers.forEach(layer => layer.canvas.setHeight(height)); easelElement.style.height = height + 2 + 'px'; } layers.setHeight(height); layers.setWidth = function(width) { layers.width = width; layers.forEach(layer => layer.canvas.setWidth(width)); easelElement.style.width = width + 2 + 'px'; } layers.setWidth(width); layers.resize = function(width, height) { layers.height = height; layers.width = width; easelElement.style.height = height * zoom + 2 + 'px'; easelElement.style.width = width * zoom + 2 + 'px'; layers.forEach(layer => layer.canvas.resize(width, height)); layers[0].canvas.fill(backgroundColor); } layers.zoom = function(newZoom) { easelElement.style.height = layers.height * zoom + 2 + 'px'; easelElement.style.width = layers.width * zoom + 2 + 'px'; layers.forEach(layer => { layer.canvas.style.height = layers.height * zoom + 'px'; layer.canvas.style.width = layers.width * zoom + 'px'; }); } 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.refreshPreviews = function() { layers.forEach(layer => { layer.refreshPreview(); }); } layers.refresh = function() { layers.refreshControllers(); layers.refreshLayers(); layers.refreshPreviews(); } layers.add = function() { const layer = makeLayer({ height: layers.height, width: layers.width, }); layers.push(layer); layers.setActive(layer); layers.refresh(); } layers.delete = function(layer) { if (!layer.background) { layer.canvas.deleteCanvas(); layers.splice(layers.indexOf(layer), 1); layers.refresh(); } } layers.deleteAll = function() { layers.forEach(function(layer) { if (!layer.background) { layer.canvas.deleteCanvas(); layers.splice(layers.indexOf(layer), 1); } }); } layers.moveDown = function(layer) { if (layer.background) return; if (layers.indexOf(layer) === layers.length - 1) return; const index = layers.indexOf(layer); const temp = layers[index + 1]; layers[index + 1] = layer; layers[index] = temp; layers.refresh(); } layers.moveUp = function(layer) { if (layer.background) return; if (layers.indexOf(layer) === 1) return; const index = layers.indexOf(layer); const temp = layers[index - 1]; layers[index - 1] = layer; layers[index] = temp; layers.refresh(); } layers.mergeUp = function(layer) { if (layer.background) return; const index = layers.indexOf(layer); const belowLayer = layers[index - 1]; if (belowLayer.background) return; belowLayer.canvas.add(layer.canvas); layers.delete(layer); layers.setActive(belowLayer); } layers.mergeDown = function(layer) { if (layer.background) return; const index = layers.indexOf(layer); if (index === layers.length - 1) return; const aboveLayer = layers[index + 1]; aboveLayer.canvas.add(layer.canvas); layers.delete(layer); layers.setActive(aboveLayer); } layers.mergeAll = function() { const backgroundLayer = layers[0]; layers.forEach(layer => { if (layer !== backgroundLayer) { backgroundLayer.canvas.add(layer.canvas); } }); layers.deleteAll(); } layers.tempMergeAll = function() { const backgroundLayerCopy = makeLayer({ height: layers.height, width: layers.width, background: true, }); // backgroundLayerCopy.canvas.fill(backgroundColor); layers.forEach(layer => { if (!layer.background) { backgroundLayerCopy.canvas.add(layer.canvas); } }); return backgroundLayerCopy; } layers.setActive = function(layer) { layers.forEach(layer => layer.deactivate()); layer.activate(); } layers.getActive = function() { return layers.find(layer => layer.active); } layers.push(makeLayer({height, width, background: true})); layers[0].canvas.fill(backgroundColor); return layers; } // }}} // }}} const layers = makeLayers({height: initialHeight, width: initialWidth}); layers.add(); 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.toRgb(); commandBarElement.appendChild(colorPreview.element); colorPreview.update = function() { colorPreview.element.style.backgroundColor = brushColor.toRgb(); } return colorPreview; } const colorPreview = makeColorPreview(); // }}} // BRUSH PREVIEW {{{ function makeBrushPreview() { const brushPreview = {}; brushPreview.element = document.createElement('div'); brushPreview.element.id = 'brush-preview'; brushPreview.element.style.width = brushSize + 'px'; brushPreview.element.style.height = brushSize + 'px'; brushPreview.element.style.position = 'absolute'; brushPreview.element.style.display = 'none'; brushPreview.element.style.pointerEvents = 'none'; brushPreview.element.style.border = '1px solid black'; brushPreview.element.style.zIndex = '1000'; document.body.appendChild(brushPreview.element); brushPreview.update = function() { brushPreview.element.style.width = brushSize * zoom + 'px'; brushPreview.element.style.height = brushSize * zoom + 'px'; if (brushShape === 'circle') { brushPreview.element.style.borderRadius = '50%'; } else { brushPreview.element.style.borderRadius = '0'; } if (brushSize < 3) { brushPreview.element.style.visibility = 'hidden'; } else { brushPreview.element.style.visibility = 'visible'; } } brushPreview.setPosition = function(x, y) { brushPreview.element.style.left = x - brushSize * zoom / 2 + 'px'; brushPreview.element.style.top = y - brushSize * zoom / 2 + 'px'; } brushPreview.show = function() { brushPreview.element.style.display = 'block'; } brushPreview.hide = function() { brushPreview.element.style.display = 'none'; } brushPreview.update(); return brushPreview; } const brushPreview = makeBrushPreview(); // }}} // 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 mergedCanvas = layers.tempMergeAll().canvas; const link = document.createElement('a'); link.download = 'canvas.png'; link.href = mergedCanvas.toDataURL(); link.click(); } }); // }}} commands.add({ // import {{{ name: 'import', key: 'i', icon: '', func: function importCanvas() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = function(e) { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = function(e) { const dataURL = e.target.result; const img = new Image(); img.src = dataURL; img.onload = function() { layers.add(); const canvas = layers.getActive().canvas; canvas.fromDataUrl(dataURL, `${img.width}x${img.height}`); layers.setWidth(img.width); layers.setHeight(img.height); layers.refresh(); } } 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]; brushPreview.update(); } }); // }}} commands.add({ //reset {{{ name: 'reset', key: 'r', icon: '', func: function resetCanvas() { zoom = 1; layers.zoom(); layers.resetPosition(); } }); // }}} // }}} // TOOLS {{{ // FACTORY {{{ function makeTool({name, key, icon, mouseDown, mouseMove, mouseUp, mouseDrag, mouseLeave}) { 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.mouseDrag = mouseDrag; tool.mouseLeave = mouseLeave; 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, mouseDrag, mouseLeave}) { const tool = makeTool({name, key, icon, mouseDown, mouseMove, mouseUp, mouseDrag, mouseLeave}); 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, brushColor.toRgb()); } else { canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, brushColor.toRgb()); } }, mouseMove: function(e) { brushPreview.show(); brushPreview.setPosition(e.clientX, e.clientY); }, mouseDrag: function(e) { const canvas = layers.getActive().canvas; if (brushSize === 1) { canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushColor.toRgb()); } else { canvas.drawLineWithShape(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushShape, brushSize, brushColor.toRgb()); } canvasStartX = canvasEndX; canvasStartY = canvasEndY; }, mouseUp: function(e) { layers.getActive().refreshPreview(); }, mouseLeave: function(e) { brushPreview.hide(); } }); // }}} tools.add({ // content-move {{{ name: 'content-move', key: 'h', icon: '', mouseDown: function(e) { const canvas = layers.getActive().canvas; canvas.saveCanvas(); }, mouseDrag: function(e) { const canvas = layers.getActive().canvas; canvas.clearCanvas(); canvas.restoreCanvas(dX, dY); }, mouseUp: function(e) { layers.getActive().refreshPreview(); }, }); // }}} tools.add({ // move {{{ name: 'move', key: 'm', icon: '', mouseDown: function(e) { startX = e.clientX - easelElement.offsetLeft; startY = e.clientY - easelElement.offsetTop; }, mouseDrag: function(e) { easelElement.style.left = dX + 'px'; easelElement.style.top = dY + 'px'; }, cursor: 'fontawesome/png/arrows-alt-solid.png', }); // }}} tools.add({ // zoom {{{ name: 'zoom', key: 'z', icon: '', mouseDrag: function(e) { zoom += dX * dZoom; if (zoom < 0.1) zoom = 0.1; layers.zoom(); brushPreview.update(); 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.toRgbaArray()); }, mouseUp: function(e) { layers.getActive().refreshPreview(); } }); // }}} 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.fromRgb(pickedColor); colorPreview.update(); } }); // }}} tools.add({ // brush-size {{{ name: 'brush-size', key: 'd', icon: '', mouseMove: function(e) { brushPreview.show(); brushPreview.setPosition(e.clientX, e.clientY); }, mouseDrag: function(e) { brushSize += dX * dBrushSize; if (brushSize < 1) brushSize = 1; if (brushSize > maxBrushSize) brushSize = maxBrushSize; startX = endX; brushPreview.update(); }, mouseLeave: function(e) { brushPreview.hide(); } }); // }}} tools.add({ // resize {{{ name: 'resize', key: 'r', icon: '', mouseDrag: function(e) { let newWidth = layers.width + dX / zoom; let newHeight = layers.height + dY / zoom; layers.resize(newWidth, newHeight); startX = endX; startY = endY; }, mouseUp: function(e) { layers.refreshPreviews(); } }); // }}} tools.add({ // color-mix {{{ name: 'color-mix', key: 'x', icon: '', mouseDown: function(e) { tempColor.copy(canvasColor); startTime = Date.now(); interval = setInterval(() => { if (!tempColor.match(canvasColor)) { startTime = Date.now(); tempColor.copy(canvasColor); } if (!canvasColor.isOpaque()) { startTime = Date.now(); } else { const elapsedTime = Date.now() - startTime; const t = Math.min(1, elapsedTime / 10000); brushColor.mixx(canvasColor, t); colorPreview.update(); if (!isMouseDown) { clearInterval(interval); startTime = Date.now(); } } }, 50); }, mouseUp: function(e) { clearInterval(interval); }, mouseLeave: function(e) { clearInterval(interval); } }); // }}} // tools.add({ // opacity {{{ // name: 'opacity', // key: 'o', // icon: '', // mouseDrag: function(e) { // layer = layers.getActive(); // var opacity = layer.opacity += dX * dOpacity; // if (opacity < 0) opacity = 0; // if (opacity > 1) opacity = 1; // layer.changeOpacity(opacity); // startX = endX; // }, // }); // }}} // }}} // PUCKS {{{ // FACTORY {{{ function makePuck({puckColor, key}) { 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 (key) { puck.key = key; const keyHint = document.createElement('div'); keyHint.className = 'key-hint'; keyHint.innerHTML = key; puck.element.appendChild(keyHint); } puck.element.addEventListener('mousedown', (e) => { const startTime = Date.now(); interval = setInterval(() => { const elapsedTime = Date.now() - startTime; const t = Math.min(1, elapsedTime / 10000); brushColor.mixxRgb(puck.element.style.backgroundColor, t); colorPreview.update(); }, 50); }); puck.element.addEventListener('mouseup', (e) => { clearInterval(interval); }); puck.element.addEventListener('mouseleave', (e) => { clearInterval(interval); }); puck.keydown = function(e) { startTime = Date.now(); var interval = setInterval(() => { const elapsedTime = Date.now() - startTime; const t = Math.min(1, elapsedTime / 10000); brushColor.mixxRgb(puck.element.style.backgroundColor, t); colorPreview.update(); }, 50); 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}) { const puck = makePuck({puckColor, key}); pucks.push(puck); } return pucks; } // }}} const pucks = makePucks(); pucks.add({ // black puckColor: 'rgb(0, 0, 0)', }); pucks.add({ // white puckColor: 'rgb(255, 255, 255)', }); pucks.add({ // Cadmium Yellow puckColor: 'rgb(254, 236, 0)', }); pucks.add({ // Hansa Yellow puckColor: 'rgb(252, 211, 0)', }); pucks.add({ // Cadmium Orange puckColor: 'rgb(255, 105, 0)', }); pucks.add({ // Cadmium Red puckColor: 'rgb(255, 39, 2)', }); pucks.add({ // Quinacridone Magenta puckColor: 'rgb(128, 2, 46)', }); pucks.add({ // Cobalt Violet puckColor: 'rgb(78, 0, 66)', }); pucks.add({ // Ultramarine Blue puckColor: 'rgb(25, 0, 89)', }); pucks.add({ // Cobalt Blue puckColor: 'rgb(0, 33, 133)', }); pucks.add({ // Phthalo Blue puckColor: 'rgb(13, 27, 68)', }); pucks.add({ // Phthalo Green puckColor: 'rgb(0, 60, 50)', }); pucks.add({ // Permanent Green puckColor: 'rgb(7, 109, 22)', }); pucks.add({ // Sap Green puckColor: 'rgb(107, 148, 4)', }); pucks.add({ // Burnt Sienna puckColor: 'rgb(123, 72, 0)', }); pucks.add({ // red puckColor: 'rgb(255, 0, 0)', }); pucks.add({ // green puckColor: 'rgb(0, 255, 0)', }); pucks.add({ // blue puckColor: 'rgb(0, 0, 255)', }); pucks.add({ // cyan puckColor: 'rgb(0, 255, 255)', }); pucks.add({ // yellow puckColor: 'rgb(255, 255, 0)', }); pucks.add({ // magenta puckColor: 'rgb(255, 0, 255)', }); // }}} // 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 brushColor.toRgb(); } }); 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; } }); infos.add({ name: 'cursor-color', updateFunction: function() { return canvasColor; } }); // }}} // 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; canvasEndX = canvas.getPositionOnCanvas(e).x; canvasEndX = canvas.getPositionOnCanvas(e).y; canvasColor.fromRgbaArray(canvas.getRgbaColorArrayAtPixel(canvasStartX, canvasStartY)); for (var i = 0; i < tools.length; i++) { var tool = tools[i]; if (tool.active) { if (tool.mouseDown) { tool.mouseDown(e); } } } 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; canvasColor.fromRgbaArray(canvas.getRgbaColorArrayAtPixel(canvasEndX, canvasEndY)); for (var i = 0; i < tools.length; i++) { var tool = tools[i]; if (tool.active) { if (tool.mouseMove) { tool.mouseMove(e); } } } if (isMouseDown) { for (var i = 0; i < tools.length; i++) { var tool = tools[i]; if (tool.active) { if (tool.mouseDrag) { tool.mouseDrag(e); } } } } infos.update(); }); 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; } } } infos.update(); }); // }}} // KEYBINDINGS {{{ document.addEventListener('keydown', (e) => { if (isKeyDown) return; 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');