7 changed files with 1158 additions and 1047 deletions
@ -1,25 +0,0 @@ |
|||||
.puck { |
|
||||
width: 100px; |
|
||||
height: 120px; |
|
||||
padding: 2px; |
|
||||
background-color: #ddd; |
|
||||
border: 1px solid; |
|
||||
position: absolute; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
} |
|
||||
|
|
||||
.well { |
|
||||
width: 100%; |
|
||||
flex-grow: 1; |
|
||||
border: 1px solid; |
|
||||
} |
|
||||
|
|
||||
.puck-menu { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
padding-top: 2px; |
|
||||
} |
|
||||
|
|
||||
|
|
@ -1,124 +1,724 @@ |
|||||
function createPuck(c) { |
const colorPreview = document.createElement('div'); |
||||
const puck = document.createElement('div'); |
colorPreview.id = 'color-preview'; |
||||
puck.className = 'puck'; |
colorPreview.className = 'puck'; |
||||
|
colorPreview.style.backgroundColor = color; |
||||
|
|
||||
const well = document.createElement('div'); |
menuBar.appendChild(colorPreview); |
||||
well.className = 'well'; |
|
||||
well.style.backgroundColor = c; |
|
||||
puck.appendChild(well); |
|
||||
|
|
||||
const puckMenu = document.createElement('div'); |
|
||||
puckMenu.className = 'puck-menu'; |
|
||||
puck.appendChild(puckMenu); |
|
||||
|
|
||||
const selectHandle = document.createElement('div'); |
// }}}
|
||||
selectHandle.className = 'select-handle'; |
|
||||
selectHandle.innerHTML = '<i class="fa-solid fa-droplet"></i>'; |
|
||||
puckMenu.appendChild(selectHandle); |
|
||||
|
|
||||
const copyHandle = document.createElement('div'); |
// helpers {{{
|
||||
copyHandle.className = 'copy-handle'; |
|
||||
copyHandle.innerHTML = '<i class="fa-regular fa-copy"></i>'; |
|
||||
puckMenu.appendChild(copyHandle); |
|
||||
|
|
||||
const deleteHandle = document.createElement('div'); |
|
||||
deleteHandle.className = 'delete-handle'; |
|
||||
deleteHandle.innerHTML = '<i class="fa-solid fa-trash-can"></i>'; |
|
||||
puckMenu.appendChild(deleteHandle); |
|
||||
|
|
||||
const updateHandle = document.createElement('div'); |
function saveCanvas() { |
||||
updateHandle.className = 'update-handle'; |
const dataURL = canvas.toDataURL(); |
||||
updateHandle.innerHTML = '<i class="fa-solid fa-fill"></i>'; |
const dimensions = `${canvas.width}x${canvas.height}`; |
||||
puckMenu.appendChild(updateHandle); |
localStorage.setItem('mixxCanvas', dataURL); |
||||
|
localStorage.setItem('mixxDimensions', dimensions); |
||||
|
console.log('Canvas saved'); |
||||
|
} |
||||
|
|
||||
|
function loadCanvas() { |
||||
|
const dataURL = localStorage.getItem('mixxCanvas'); |
||||
|
const dimensions = localStorage.getItem('mixxDimensions'); |
||||
|
if (dataURL && dimensions) { |
||||
|
const img = new Image(); |
||||
|
img.src = dataURL; |
||||
|
img.onload = function() { |
||||
|
canvas.width = dimensions.split('x')[0]; |
||||
|
canvas.height = dimensions.split('x')[1]; |
||||
|
canvas.style.width = canvas.width * zoom + 'px'; |
||||
|
canvas.style.height = canvas.height * zoom + 'px'; |
||||
|
canvasWidth = canvas.width; |
||||
|
canvasHeight = canvas.height; |
||||
|
ctx.drawImage(img, 0, 0); |
||||
|
} |
||||
|
} else { |
||||
|
console.log('No saved canvas found'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
const dragHandle = document.createElement('div'); |
function clearCanvasFromLocalStorage() { |
||||
dragHandle.className = 'drag-handle'; |
localStorage.removeItem('savedCanvas'); |
||||
dragHandle.innerHTML = '<i class="fa-solid fa-bars"></i>'; |
localStorage.removeItem('canvasDimensions'); |
||||
puckMenu.appendChild(dragHandle); |
} |
||||
|
|
||||
well.addEventListener('mousedown', (e) => { |
function saveState() { |
||||
let isMixing = true; |
if (undoStack.length >= maxHistory) { |
||||
let startX = e.clientX; |
undoStack.shift(); // Remove the oldest state if the stack exceeds the limit
|
||||
let startY = e.clientY; |
} |
||||
|
|
||||
document.addEventListener('mousemove', onMouseMove); |
undoStack.push({ |
||||
document.addEventListener('mouseup', onMouseUp); |
imageData: canvas.toDataURL(), |
||||
|
width: canvas.width, |
||||
|
height: canvas.height |
||||
|
}); |
||||
|
|
||||
function onMouseMove(e) { |
redoStack = []; // Clear the redo stack whenever a new action is performed
|
||||
if (isMixing) { |
} |
||||
const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); |
|
||||
|
function undo() { |
||||
|
if (undoStack.length > 0) { |
||||
|
const currentState = { |
||||
|
imageData: canvas.toDataURL(), |
||||
|
width: canvas.width, |
||||
|
height: canvas.height |
||||
|
}; |
||||
|
|
||||
|
redoStack.push(currentState); // Save current state to the redo stack
|
||||
|
const lastState = undoStack.pop(); // Get the last state from the undo stack
|
||||
|
|
||||
|
canvas.width = lastState.width; |
||||
|
canvas.height = lastState.height; |
||||
|
canvas.style.width = canvas.width * zoom + 'px'; |
||||
|
canvas.style.height = canvas.height * zoom + 'px'; |
||||
|
canvasWidth = canvas.width; |
||||
|
canvasHeight = canvas.height; |
||||
|
|
||||
|
const img = new Image(); |
||||
|
img.src = lastState.imageData; |
||||
|
img.onload = function() { |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.drawImage(img, 0, 0); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function redo() { |
||||
|
if (redoStack.length > 0) { |
||||
|
const currentState = { |
||||
|
imageData: canvas.toDataURL(), |
||||
|
width: canvas.width, |
||||
|
height: canvas.height |
||||
|
}; |
||||
|
|
||||
|
undoStack.push(currentState); // Save current state to the undo stack
|
||||
|
const nextState = redoStack.pop(); // Get the last state from the redo stack
|
||||
|
|
||||
|
canvas.width = nextState.width; |
||||
|
canvas.height = nextState.height; |
||||
|
canvas.style.width = canvas.width * zoom + 'px'; |
||||
|
canvas.style.height = canvas.height * zoom + 'px'; |
||||
|
canvasWidth = canvas.width; |
||||
|
canvasHeight = canvas.height; |
||||
|
|
||||
|
const img = new Image(); |
||||
|
img.src = nextState.imageData; |
||||
|
img.onload = function() { |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.drawImage(img, 0, 0); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getPositionOnCanvas(e) { |
||||
|
const rect = canvas.getBoundingClientRect(); |
||||
|
return { |
||||
|
x: Math.round((e.clientX - rect.left) / zoom), |
||||
|
y: Math.round((e.clientY - rect.top) / zoom), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function drawCircle(x, y) { |
||||
|
ctx.beginPath(); |
||||
|
ctx.arc(x, y, brushSize / 2, 0, 2 * Math.PI, false); |
||||
|
ctx.fillStyle = color; |
||||
|
ctx.fill(); |
||||
|
} |
||||
|
|
||||
|
function drawLineWithCircles(x1, y1, x2, y2) { |
||||
|
const dx = x2 - x1; |
||||
|
const dy = y2 - y1; |
||||
|
const distance = Math.sqrt(dx * dx + dy * dy); |
||||
|
const steps = Math.ceil(distance / (brushSize / 5)); |
||||
|
|
||||
|
for (let i = 0; i <= steps; i++) { |
||||
|
const x = x1 + (dx * i) / steps; |
||||
|
const y = y1 + (dy * i) / steps; |
||||
|
drawCircle(x, y); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function saveCanvasContents() { |
||||
|
tempCanvas = document.createElement('canvas'); |
||||
|
tempCanvas.width = canvas.width; |
||||
|
tempCanvas.height = canvas.height; |
||||
|
const tempCtx = tempCanvas.getContext('2d'); |
||||
|
tempCtx.drawImage(canvas, 0, 0); |
||||
|
} |
||||
|
|
||||
|
function updateColorPreview() { |
||||
|
colorPreview.style.backgroundColor = color; |
||||
|
} |
||||
|
|
||||
|
function hexToRgbArray(hex) { |
||||
|
if (hex.startsWith('#')) { |
||||
|
hex = hex.slice(1); |
||||
|
} |
||||
|
|
||||
|
if (hex.length === 3) { |
||||
|
hex = hex.split('').map(char => char + char).join(''); |
||||
|
} |
||||
|
|
||||
|
const bigint = parseInt(hex, 16); |
||||
|
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; |
||||
|
} |
||||
|
|
||||
|
function floodFill(x, y, fillColor) { |
||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
||||
|
const data = imageData.data; |
||||
|
|
||||
|
const targetColor = getColorAtPixel(data, x, y); |
||||
|
const fillColorArray = hexToRgbArray(fillColor); |
||||
|
|
||||
|
if (colorsMatch(targetColor, fillColorArray)) { |
||||
|
return; // The clicked point is already the fill color
|
||||
|
} |
||||
|
|
||||
|
const stack = [{x, y}]; |
||||
|
|
||||
|
while (stack.length > 0) { |
||||
|
const {x, y} = stack.pop(); |
||||
|
const currentColor = getColorAtPixel(data, x, y); |
||||
|
|
||||
|
if (colorsMatch(currentColor, targetColor)) { |
||||
|
setColorAtPixel(data, x, y, fillColorArray); |
||||
|
|
||||
|
if (x > 0) stack.push({x: x - 1, y}); |
||||
|
if (x < canvas.width - 1) stack.push({x: x + 1, y}); |
||||
|
if (y > 0) stack.push({x, y: y - 1}); |
||||
|
if (y < canvas.height - 1) stack.push({x, y: y + 1}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ctx.putImageData(imageData, 0, 0); |
||||
|
} |
||||
|
|
||||
|
function getColorAtPixel(data, x, y) { |
||||
|
const index = (y * canvas.width + x) * 4; |
||||
|
return [data[index], data[index + 1], data[index + 2], data[index + 3]]; |
||||
|
} |
||||
|
|
||||
|
function setColorAtPixel(data, x, y, color) { |
||||
|
const index = (y * canvas.width + x) * 4; |
||||
|
data[index] = color[0]; |
||||
|
data[index + 1] = color[1]; |
||||
|
data[index + 2] = color[2]; |
||||
|
data[index + 3] = 255; // Set alpha to fully opaque
|
||||
|
} |
||||
|
|
||||
|
function colorsMatch(a, b) { |
||||
|
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; |
||||
|
} |
||||
|
|
||||
|
// }}}
|
||||
|
|
||||
|
// mousedown {{{
|
||||
|
|
||||
|
canvasArea.addEventListener('mousedown', (e) => { |
||||
|
if (e.target.closest('.puck')) return; |
||||
|
|
||||
|
startX = e.clientX; |
||||
|
startY = e.clientY; |
||||
|
canvasStartX = getPositionOnCanvas(e).x; |
||||
|
canvasStartY = getPositionOnCanvas(e).y; |
||||
|
saveCanvasContents(); |
||||
|
isMouseDown = true; |
||||
|
|
||||
|
if ( |
||||
|
tool === 'brush' || |
||||
|
tool === 'content-move' || |
||||
|
tool === 'resize' || |
||||
|
tool === 'zoom' || |
||||
|
tool === 'bucket-fill' |
||||
|
) { |
||||
|
saveState(); |
||||
|
} |
||||
|
|
||||
|
if (tool === 'brush') { |
||||
|
console.log('brush'); |
||||
|
drawCircle(canvasStartX, canvasStartY); |
||||
|
} else if (tool === 'bucket-fill') { |
||||
|
floodFill(canvasStartX, canvasStartY, color); |
||||
|
return; |
||||
|
} else if (tool === 'move') { |
||||
|
startX = e.clientX - canvasContainer.offsetLeft; |
||||
|
startY = e.clientY - canvasContainer.offsetTop; |
||||
|
} else if (tool === 'color-picker') { |
||||
|
const imageData = ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data; |
||||
|
const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; |
||||
|
color = pickedColor; |
||||
|
console.log('Picked Color:', pickedColor); |
||||
|
updateColorPreview(); |
||||
|
return; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// }}}
|
||||
|
|
||||
|
// mousemove {{{
|
||||
|
|
||||
|
canvasArea.addEventListener('mousemove', (e) => { |
||||
|
|
||||
|
endX = e.clientX; |
||||
|
endY = e.clientY; |
||||
|
dX = endX - startX; |
||||
|
dY = endY - startY; |
||||
|
|
||||
|
canvasEndX = getPositionOnCanvas(e).x; |
||||
|
canvasEndY = getPositionOnCanvas(e).y; |
||||
|
canvasDX = canvasEndX - canvasStartX; |
||||
|
canvasDY = canvasEndY - canvasStartY; |
||||
|
|
||||
|
if (tool == 'brush-size') { |
||||
|
brushPreview.style.display = 'block'; |
||||
|
brushPreview.style.width = brushSize + 'px'; |
||||
|
brushPreview.style.height = brushSize + 'px'; |
||||
|
brushPreview.style.left = e.clientX - brushSize / 2 + 'px'; |
||||
|
brushPreview.style.top = e.clientY - brushSize / 2 + 'px'; |
||||
|
} |
||||
|
|
||||
|
if (isMouseDown) { |
||||
|
|
||||
|
if (tool === 'brush-size') { |
||||
|
brushSize += dX * dBrushSize; |
||||
|
if (brushSize < 1) brushSize = 1; |
||||
|
if (brushSize > maxBrushSize) brushSize = maxBrushSize; |
||||
|
startX = endX; |
||||
|
} else if (tool === 'brush') { |
||||
|
drawLineWithCircles(canvasStartX, canvasStartY, canvasEndX, canvasEndY); |
||||
|
|
||||
|
canvasStartX = canvasEndX; |
||||
|
canvasStartY = canvasEndY; |
||||
|
|
||||
|
} else if (tool === 'content-move') { |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.fillStyle = backgroundColor; |
||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.drawImage(tempCanvas, dX, dY); |
||||
|
} else if (tool === 'move') { |
||||
|
canvasContainer.style.left = dX + 'px'; |
||||
|
canvasContainer.style.top = dY + 'px'; |
||||
|
} else if (tool === 'zoom') { |
||||
|
zoom += dX * dZoom; |
||||
|
if (zoom < 0.1) zoom = 0.1; |
||||
|
canvas.style.height = canvasHeight * zoom + 'px'; |
||||
|
canvas.style.width = canvasWidth * zoom + 'px'; |
||||
|
startX = endX; |
||||
|
} else if (tool === 'resize') { |
||||
|
let newWidth = canvasWidth + dX / zoom; |
||||
|
let newHeight = canvasHeight + dY / zoom; |
||||
|
if (newWidth > 0 && newHeight > 0) { |
||||
|
canvas.width = newWidth; |
||||
|
canvas.height = newHeight; |
||||
|
canvas.style.width = newWidth * zoom + 'px'; |
||||
|
canvas.style.height = newHeight * zoom + 'px'; |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.fillStyle = backgroundColor; |
||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.drawImage(tempCanvas, 0, 0); |
||||
|
} |
||||
|
} else if (tool === 'color-mix') { |
||||
|
|
||||
|
const imageData = ctx.getImageData(canvasEndX, canvasEndY, 1, 1).data; |
||||
|
const canvasColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; |
||||
|
|
||||
|
const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); |
||||
const t = Math.min(1, distance / 300); |
const t = Math.min(1, distance / 300); |
||||
|
|
||||
const mixedColor = mixbox.lerp(color, well.style.backgroundColor, t); |
const mixedColor = mixbox.lerp(color, canvasColor, t); |
||||
|
|
||||
color = mixedColor; |
color = mixedColor; |
||||
|
|
||||
startX = e.clientX; |
startX = e.clientX; |
||||
startY = e.clientY; |
startY = e.clientY; |
||||
} |
|
||||
} |
} |
||||
|
|
||||
function onMouseUp() { |
|
||||
isMixing = false; |
|
||||
document.removeEventListener('mousemove', onMouseMove); |
|
||||
document.removeEventListener('mouseup', onMouseUp); |
|
||||
} |
} |
||||
|
|
||||
|
updateInfos(); |
||||
|
updateColorPreview(); |
||||
}); |
}); |
||||
|
|
||||
dragHandle.addEventListener('mousedown', (e) => { |
// }}}
|
||||
let isMovingPuck = true; |
|
||||
let startX = e.clientX; |
// mouseup {{{
|
||||
let startY = e.clientY; |
|
||||
let left = puck.offsetLeft; |
canvasArea.addEventListener('mouseup', (e) => { |
||||
let top = puck.offsetTop; |
isMouseDown = false; |
||||
document.addEventListener('mousemove', (e) => { |
if (tool === 'brush') { |
||||
if (isMovingPuck) { |
ctx.closePath(); |
||||
puck.style.left = left + e.clientX - startX + 'px'; |
} else if (tool === 'resize') { |
||||
puck.style.top = top + e.clientY - startY + 'px'; |
canvasWidth = canvas.width; |
||||
|
canvasHeight = canvas.height; |
||||
} |
} |
||||
|
|
||||
|
updateColorPreview(); |
||||
}); |
}); |
||||
document.addEventListener('mouseup', () => { |
|
||||
isMovingPuck = false; |
// }}}
|
||||
document.removeEventListener('mousemove', () => {}); |
|
||||
document.removeEventListener('mouseup', () => {}); |
// mouseleave {{{
|
||||
|
|
||||
|
canvasArea.addEventListener('mouseleave', (e) => { |
||||
|
isMouseDown = false; |
||||
|
brushPreview.style.display = 'none'; |
||||
}); |
}); |
||||
|
|
||||
|
// }}}
|
||||
|
|
||||
|
// keybindings {{{
|
||||
|
|
||||
|
const toolKeyBindings = {} |
||||
|
|
||||
|
const functionKeyBindings = { |
||||
|
} |
||||
|
|
||||
|
document.addEventListener('keydown', (e) => { |
||||
|
if (keyDown) return; |
||||
|
|
||||
|
const newTool = toolKeyBindings[e.key.toLowerCase()] |
||||
|
if (newTool) { |
||||
|
prevTool = tool; |
||||
|
keyDown = true; |
||||
|
changeTool(newTool); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const func = functionKeyBindings[e.key]; |
||||
|
if (func) { |
||||
|
func(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
}); |
}); |
||||
|
|
||||
updateHandle.addEventListener('click', () => { |
document.addEventListener('keyup', (e) => { |
||||
console.log('update'); |
const currentTool = toolKeyBindings[e.key.toLowerCase()] |
||||
well.style.backgroundColor = color; |
if (currentTool) { |
||||
|
keyDown = false; |
||||
|
if (e.key == e.key.toLowerCase()) { |
||||
|
changeTool(prevTool); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
}); |
}); |
||||
|
|
||||
selectHandle.addEventListener('click', () => { |
// }}}
|
||||
console.log('select'); |
|
||||
color = well.style.backgroundColor; |
// tools {{{
|
||||
console.log(color); |
|
||||
updateColorPreview(); |
var toolButtons = []; |
||||
|
|
||||
|
function changeTool(toolName) { |
||||
|
toolButtons.forEach(button => button.button.classList.remove('active')); |
||||
|
toolButtons.find(button => button.name === toolName).button.classList.add('active'); |
||||
|
tool = toolName; |
||||
|
brushPreview.style.display = 'none'; |
||||
|
updateInfos(); |
||||
|
} |
||||
|
|
||||
|
function createToolButton(toolName, displayName, icon, key=undefined) { |
||||
|
const button = document.createElement('div'); |
||||
|
button.classList.add('button'); |
||||
|
button.classList.add('tool'); |
||||
|
button.innerHTML = icon; |
||||
|
button.title = displayName; |
||||
|
button.addEventListener('click', () => { |
||||
|
changeTool(toolName); |
||||
}); |
}); |
||||
|
|
||||
copyHandle.addEventListener('click', () => { |
if (key) { |
||||
pucks.push(createPuck(well.style.backgroundColor)); |
const keyHint = document.createElement('span'); |
||||
|
keyHint.className = 'key-hint'; |
||||
|
keyHint.innerHTML = key; |
||||
|
button.appendChild(keyHint); |
||||
|
toolKeyBindings[key] = toolName; |
||||
|
} |
||||
|
|
||||
|
toolBar.appendChild(button); |
||||
|
toolButtons.push({'name': toolName, 'button': button}); |
||||
|
return button; |
||||
|
} |
||||
|
|
||||
|
createToolButton('brush', 'Brush', '<i class="fa-solid fa-paintbrush"></i>', 'b'); |
||||
|
createToolButton('content-move', 'Move Content', '<i class="fa-regular fa-hand"></i>', 'h'); |
||||
|
createToolButton('move', 'Move Canvas', '<i class="fa-solid fa-arrows-up-down-left-right"></i>', 'm'); |
||||
|
createToolButton('zoom', 'Zoom', '<i class="fa-solid fa-magnifying-glass"></i>', 'z'); |
||||
|
createToolButton('resize', 'Resize', '<i class="fa-solid fa-ruler-combined"></i>', 'r'); |
||||
|
createToolButton('color-picker', 'Color Picker', '<i class="fa-solid fa-eye-dropper"></i>', 'a'); |
||||
|
createToolButton('color-mix', 'Color Mix', '<i class="fa-solid fa-mortar-pestle"></i>', 'x'); |
||||
|
createToolButton('brush-size', 'Brush Size', '<i class="fa-regular fa-circle-dot"></i>', 'd'); |
||||
|
createToolButton('bucket-fill', 'Bucket Fill', '<i class="fa-solid fa-fill"></i>', 'k'); |
||||
|
|
||||
|
// }}}
|
||||
|
|
||||
|
// menu functons {{{
|
||||
|
|
||||
|
function flipCanvasHorizontally(e) { |
||||
|
saveState(); |
||||
|
ctx.save(); |
||||
|
saveCanvasContents(); |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.scale(-1, 1); |
||||
|
ctx.translate(-canvas.width, 0); |
||||
|
ctx.drawImage(tempCanvas, 0, 0); |
||||
|
ctx.restore(); |
||||
|
} |
||||
|
|
||||
|
function flipCanvasVertically(e) { |
||||
|
saveState(); |
||||
|
ctx.save(); |
||||
|
saveCanvasContents(); |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.scale(1, -1); |
||||
|
ctx.translate(0, -canvas.height); |
||||
|
ctx.drawImage(tempCanvas, 0, 0); |
||||
|
ctx.restore(); |
||||
|
} |
||||
|
|
||||
|
function exportCanvas(e) { |
||||
|
const link = document.createElement('a'); |
||||
|
link.download = 'canvas.png'; |
||||
|
link.href = canvas.toDataURL(); |
||||
|
link.click(); |
||||
|
} |
||||
|
|
||||
|
function importCanvas(e) { |
||||
|
const input = document.createElement('input'); |
||||
|
input.type = 'file'; |
||||
|
input.accept = 'image/*'; |
||||
|
input.onchange = (e) => { |
||||
|
const file = e.target.files[0]; |
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
const img = new Image(); |
||||
|
img.onload = () => { |
||||
|
canvas.width = img.width; |
||||
|
canvas.height = img.height; |
||||
|
ctx.drawImage(img, 0, 0); |
||||
|
} |
||||
|
img.src = e.target.result; |
||||
|
} |
||||
|
reader.readAsDataURL(file); |
||||
|
} |
||||
|
input.click(); |
||||
|
} |
||||
|
|
||||
|
function clearCanvas(e) { |
||||
|
saveState(); |
||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
ctx.fillStyle = backgroundColor; |
||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
||||
|
} |
||||
|
|
||||
|
function resetZoom(e) { |
||||
|
zoom = 1; |
||||
|
canvas.style.width = canvas.width * zoom + 'px'; |
||||
|
canvas.style.height = canvas.height * zoom + 'px'; |
||||
|
canvasWidth = canvas.width; |
||||
|
canvasHeight = canvas.height; |
||||
|
|
||||
|
canvasAreaRect = canvasArea.getBoundingClientRect(); |
||||
|
|
||||
|
canvasContainer.style.left = `${canvasAreaRect.left}px`; |
||||
|
canvasContainer.style.top = `${canvasAreaRect.top}px`; |
||||
|
} |
||||
|
|
||||
|
// }}}
|
||||
|
|
||||
|
// menu {{{
|
||||
|
|
||||
|
var menuButtons = []; |
||||
|
|
||||
|
function createMenuButton(icon, name, clickFunction, key=undefined) { |
||||
|
const button = document.createElement('div'); |
||||
|
button.className = 'button'; |
||||
|
button.innerHTML = icon; |
||||
|
button.title = name; |
||||
|
if (clickFunction) { |
||||
|
button.addEventListener('click', () => { |
||||
|
clickFunction() |
||||
|
updateInfos(); |
||||
}); |
}); |
||||
|
} |
||||
|
menuBar.appendChild(button); |
||||
|
if (key) { |
||||
|
const keyHint = document.createElement('span'); |
||||
|
keyHint.className = 'key-hint'; |
||||
|
keyHint.innerHTML = key; |
||||
|
button.appendChild(keyHint); |
||||
|
functionKeyBindings[key] = clickFunction; |
||||
|
} |
||||
|
return button; |
||||
|
} |
||||
|
|
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-download"></i>', 'Save', saveCanvas, 's')); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-upload"></i>', 'Load', loadCanvas)); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-trash-can"></i>', 'Clear', clearCanvas)); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-floppy-disk"></i>', 'Export', exportCanvas)); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-regular fa-folder-open"></i>', 'Import', importCanvas)); |
||||
|
|
||||
|
|
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-left-right"></i>', 'Flip Horizontally', flipCanvasHorizontally, 'f')); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-up-down"></i>', 'Flip Vertically', flipCanvasVertically, 'v')); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-undo"></i>', 'Undo', undo, 'u')); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-redo"></i>', 'Redo', redo, 'y')); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-pump-soap"></i>', 'Clear', clearCanvas, 'c')); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-house"></i>', 'Reset', resetZoom, 't')); |
||||
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-plus"></i>', 'Add Color', createPuck)); |
||||
|
|
||||
|
// }}}
|
||||
|
|
||||
|
// pucks {{{
|
||||
|
|
||||
|
function createPuck(c, editable=true, key=undefined) { |
||||
|
if (c === undefined) { |
||||
|
c = color; |
||||
|
} |
||||
|
|
||||
|
const puck = document.createElement('div'); |
||||
|
puck.className = 'puck'; |
||||
|
puck.style.backgroundColor = c; |
||||
|
|
||||
|
// const selectHandle = document.createElement('div');
|
||||
|
// selectHandle.className = 'select-handle';
|
||||
|
// selectHandle.innerHTML = '<i class="fa-solid fa-droplet"></i>';
|
||||
|
// puck.appendChild(selectHandle);
|
||||
|
|
||||
|
// selectHandle.addEventListener('click', () => {
|
||||
|
// color = puck.style.backgroundColor;
|
||||
|
// updateColorPreview();
|
||||
|
// updateInfos();
|
||||
|
// });
|
||||
|
|
||||
|
if (editable) { |
||||
|
// const updateHandle = document.createElement('div');
|
||||
|
// updateHandle.className = 'update-handle';
|
||||
|
// updateHandle.innerHTML = '<i class="fa-solid fa-fill"></i>';
|
||||
|
// puck.appendChild(updateHandle);
|
||||
|
|
||||
|
// updateHandle.addEventListener('click', () => {
|
||||
|
// puck.style.backgroundColor = color;
|
||||
|
// });
|
||||
|
|
||||
|
const deleteHandle = document.createElement('div'); |
||||
|
deleteHandle.className = 'delete-handle'; |
||||
|
deleteHandle.innerHTML = '<i class="fa-solid fa-trash-can"></i>'; |
||||
|
puck.appendChild(deleteHandle); |
||||
|
|
||||
deleteHandle.addEventListener('click', () => { |
deleteHandle.addEventListener('click', () => { |
||||
pucks = pucks.filter(p => p !== puck); |
console.log("test"); |
||||
puck.remove(); |
puck.remove(); |
||||
}); |
}); |
||||
|
} |
||||
|
|
||||
|
if (key) { |
||||
|
const keyHint = document.createElement('div'); |
||||
|
keyHint.className = 'key-hint'; |
||||
|
keyHint.innerHTML = key; |
||||
|
puck.appendChild(keyHint); |
||||
|
} |
||||
|
|
||||
|
function mixx(startTime) { |
||||
|
var interval = setInterval(() => { |
||||
|
const elapsedTime = Date.now() - startTime; |
||||
|
const t = Math.min(1, elapsedTime / 10000); |
||||
|
const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t); |
||||
|
color = mixedColor; |
||||
|
updateColorPreview(); |
||||
|
updateInfos(); |
||||
|
}, 50); |
||||
|
return interval; |
||||
|
} |
||||
|
|
||||
|
puck.addEventListener('mousedown', (e) => { |
||||
|
const startTime = Date.now(); |
||||
|
var interval = mixx(startTime); |
||||
|
function onMouseUp() { |
||||
|
clearInterval(interval); |
||||
|
document.removeEventListener('mouseup', onMouseUp); |
||||
|
} |
||||
|
document.addEventListener('mouseup', onMouseUp); |
||||
|
}); |
||||
|
|
||||
|
document.addEventListener('keydown', (e) => { |
||||
|
if (e.key == key) { |
||||
|
console.log(e.key); |
||||
|
const startTime = Date.now(); |
||||
|
var interval = mixx(startTime); |
||||
|
function onKeyUp() { |
||||
|
clearInterval(interval); |
||||
|
document.removeEventListener('keyup', onKeyUp); |
||||
|
} |
||||
|
document.addEventListener('keyup', onKeyUp); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
menuBar.appendChild(puck); |
||||
|
} |
||||
|
|
||||
|
createPuck(c='rgb(0, 0, 0)', editable=false, key='1'); |
||||
|
createPuck(c='rgb(255, 0, 0)', editale=false, key='2'); |
||||
|
createPuck(c='rgb(0, 0, 255)', editale=false, key='3'); |
||||
|
createPuck(c='rgb(255, 255, 0)', editale=false, key='4'); |
||||
|
createPuck(c='rgb(99, 60, 22)', editale=false, key='5'); |
||||
|
createPuck(c='rgb(0, 255, 0)', editale=false, key='6'); |
||||
|
createPuck(c='rgb(255, 0, 255)', editale=false, key='7'); |
||||
|
createPuck(c='rgb(0, 255, 255)', editale=false, key='8'); |
||||
|
createPuck(c='rgb(255, 255, 255)', editale=false, key='9'); |
||||
|
|
||||
|
|
||||
canvasArea.appendChild(puck); |
// }}}
|
||||
|
|
||||
let canvasWidth = canvasArea.offsetWidth; |
// info {{{
|
||||
let canvasHeight = canvasArea.offsetHeight; |
|
||||
|
|
||||
let randonX = Math.floor(Math.random() * canvasWidth); |
var infos = []; |
||||
let randonY = Math.floor(Math.random() * canvasHeight); |
|
||||
|
|
||||
puck.style.left = randonX + 'px'; |
function createInfo(name, updateFunction) { |
||||
puck.style.top = randonY + 'px'; |
const info = document.createElement('span'); |
||||
|
info.className = 'info'; |
||||
|
const key = document.createElement('span'); |
||||
|
key.className = 'key'; |
||||
|
key.innerHTML = name + ':'; |
||||
|
const value = document.createElement('span'); |
||||
|
value.className = 'value'; |
||||
|
value.innerHTML = '0'; |
||||
|
info.appendChild(key); |
||||
|
info.appendChild(value); |
||||
|
infoBar.appendChild(info); |
||||
|
function update() { |
||||
|
let v = updateFunction(); |
||||
|
if (v === undefined) v = '?'; |
||||
|
value.innerHTML = v; |
||||
|
} |
||||
|
return update; |
||||
|
} |
||||
|
|
||||
|
infos.push(createInfo('zoom', function() { |
||||
|
var percent = zoom * 100; |
||||
|
return percent.toFixed(0) + '%'; |
||||
|
})); |
||||
|
infos.push(createInfo('brush', function() { return brushSize; })); |
||||
|
infos.push(createInfo('x', function() { return canvasEndX; })); |
||||
|
infos.push(createInfo('y', function() { return canvasEndY; })); |
||||
|
infos.push(createInfo('color', function() { return color; })); |
||||
|
infos.push(createInfo('width', function() { return canvas.width; })); |
||||
|
infos.push(createInfo('height', function() { return canvas.height; })); |
||||
|
|
||||
return puck; |
function updateInfos() { |
||||
|
infos.forEach(info => info()); |
||||
} |
} |
||||
|
|
||||
|
// }}}
|
||||
|
|
||||
|
// start {{{
|
||||
|
|
||||
|
ctx.fillStyle = backgroundColor; |
||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
||||
|
updateInfos(); |
||||
|
toolButtons[0]['button'].click(); |
||||
|
|
||||
|
// }}}
|
||||
|
@ -1,724 +0,0 @@ |
|||||
const colorPreview = document.createElement('div'); |
|
||||
colorPreview.id = 'color-preview'; |
|
||||
colorPreview.className = 'puck'; |
|
||||
colorPreview.style.backgroundColor = color; |
|
||||
|
|
||||
menuBar.appendChild(colorPreview); |
|
||||
|
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// helpers {{{
|
|
||||
|
|
||||
|
|
||||
function saveCanvas() { |
|
||||
const dataURL = canvas.toDataURL(); |
|
||||
const dimensions = `${canvas.width}x${canvas.height}`; |
|
||||
localStorage.setItem('mixxCanvas', dataURL); |
|
||||
localStorage.setItem('mixxDimensions', dimensions); |
|
||||
console.log('Canvas saved'); |
|
||||
} |
|
||||
|
|
||||
function loadCanvas() { |
|
||||
const dataURL = localStorage.getItem('mixxCanvas'); |
|
||||
const dimensions = localStorage.getItem('mixxDimensions'); |
|
||||
if (dataURL && dimensions) { |
|
||||
const img = new Image(); |
|
||||
img.src = dataURL; |
|
||||
img.onload = function() { |
|
||||
canvas.width = dimensions.split('x')[0]; |
|
||||
canvas.height = dimensions.split('x')[1]; |
|
||||
canvas.style.width = canvas.width * zoom + 'px'; |
|
||||
canvas.style.height = canvas.height * zoom + 'px'; |
|
||||
canvasWidth = canvas.width; |
|
||||
canvasHeight = canvas.height; |
|
||||
ctx.drawImage(img, 0, 0); |
|
||||
} |
|
||||
} else { |
|
||||
console.log('No saved canvas found'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function clearCanvasFromLocalStorage() { |
|
||||
localStorage.removeItem('savedCanvas'); |
|
||||
localStorage.removeItem('canvasDimensions'); |
|
||||
} |
|
||||
|
|
||||
function saveState() { |
|
||||
if (undoStack.length >= maxHistory) { |
|
||||
undoStack.shift(); // Remove the oldest state if the stack exceeds the limit
|
|
||||
} |
|
||||
|
|
||||
undoStack.push({ |
|
||||
imageData: canvas.toDataURL(), |
|
||||
width: canvas.width, |
|
||||
height: canvas.height |
|
||||
}); |
|
||||
|
|
||||
redoStack = []; // Clear the redo stack whenever a new action is performed
|
|
||||
} |
|
||||
|
|
||||
function undo() { |
|
||||
if (undoStack.length > 0) { |
|
||||
const currentState = { |
|
||||
imageData: canvas.toDataURL(), |
|
||||
width: canvas.width, |
|
||||
height: canvas.height |
|
||||
}; |
|
||||
|
|
||||
redoStack.push(currentState); // Save current state to the redo stack
|
|
||||
const lastState = undoStack.pop(); // Get the last state from the undo stack
|
|
||||
|
|
||||
canvas.width = lastState.width; |
|
||||
canvas.height = lastState.height; |
|
||||
canvas.style.width = canvas.width * zoom + 'px'; |
|
||||
canvas.style.height = canvas.height * zoom + 'px'; |
|
||||
canvasWidth = canvas.width; |
|
||||
canvasHeight = canvas.height; |
|
||||
|
|
||||
const img = new Image(); |
|
||||
img.src = lastState.imageData; |
|
||||
img.onload = function() { |
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.drawImage(img, 0, 0); |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function redo() { |
|
||||
if (redoStack.length > 0) { |
|
||||
const currentState = { |
|
||||
imageData: canvas.toDataURL(), |
|
||||
width: canvas.width, |
|
||||
height: canvas.height |
|
||||
}; |
|
||||
|
|
||||
undoStack.push(currentState); // Save current state to the undo stack
|
|
||||
const nextState = redoStack.pop(); // Get the last state from the redo stack
|
|
||||
|
|
||||
canvas.width = nextState.width; |
|
||||
canvas.height = nextState.height; |
|
||||
canvas.style.width = canvas.width * zoom + 'px'; |
|
||||
canvas.style.height = canvas.height * zoom + 'px'; |
|
||||
canvasWidth = canvas.width; |
|
||||
canvasHeight = canvas.height; |
|
||||
|
|
||||
const img = new Image(); |
|
||||
img.src = nextState.imageData; |
|
||||
img.onload = function() { |
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.drawImage(img, 0, 0); |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function getPositionOnCanvas(e) { |
|
||||
const rect = canvas.getBoundingClientRect(); |
|
||||
return { |
|
||||
x: Math.round((e.clientX - rect.left) / zoom), |
|
||||
y: Math.round((e.clientY - rect.top) / zoom), |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
function drawCircle(x, y) { |
|
||||
ctx.beginPath(); |
|
||||
ctx.arc(x, y, brushSize / 2, 0, 2 * Math.PI, false); |
|
||||
ctx.fillStyle = color; |
|
||||
ctx.fill(); |
|
||||
} |
|
||||
|
|
||||
function drawLineWithCircles(x1, y1, x2, y2) { |
|
||||
const dx = x2 - x1; |
|
||||
const dy = y2 - y1; |
|
||||
const distance = Math.sqrt(dx * dx + dy * dy); |
|
||||
const steps = Math.ceil(distance / (brushSize / 5)); |
|
||||
|
|
||||
for (let i = 0; i <= steps; i++) { |
|
||||
const x = x1 + (dx * i) / steps; |
|
||||
const y = y1 + (dy * i) / steps; |
|
||||
drawCircle(x, y); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function saveCanvasContents() { |
|
||||
tempCanvas = document.createElement('canvas'); |
|
||||
tempCanvas.width = canvas.width; |
|
||||
tempCanvas.height = canvas.height; |
|
||||
const tempCtx = tempCanvas.getContext('2d'); |
|
||||
tempCtx.drawImage(canvas, 0, 0); |
|
||||
} |
|
||||
|
|
||||
function updateColorPreview() { |
|
||||
colorPreview.style.backgroundColor = color; |
|
||||
} |
|
||||
|
|
||||
function hexToRgbArray(hex) { |
|
||||
if (hex.startsWith('#')) { |
|
||||
hex = hex.slice(1); |
|
||||
} |
|
||||
|
|
||||
if (hex.length === 3) { |
|
||||
hex = hex.split('').map(char => char + char).join(''); |
|
||||
} |
|
||||
|
|
||||
const bigint = parseInt(hex, 16); |
|
||||
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; |
|
||||
} |
|
||||
|
|
||||
function floodFill(x, y, fillColor) { |
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
||||
const data = imageData.data; |
|
||||
|
|
||||
const targetColor = getColorAtPixel(data, x, y); |
|
||||
const fillColorArray = hexToRgbArray(fillColor); |
|
||||
|
|
||||
if (colorsMatch(targetColor, fillColorArray)) { |
|
||||
return; // The clicked point is already the fill color
|
|
||||
} |
|
||||
|
|
||||
const stack = [{x, y}]; |
|
||||
|
|
||||
while (stack.length > 0) { |
|
||||
const {x, y} = stack.pop(); |
|
||||
const currentColor = getColorAtPixel(data, x, y); |
|
||||
|
|
||||
if (colorsMatch(currentColor, targetColor)) { |
|
||||
setColorAtPixel(data, x, y, fillColorArray); |
|
||||
|
|
||||
if (x > 0) stack.push({x: x - 1, y}); |
|
||||
if (x < canvas.width - 1) stack.push({x: x + 1, y}); |
|
||||
if (y > 0) stack.push({x, y: y - 1}); |
|
||||
if (y < canvas.height - 1) stack.push({x, y: y + 1}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
ctx.putImageData(imageData, 0, 0); |
|
||||
} |
|
||||
|
|
||||
function getColorAtPixel(data, x, y) { |
|
||||
const index = (y * canvas.width + x) * 4; |
|
||||
return [data[index], data[index + 1], data[index + 2], data[index + 3]]; |
|
||||
} |
|
||||
|
|
||||
function setColorAtPixel(data, x, y, color) { |
|
||||
const index = (y * canvas.width + x) * 4; |
|
||||
data[index] = color[0]; |
|
||||
data[index + 1] = color[1]; |
|
||||
data[index + 2] = color[2]; |
|
||||
data[index + 3] = 255; // Set alpha to fully opaque
|
|
||||
} |
|
||||
|
|
||||
function colorsMatch(a, b) { |
|
||||
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; |
|
||||
} |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// mousedown {{{
|
|
||||
|
|
||||
canvasArea.addEventListener('mousedown', (e) => { |
|
||||
if (e.target.closest('.puck')) return; |
|
||||
|
|
||||
startX = e.clientX; |
|
||||
startY = e.clientY; |
|
||||
canvasStartX = getPositionOnCanvas(e).x; |
|
||||
canvasStartY = getPositionOnCanvas(e).y; |
|
||||
saveCanvasContents(); |
|
||||
isMouseDown = true; |
|
||||
|
|
||||
if ( |
|
||||
tool === 'brush' || |
|
||||
tool === 'content-move' || |
|
||||
tool === 'resize' || |
|
||||
tool === 'zoom' || |
|
||||
tool === 'bucket-fill' |
|
||||
) { |
|
||||
saveState(); |
|
||||
} |
|
||||
|
|
||||
if (tool === 'brush') { |
|
||||
console.log('brush'); |
|
||||
drawCircle(canvasStartX, canvasStartY); |
|
||||
} else if (tool === 'bucket-fill') { |
|
||||
floodFill(canvasStartX, canvasStartY, color); |
|
||||
return; |
|
||||
} else if (tool === 'move') { |
|
||||
startX = e.clientX - canvasContainer.offsetLeft; |
|
||||
startY = e.clientY - canvasContainer.offsetTop; |
|
||||
} else if (tool === 'color-picker') { |
|
||||
const imageData = ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data; |
|
||||
const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; |
|
||||
color = pickedColor; |
|
||||
console.log('Picked Color:', pickedColor); |
|
||||
updateColorPreview(); |
|
||||
return; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// mousemove {{{
|
|
||||
|
|
||||
canvasArea.addEventListener('mousemove', (e) => { |
|
||||
|
|
||||
endX = e.clientX; |
|
||||
endY = e.clientY; |
|
||||
dX = endX - startX; |
|
||||
dY = endY - startY; |
|
||||
|
|
||||
canvasEndX = getPositionOnCanvas(e).x; |
|
||||
canvasEndY = getPositionOnCanvas(e).y; |
|
||||
canvasDX = canvasEndX - canvasStartX; |
|
||||
canvasDY = canvasEndY - canvasStartY; |
|
||||
|
|
||||
if (tool == 'brush-size') { |
|
||||
brushPreview.style.display = 'block'; |
|
||||
brushPreview.style.width = brushSize + 'px'; |
|
||||
brushPreview.style.height = brushSize + 'px'; |
|
||||
brushPreview.style.left = e.clientX - brushSize / 2 + 'px'; |
|
||||
brushPreview.style.top = e.clientY - brushSize / 2 + 'px'; |
|
||||
} |
|
||||
|
|
||||
if (isMouseDown) { |
|
||||
|
|
||||
if (tool === 'brush-size') { |
|
||||
brushSize += dX * dBrushSize; |
|
||||
if (brushSize < 1) brushSize = 1; |
|
||||
if (brushSize > maxBrushSize) brushSize = maxBrushSize; |
|
||||
startX = endX; |
|
||||
} else if (tool === 'brush') { |
|
||||
drawLineWithCircles(canvasStartX, canvasStartY, canvasEndX, canvasEndY); |
|
||||
|
|
||||
canvasStartX = canvasEndX; |
|
||||
canvasStartY = canvasEndY; |
|
||||
|
|
||||
} else if (tool === 'content-move') { |
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.fillStyle = backgroundColor; |
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.drawImage(tempCanvas, dX, dY); |
|
||||
} else if (tool === 'move') { |
|
||||
canvasContainer.style.left = dX + 'px'; |
|
||||
canvasContainer.style.top = dY + 'px'; |
|
||||
} else if (tool === 'zoom') { |
|
||||
zoom += dX * dZoom; |
|
||||
if (zoom < 0.1) zoom = 0.1; |
|
||||
canvas.style.height = canvasHeight * zoom + 'px'; |
|
||||
canvas.style.width = canvasWidth * zoom + 'px'; |
|
||||
startX = endX; |
|
||||
} else if (tool === 'resize') { |
|
||||
let newWidth = canvasWidth + dX / zoom; |
|
||||
let newHeight = canvasHeight + dY / zoom; |
|
||||
if (newWidth > 0 && newHeight > 0) { |
|
||||
canvas.width = newWidth; |
|
||||
canvas.height = newHeight; |
|
||||
canvas.style.width = newWidth * zoom + 'px'; |
|
||||
canvas.style.height = newHeight * zoom + 'px'; |
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.fillStyle = backgroundColor; |
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.drawImage(tempCanvas, 0, 0); |
|
||||
} |
|
||||
} else if (tool === 'color-mix') { |
|
||||
|
|
||||
const imageData = ctx.getImageData(canvasEndX, canvasEndY, 1, 1).data; |
|
||||
const canvasColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`; |
|
||||
|
|
||||
const distance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); |
|
||||
const t = Math.min(1, distance / 300); |
|
||||
|
|
||||
const mixedColor = mixbox.lerp(color, canvasColor, t); |
|
||||
|
|
||||
color = mixedColor; |
|
||||
|
|
||||
startX = e.clientX; |
|
||||
startY = e.clientY; |
|
||||
|
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
updateInfos(); |
|
||||
updateColorPreview(); |
|
||||
}); |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// mouseup {{{
|
|
||||
|
|
||||
canvasArea.addEventListener('mouseup', (e) => { |
|
||||
isMouseDown = false; |
|
||||
if (tool === 'brush') { |
|
||||
ctx.closePath(); |
|
||||
} else if (tool === 'resize') { |
|
||||
canvasWidth = canvas.width; |
|
||||
canvasHeight = canvas.height; |
|
||||
} |
|
||||
|
|
||||
updateColorPreview(); |
|
||||
}); |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// mouseleave {{{
|
|
||||
|
|
||||
canvasArea.addEventListener('mouseleave', (e) => { |
|
||||
isMouseDown = false; |
|
||||
brushPreview.style.display = 'none'; |
|
||||
}); |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// keybindings {{{
|
|
||||
|
|
||||
const toolKeyBindings = {} |
|
||||
|
|
||||
const functionKeyBindings = { |
|
||||
} |
|
||||
|
|
||||
document.addEventListener('keydown', (e) => { |
|
||||
if (keyDown) return; |
|
||||
|
|
||||
const newTool = toolKeyBindings[e.key.toLowerCase()] |
|
||||
if (newTool) { |
|
||||
prevTool = tool; |
|
||||
keyDown = true; |
|
||||
changeTool(newTool); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const func = functionKeyBindings[e.key]; |
|
||||
if (func) { |
|
||||
func(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
}); |
|
||||
|
|
||||
document.addEventListener('keyup', (e) => { |
|
||||
const currentTool = toolKeyBindings[e.key.toLowerCase()] |
|
||||
if (currentTool) { |
|
||||
keyDown = false; |
|
||||
if (e.key == e.key.toLowerCase()) { |
|
||||
changeTool(prevTool); |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// tools {{{
|
|
||||
|
|
||||
var toolButtons = []; |
|
||||
|
|
||||
function changeTool(toolName) { |
|
||||
toolButtons.forEach(button => button.button.classList.remove('active')); |
|
||||
toolButtons.find(button => button.name === toolName).button.classList.add('active'); |
|
||||
tool = toolName; |
|
||||
brushPreview.style.display = 'none'; |
|
||||
updateInfos(); |
|
||||
} |
|
||||
|
|
||||
function createToolButton(toolName, displayName, icon, key=undefined) { |
|
||||
const button = document.createElement('div'); |
|
||||
button.classList.add('button'); |
|
||||
button.classList.add('tool'); |
|
||||
button.innerHTML = icon; |
|
||||
button.title = displayName; |
|
||||
button.addEventListener('click', () => { |
|
||||
changeTool(toolName); |
|
||||
}); |
|
||||
|
|
||||
if (key) { |
|
||||
const keyHint = document.createElement('span'); |
|
||||
keyHint.className = 'key-hint'; |
|
||||
keyHint.innerHTML = key; |
|
||||
button.appendChild(keyHint); |
|
||||
toolKeyBindings[key] = toolName; |
|
||||
} |
|
||||
|
|
||||
toolBar.appendChild(button); |
|
||||
toolButtons.push({'name': toolName, 'button': button}); |
|
||||
return button; |
|
||||
} |
|
||||
|
|
||||
createToolButton('brush', 'Brush', '<i class="fa-solid fa-paintbrush"></i>', 'b'); |
|
||||
createToolButton('content-move', 'Move Content', '<i class="fa-regular fa-hand"></i>', 'h'); |
|
||||
createToolButton('move', 'Move Canvas', '<i class="fa-solid fa-arrows-up-down-left-right"></i>', 'm'); |
|
||||
createToolButton('zoom', 'Zoom', '<i class="fa-solid fa-magnifying-glass"></i>', 'z'); |
|
||||
createToolButton('resize', 'Resize', '<i class="fa-solid fa-ruler-combined"></i>', 'r'); |
|
||||
createToolButton('color-picker', 'Color Picker', '<i class="fa-solid fa-eye-dropper"></i>', 'a'); |
|
||||
createToolButton('color-mix', 'Color Mix', '<i class="fa-solid fa-mortar-pestle"></i>', 'x'); |
|
||||
createToolButton('brush-size', 'Brush Size', '<i class="fa-regular fa-circle-dot"></i>', 'd'); |
|
||||
createToolButton('bucket-fill', 'Bucket Fill', '<i class="fa-solid fa-fill"></i>', 'k'); |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// menu functons {{{
|
|
||||
|
|
||||
function flipCanvasHorizontally(e) { |
|
||||
saveState(); |
|
||||
ctx.save(); |
|
||||
saveCanvasContents(); |
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.scale(-1, 1); |
|
||||
ctx.translate(-canvas.width, 0); |
|
||||
ctx.drawImage(tempCanvas, 0, 0); |
|
||||
ctx.restore(); |
|
||||
} |
|
||||
|
|
||||
function flipCanvasVertically(e) { |
|
||||
saveState(); |
|
||||
ctx.save(); |
|
||||
saveCanvasContents(); |
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.scale(1, -1); |
|
||||
ctx.translate(0, -canvas.height); |
|
||||
ctx.drawImage(tempCanvas, 0, 0); |
|
||||
ctx.restore(); |
|
||||
} |
|
||||
|
|
||||
function exportCanvas(e) { |
|
||||
const link = document.createElement('a'); |
|
||||
link.download = 'canvas.png'; |
|
||||
link.href = canvas.toDataURL(); |
|
||||
link.click(); |
|
||||
} |
|
||||
|
|
||||
function importCanvas(e) { |
|
||||
const input = document.createElement('input'); |
|
||||
input.type = 'file'; |
|
||||
input.accept = 'image/*'; |
|
||||
input.onchange = (e) => { |
|
||||
const file = e.target.files[0]; |
|
||||
const reader = new FileReader(); |
|
||||
reader.onload = (e) => { |
|
||||
const img = new Image(); |
|
||||
img.onload = () => { |
|
||||
canvas.width = img.width; |
|
||||
canvas.height = img.height; |
|
||||
ctx.drawImage(img, 0, 0); |
|
||||
} |
|
||||
img.src = e.target.result; |
|
||||
} |
|
||||
reader.readAsDataURL(file); |
|
||||
} |
|
||||
input.click(); |
|
||||
} |
|
||||
|
|
||||
function clearCanvas(e) { |
|
||||
saveState(); |
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
ctx.fillStyle = backgroundColor; |
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
||||
} |
|
||||
|
|
||||
function resetZoom(e) { |
|
||||
zoom = 1; |
|
||||
canvas.style.width = canvas.width * zoom + 'px'; |
|
||||
canvas.style.height = canvas.height * zoom + 'px'; |
|
||||
canvasWidth = canvas.width; |
|
||||
canvasHeight = canvas.height; |
|
||||
|
|
||||
canvasAreaRect = canvasArea.getBoundingClientRect(); |
|
||||
|
|
||||
canvasContainer.style.left = `${canvasAreaRect.left}px`; |
|
||||
canvasContainer.style.top = `${canvasAreaRect.top}px`; |
|
||||
} |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// menu {{{
|
|
||||
|
|
||||
var menuButtons = []; |
|
||||
|
|
||||
function createMenuButton(icon, name, clickFunction, key=undefined) { |
|
||||
const button = document.createElement('div'); |
|
||||
button.className = 'button'; |
|
||||
button.innerHTML = icon; |
|
||||
button.title = name; |
|
||||
if (clickFunction) { |
|
||||
button.addEventListener('click', () => { |
|
||||
clickFunction() |
|
||||
updateInfos(); |
|
||||
}); |
|
||||
} |
|
||||
menuBar.appendChild(button); |
|
||||
if (key) { |
|
||||
const keyHint = document.createElement('span'); |
|
||||
keyHint.className = 'key-hint'; |
|
||||
keyHint.innerHTML = key; |
|
||||
button.appendChild(keyHint); |
|
||||
functionKeyBindings[key] = clickFunction; |
|
||||
} |
|
||||
return button; |
|
||||
} |
|
||||
|
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-download"></i>', 'Save', saveCanvas, 's')); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-upload"></i>', 'Load', loadCanvas)); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-trash-can"></i>', 'Clear', clearCanvas)); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-floppy-disk"></i>', 'Export', exportCanvas)); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-regular fa-folder-open"></i>', 'Import', importCanvas)); |
|
||||
|
|
||||
|
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-left-right"></i>', 'Flip Horizontally', flipCanvasHorizontally, 'f')); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-up-down"></i>', 'Flip Vertically', flipCanvasVertically, 'v')); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-undo"></i>', 'Undo', undo, 'u')); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-redo"></i>', 'Redo', redo, 'y')); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-pump-soap"></i>', 'Clear', clearCanvas, 'c')); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-house"></i>', 'Reset', resetZoom, 't')); |
|
||||
menuButtons.push(createMenuButton('<i class="fa-solid fa-plus"></i>', 'Add Color', createPuck)); |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// pucks {{{
|
|
||||
|
|
||||
function createPuck(c, editable=true, key=undefined) { |
|
||||
if (c === undefined) { |
|
||||
c = color; |
|
||||
} |
|
||||
|
|
||||
const puck = document.createElement('div'); |
|
||||
puck.className = 'puck'; |
|
||||
puck.style.backgroundColor = c; |
|
||||
|
|
||||
// const selectHandle = document.createElement('div');
|
|
||||
// selectHandle.className = 'select-handle';
|
|
||||
// selectHandle.innerHTML = '<i class="fa-solid fa-droplet"></i>';
|
|
||||
// puck.appendChild(selectHandle);
|
|
||||
|
|
||||
// selectHandle.addEventListener('click', () => {
|
|
||||
// color = puck.style.backgroundColor;
|
|
||||
// updateColorPreview();
|
|
||||
// updateInfos();
|
|
||||
// });
|
|
||||
|
|
||||
if (editable) { |
|
||||
// const updateHandle = document.createElement('div');
|
|
||||
// updateHandle.className = 'update-handle';
|
|
||||
// updateHandle.innerHTML = '<i class="fa-solid fa-fill"></i>';
|
|
||||
// puck.appendChild(updateHandle);
|
|
||||
|
|
||||
// updateHandle.addEventListener('click', () => {
|
|
||||
// puck.style.backgroundColor = color;
|
|
||||
// });
|
|
||||
|
|
||||
const deleteHandle = document.createElement('div'); |
|
||||
deleteHandle.className = 'delete-handle'; |
|
||||
deleteHandle.innerHTML = '<i class="fa-solid fa-trash-can"></i>'; |
|
||||
puck.appendChild(deleteHandle); |
|
||||
|
|
||||
deleteHandle.addEventListener('click', () => { |
|
||||
console.log("test"); |
|
||||
puck.remove(); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
if (key) { |
|
||||
const keyHint = document.createElement('div'); |
|
||||
keyHint.className = 'key-hint'; |
|
||||
keyHint.innerHTML = key; |
|
||||
puck.appendChild(keyHint); |
|
||||
} |
|
||||
|
|
||||
function mixx(startTime) { |
|
||||
var interval = setInterval(() => { |
|
||||
const elapsedTime = Date.now() - startTime; |
|
||||
const t = Math.min(1, elapsedTime / 10000); |
|
||||
const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t); |
|
||||
color = mixedColor; |
|
||||
updateColorPreview(); |
|
||||
updateInfos(); |
|
||||
}, 50); |
|
||||
return interval; |
|
||||
} |
|
||||
|
|
||||
puck.addEventListener('mousedown', (e) => { |
|
||||
const startTime = Date.now(); |
|
||||
var interval = mixx(startTime); |
|
||||
function onMouseUp() { |
|
||||
clearInterval(interval); |
|
||||
document.removeEventListener('mouseup', onMouseUp); |
|
||||
} |
|
||||
document.addEventListener('mouseup', onMouseUp); |
|
||||
}); |
|
||||
|
|
||||
document.addEventListener('keydown', (e) => { |
|
||||
if (e.key == key) { |
|
||||
console.log(e.key); |
|
||||
const startTime = Date.now(); |
|
||||
var interval = mixx(startTime); |
|
||||
function onKeyUp() { |
|
||||
clearInterval(interval); |
|
||||
document.removeEventListener('keyup', onKeyUp); |
|
||||
} |
|
||||
document.addEventListener('keyup', onKeyUp); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
menuBar.appendChild(puck); |
|
||||
} |
|
||||
|
|
||||
createPuck(c='rgb(0, 0, 0)', editable=false, key='1'); |
|
||||
createPuck(c='rgb(255, 0, 0)', editale=false, key='2'); |
|
||||
createPuck(c='rgb(0, 0, 255)', editale=false, key='3'); |
|
||||
createPuck(c='rgb(255, 255, 0)', editale=false, key='4'); |
|
||||
createPuck(c='rgb(99, 60, 22)', editale=false, key='5'); |
|
||||
createPuck(c='rgb(0, 255, 0)', editale=false, key='6'); |
|
||||
createPuck(c='rgb(255, 0, 255)', editale=false, key='7'); |
|
||||
createPuck(c='rgb(0, 255, 255)', editale=false, key='8'); |
|
||||
createPuck(c='rgb(255, 255, 255)', editale=false, key='9'); |
|
||||
|
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// info {{{
|
|
||||
|
|
||||
var infos = []; |
|
||||
|
|
||||
function createInfo(name, updateFunction) { |
|
||||
const info = document.createElement('span'); |
|
||||
info.className = 'info'; |
|
||||
const key = document.createElement('span'); |
|
||||
key.className = 'key'; |
|
||||
key.innerHTML = name + ':'; |
|
||||
const value = document.createElement('span'); |
|
||||
value.className = 'value'; |
|
||||
value.innerHTML = '0'; |
|
||||
info.appendChild(key); |
|
||||
info.appendChild(value); |
|
||||
infoBar.appendChild(info); |
|
||||
function update() { |
|
||||
let v = updateFunction(); |
|
||||
if (v === undefined) v = '?'; |
|
||||
value.innerHTML = v; |
|
||||
} |
|
||||
return update; |
|
||||
} |
|
||||
|
|
||||
infos.push(createInfo('zoom', function() { |
|
||||
var percent = zoom * 100; |
|
||||
return percent.toFixed(0) + '%'; |
|
||||
})); |
|
||||
infos.push(createInfo('brush', function() { return brushSize; })); |
|
||||
infos.push(createInfo('x', function() { return canvasEndX; })); |
|
||||
infos.push(createInfo('y', function() { return canvasEndY; })); |
|
||||
infos.push(createInfo('color', function() { return color; })); |
|
||||
infos.push(createInfo('width', function() { return canvas.width; })); |
|
||||
infos.push(createInfo('height', function() { return canvas.height; })); |
|
||||
|
|
||||
function updateInfos() { |
|
||||
infos.forEach(info => info()); |
|
||||
} |
|
||||
|
|
||||
// }}}
|
|
||||
|
|
||||
// start {{{
|
|
||||
|
|
||||
ctx.fillStyle = backgroundColor; |
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
||||
updateInfos(); |
|
||||
toolButtons[0]['button'].click(); |
|
||||
|
|
||||
// }}}
|
|
Loading…
Reference in new issue