|
|
@ -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: '<i class="fa-solid fa-fill"></i>', |
|
|
|
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: '<i class="fa-solid fa-mortar-pestle"></i>', |
|
|
|
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; |
|
|
|
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); |
|
|
|
} |
|
|
|
|
|
|
|
}); // }}}
|
|
|
|
|
|
|
|
// }}}
|
|
|
@ -901,39 +963,33 @@ 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() { |
|
|
|
puck.element.addEventListener('mouseup', (e) => { |
|
|
|
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); |
|
|
|
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(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|