|
|
|
// init {{{
|
|
|
|
|
|
|
|
const menuBar = document.getElementById('menu-bar');
|
|
|
|
const toolBar = document.getElementById('tool-bar');
|
|
|
|
const layerBar = document.getElementById('layer-bar');
|
|
|
|
const canvasArea = document.getElementById('canvas-area');
|
|
|
|
const infoBar = document.getElementById('info-bar');
|
|
|
|
const canvasContainer = document.getElementById('canvas-container');
|
|
|
|
const brushPreview = document.getElementById('brush-preview');
|
|
|
|
|
|
|
|
const canvas = document.getElementById('canvas');
|
|
|
|
// canvas.style.imageRendering = 'pixelated';
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
ctx.webkitImageSmoothingEnabled = false;
|
|
|
|
ctx.mozImageSmoothingEnabled = false;
|
|
|
|
|
|
|
|
canvas.width = 800;
|
|
|
|
canvas.height = 600;
|
|
|
|
let canvasWidth = canvas.width;
|
|
|
|
let canvasHeight = canvas.height;
|
|
|
|
|
|
|
|
let undoStack = [];
|
|
|
|
let redoStack = [];
|
|
|
|
let maxHistory = 30;
|
|
|
|
|
|
|
|
const dZoom = 0.001;
|
|
|
|
|
|
|
|
let zoom = 1;
|
|
|
|
let brushSize = 5;
|
|
|
|
let dBrushSize = 0.5;
|
|
|
|
let maxBrushSize = 500;
|
|
|
|
let backgroundColor = 'rgb(255, 255, 255)';
|
|
|
|
let color = 'rgb(0, 0, 0)';
|
|
|
|
let tool
|
|
|
|
|
|
|
|
let tempCanvas;
|
|
|
|
let startX, startY;
|
|
|
|
let endX, endY;
|
|
|
|
let dX, dY;
|
|
|
|
let canvasStartX
|
|
|
|
let canvasStartY;
|
|
|
|
let canvasEndX;
|
|
|
|
let canvasEndY;
|
|
|
|
let canvasDX
|
|
|
|
let canvasDY;
|
|
|
|
|
|
|
|
let isMouseDown = false;
|
|
|
|
|
|
|
|
const colorPreview = document.createElement('div');
|
|
|
|
colorPreview.id = 'color-preview';
|
|
|
|
colorPreview.className = 'puck';
|
|
|
|
colorPreview.style.backgroundColor = color;
|
|
|
|
|
|
|
|
menuBar.appendChild(colorPreview);
|
|
|
|
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// helpers {{{
|
|
|
|
|
|
|
|
function saveState() {
|
|
|
|
if (undoStack.length >= maxHistory) {
|
|
|
|
undoStack.shift(); // Remove the oldest state if the stack exceeds the limit
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save the current canvas content and dimensions
|
|
|
|
undoStack.push({
|
|
|
|
imageData: canvas.toDataURL(),
|
|
|
|
width: canvas.width,
|
|
|
|
height: canvas.height
|
|
|
|
});
|
|
|
|
|
|
|
|
redoStack = []; // Clear the redo stack whenever a new action is performed
|
|
|
|
}
|
|
|
|
|
|
|
|
function undo() {
|
|
|
|
if (undoStack.length > 0) {
|
|
|
|
const currentState = {
|
|
|
|
imageData: canvas.toDataURL(),
|
|
|
|
width: canvas.width,
|
|
|
|
height: canvas.height
|
|
|
|
};
|
|
|
|
|
|
|
|
redoStack.push(currentState); // Save current state to the redo stack
|
|
|
|
const lastState = undoStack.pop(); // Get the last state from the undo stack
|
|
|
|
|
|
|
|
// Restore the canvas dimensions
|
|
|
|
canvas.width = lastState.width;
|
|
|
|
canvas.height = lastState.height;
|
|
|
|
canvas.style.width = canvas.width * zoom + 'px';
|
|
|
|
canvas.style.height = canvas.height * zoom + 'px';
|
|
|
|
canvasWidth = canvas.width;
|
|
|
|
canvasHeight = canvas.height;
|
|
|
|
|
|
|
|
// Restore the canvas content
|
|
|
|
const img = new Image();
|
|
|
|
img.src = lastState.imageData;
|
|
|
|
img.onload = function() {
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function redo() {
|
|
|
|
if (redoStack.length > 0) {
|
|
|
|
const currentState = {
|
|
|
|
imageData: canvas.toDataURL(),
|
|
|
|
width: canvas.width,
|
|
|
|
height: canvas.height
|
|
|
|
};
|
|
|
|
|
|
|
|
undoStack.push(currentState); // Save current state to the undo stack
|
|
|
|
const nextState = redoStack.pop(); // Get the last state from the redo stack
|
|
|
|
|
|
|
|
// Restore the canvas dimensions
|
|
|
|
canvas.width = nextState.width;
|
|
|
|
canvas.height = nextState.height;
|
|
|
|
canvas.style.width = canvas.width * zoom + 'px';
|
|
|
|
canvas.style.height = canvas.height * zoom + 'px';
|
|
|
|
canvasWidth = canvas.width;
|
|
|
|
canvasHeight = canvas.height;
|
|
|
|
|
|
|
|
// Restore the canvas content
|
|
|
|
const img = new Image();
|
|
|
|
img.src = nextState.imageData;
|
|
|
|
img.onload = function() {
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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') {
|
|
|
|
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';
|
|
|
|
});
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// 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(displayName, icon, toolName, jumpKey=undefined, temporaryKey=undefined) {
|
|
|
|
const button = document.createElement('div');
|
|
|
|
button.classList.add('button');
|
|
|
|
button.classList.add('tool');
|
|
|
|
button.innerHTML = icon;
|
|
|
|
button.title = displayName;
|
|
|
|
button.addEventListener('click', () => {
|
|
|
|
changeTool(toolName);
|
|
|
|
});
|
|
|
|
if (jumpKey) {
|
|
|
|
const jumpKeyHint = document.createElement('span');
|
|
|
|
jumpKeyHint.className = 'jump-key-hint';
|
|
|
|
jumpKeyHint.innerHTML = jumpKey;
|
|
|
|
button.appendChild(jumpKeyHint);
|
|
|
|
}
|
|
|
|
if (temporaryKey) {
|
|
|
|
const temporaryKeyHint = document.createElement('span');
|
|
|
|
temporaryKeyHint.className = 'temporary-key-hint';
|
|
|
|
temporaryKeyHint.innerHTML = temporaryKey;
|
|
|
|
button.appendChild(temporaryKeyHint);
|
|
|
|
}
|
|
|
|
|
|
|
|
toolBar.appendChild(button);
|
|
|
|
return button;
|
|
|
|
}
|
|
|
|
|
|
|
|
toolButtons.push({'name': 'brush', 'button': createToolButton('Brush', '<i class="fa-solid fa-paintbrush"></i>', 'brush', 'e', undefined)});
|
|
|
|
toolButtons.push({'name': 'content-move', 'button': createToolButton('Move Content', '<i class="fa-regular fa-hand"></i>', 'content-move', 'h', undefined)});
|
|
|
|
toolButtons.push({'name': 'move', 'button': createToolButton('Move Canvas', '<i class="fa-solid fa-arrows-up-down-left-right"></i>', 'move', 'm', undefined)});
|
|
|
|
toolButtons.push({'name': 'zoom', 'button': createToolButton('Zoom', '<i class="fa-solid fa-magnifying-glass"></i>', 'zoom', 'z', undefined)});
|
|
|
|
toolButtons.push({'name': 'resize', 'button': createToolButton('Resize', '<i class="fa-solid fa-ruler-combined"></i>', 'resize', 'r', undefined)});
|
|
|
|
toolButtons.push({'name': 'color-picker', 'button': createToolButton('Color Picker', '<i class="fa-solid fa-eye-dropper"></i>', 'color-picker', 'a', undefined)});
|
|
|
|
toolButtons.push({'name': 'color-mix', 'button': createToolButton('Color Mix', '<i class="fa-solid fa-mortar-pestle"></i>', 'color-mix', 's', undefined)});
|
|
|
|
toolButtons.push({'name': 'brush-size', 'button': createToolButton('Brush Size', '<i class="fa-regular fa-circle-dot"></i>', 'brush-size', 'd', undefined)});
|
|
|
|
toolButtons.push({'name': 'bucket-fill', 'button': createToolButton('Bucket Fill', '<i class="fa-solid fa-fill"></i>', 'bucket-fill', 'f', undefined)});
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// 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 saveCanvas(e) {
|
|
|
|
const link = document.createElement('a');
|
|
|
|
link.download = 'canvas.png';
|
|
|
|
link.href = canvas.toDataURL();
|
|
|
|
link.click();
|
|
|
|
}
|
|
|
|
|
|
|
|
function openCanvas(e) {
|
|
|
|
const input = document.createElement('input');
|
|
|
|
input.type = 'file';
|
|
|
|
input.accept = 'image/*';
|
|
|
|
input.onchange = (e) => {
|
|
|
|
const file = e.target.files[0];
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (e) => {
|
|
|
|
const img = new Image();
|
|
|
|
img.onload = () => {
|
|
|
|
canvas.width = img.width;
|
|
|
|
canvas.height = img.height;
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
}
|
|
|
|
img.src = e.target.result;
|
|
|
|
}
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
}
|
|
|
|
input.click();
|
|
|
|
}
|
|
|
|
|
|
|
|
function clearCanvas(e) {
|
|
|
|
saveState();
|
|
|
|
ctx.clearRect(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) {
|
|
|
|
const button = document.createElement('div');
|
|
|
|
button.className = 'button';
|
|
|
|
button.innerHTML = icon;
|
|
|
|
button.title = name;
|
|
|
|
if (clickFunction) {
|
|
|
|
button.addEventListener('click', () => {
|
|
|
|
clickFunction()
|
|
|
|
updateInfos();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
menuBar.appendChild(button);
|
|
|
|
return button;
|
|
|
|
}
|
|
|
|
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-regular fa-folder-open"></i>', 'Open', openCanvas));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-floppy-disk"></i>', 'Save', saveCanvas));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-left-right"></i>', 'Flip Horizontally', flipCanvasHorizontally));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-up-down"></i>', 'Flip Vertically', flipCanvasVertically));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-undo"></i>', 'Undo', undo));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-redo"></i>', 'Redo', redo));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-regular fa-trash-can"></i>', 'Clear', clearCanvas));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-house"></i>', 'Reset', resetZoom));
|
|
|
|
menuButtons.push(createMenuButton('<i class="fa-solid fa-plus"></i>', 'Add Color', createPuck));
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// pucks {{{
|
|
|
|
|
|
|
|
function createPuck(c, editable=true) {
|
|
|
|
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();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
puck.addEventListener('mousedown', (e) => {
|
|
|
|
let isMixing = true;
|
|
|
|
const startTime = Date.now(); // Record the time when the mouse is pressed
|
|
|
|
|
|
|
|
// Interval to update the color based on time
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
if (isMixing) {
|
|
|
|
const elapsedTime = Date.now() - startTime;
|
|
|
|
const t = Math.min(1, elapsedTime / 10000);
|
|
|
|
|
|
|
|
const mixedColor = mixbox.lerp(color, puck.style.backgroundColor, t);
|
|
|
|
|
|
|
|
color = mixedColor;
|
|
|
|
|
|
|
|
updateColorPreview();
|
|
|
|
updateInfos();
|
|
|
|
}
|
|
|
|
}, 50); // Update every 50ms
|
|
|
|
|
|
|
|
document.addEventListener('mouseup', onMouseUp);
|
|
|
|
|
|
|
|
function onMouseUp() {
|
|
|
|
isMixing = false;
|
|
|
|
clearInterval(interval); // Stop the interval when the mouse is released
|
|
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// puck.addEventListener('mousedown', (e) => {
|
|
|
|
// let isMixing = true;
|
|
|
|
// let startX = e.clientX;
|
|
|
|
// let startY = e.clientY;
|
|
|
|
|
|
|
|
// document.addEventListener('mousemove', onMouseMove);
|
|
|
|
// document.addEventListener('mouseup', onMouseUp);
|
|
|
|
|
|
|
|
// function onMouseMove(e) {
|
|
|
|
// if (isMixing) {
|
|
|
|
// 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, puck.style.backgroundColor, t);
|
|
|
|
|
|
|
|
// color = mixedColor;
|
|
|
|
|
|
|
|
// startX = e.clientX;
|
|
|
|
// startY = e.clientY;
|
|
|
|
// updateColorPreview();
|
|
|
|
// updateInfos();
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// function onMouseUp() {
|
|
|
|
// isMixing = false;
|
|
|
|
// document.removeEventListener('mousemove', onMouseMove);
|
|
|
|
// document.removeEventListener('mouseup', onMouseUp);
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
|
|
menuBar.appendChild(puck);
|
|
|
|
}
|
|
|
|
|
|
|
|
createPuck(c='rgb(0, 0, 0)', editable=false);
|
|
|
|
createPuck(c='rgb(255, 255, 255)', editale=false);
|
|
|
|
createPuck(c='rgb(0, 255, 0)', editale=false);
|
|
|
|
createPuck(c='rgb(0, 0, 255)', editale=false);
|
|
|
|
createPuck(c='rgb(255, 255, 0)', editale=false);
|
|
|
|
createPuck(c='rgb(255, 0, 0)', editale=false);
|
|
|
|
createPuck(c='rgb(255, 0, 255)', editale=false);
|
|
|
|
createPuck(c='rgb(0, 255, 255)', editale=false);
|
|
|
|
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// 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());
|
|
|
|
}
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// keybindings {{{
|
|
|
|
|
|
|
|
let keyDown = false;
|
|
|
|
let oldTool = tool;
|
|
|
|
|
|
|
|
const toolBindings = [
|
|
|
|
{'key': 'e', 'tool': 'brush', 'persistent': true},
|
|
|
|
{'key': 'h', 'tool': 'content-move', 'persistent': true},
|
|
|
|
{'key': 'm', 'tool': 'move', 'persistent': true},
|
|
|
|
{'key': 'z', 'tool': 'zoom', 'persistent': true},
|
|
|
|
{'key': 'r', 'tool': 'resize', 'persistent': true},
|
|
|
|
{'key': 'a', 'tool': 'color-picker', 'persistent': false},
|
|
|
|
{'key': 's', 'tool': 'color-mix', 'persistent': false},
|
|
|
|
{'key': 'd', 'tool': 'brush-size', 'persistent': false},
|
|
|
|
]
|
|
|
|
|
|
|
|
const functionBindings = [
|
|
|
|
{'key': 'u', 'function': undo},
|
|
|
|
{'key': 'y', 'function': redo},
|
|
|
|
{'key': 'backspace', 'function': clearCanvas},
|
|
|
|
]
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
if (keyDown) return;
|
|
|
|
|
|
|
|
if (toolBindings.map(b => b.key).includes(e.key)) {
|
|
|
|
oldTool = tool;
|
|
|
|
keyDown = true;
|
|
|
|
changeTool(toolBindings.find(b => b.key === e.key).tool);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (functionBindings.map(b => b.key).includes(e.key)) {
|
|
|
|
functionBindings.find(b => b.key === e.key).function();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener('keyup', (e) => {
|
|
|
|
keyDown = false;
|
|
|
|
if (toolBindings.filter(b => !b.persistent).map(b => b.key).includes(e.key)) {
|
|
|
|
changeTool(oldTool);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// start {{{
|
|
|
|
|
|
|
|
ctx.fillStyle = backgroundColor;
|
|
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
updateInfos();
|
|
|
|
toolButtons[0]['button'].click();
|
|
|
|
resetZoom();
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|