@ -1,77 +0,0 @@ |
|||
function clearFromLocalStorage() { |
|||
localStorage.removeItem(LOCAL_STORAGE_CANVAS_NAME); |
|||
localStorage.removeItem(LOCAL_STORAGE_DIMENSIONS_NAME); |
|||
} |
|||
|
|||
// }}}
|
|||
|
|||
// history {{{
|
|||
|
|||
function saveState(undoStack, redoStack, maxHistory) { |
|||
if (undoStack.length >= maxHistory) { |
|||
undoStack.shift(); |
|||
} |
|||
undoStack.push({ |
|||
imageData: canvas.toDataURL(), |
|||
width: canvas.width, |
|||
height: canvas.height |
|||
}); |
|||
redoStack = []; |
|||
} |
|||
|
|||
function undo(canvas, ctx, undoStack, redoStack) { |
|||
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); |
|||
}; |
|||
} |
|||
} |
|||
|
|||
// }}}
|
|||
|
@ -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(); |
|||
|
|||
// }}}
|
@ -1,21 +0,0 @@ |
|||
// For ES Module Syntax
|
|||
export default [ |
|||
{ |
|||
languageOptions: { |
|||
ecmaVersion: "latest", // Or specify a specific version like 2021, 2020, etc.
|
|||
sourceType: "module", // "module" for ES modules, "script" for non-modular code
|
|||
globals: { |
|||
// Define global variables that are predefined
|
|||
browser: true, |
|||
node: true, |
|||
es6: true, |
|||
}, |
|||
}, |
|||
rules: { |
|||
"no-unused-vars": "off", |
|||
"semi": ["error", "always"], |
|||
"quotes": ["error", "double"] |
|||
} |
|||
} |
|||
]; |
|||
|
After Width: | Height: | Size: 804 B |
@ -0,0 +1,93 @@ |
|||
Copyright 2011, The VT323 Project Authors ([email protected]) |
|||
|
|||
This Font Software is licensed under the SIL Open Font License, Version 1.1. |
|||
This license is copied below, and is also available with a FAQ at: |
|||
https://openfontlicense.org |
|||
|
|||
|
|||
----------------------------------------------------------- |
|||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 |
|||
----------------------------------------------------------- |
|||
|
|||
PREAMBLE |
|||
The goals of the Open Font License (OFL) are to stimulate worldwide |
|||
development of collaborative font projects, to support the font creation |
|||
efforts of academic and linguistic communities, and to provide a free and |
|||
open framework in which fonts may be shared and improved in partnership |
|||
with others. |
|||
|
|||
The OFL allows the licensed fonts to be used, studied, modified and |
|||
redistributed freely as long as they are not sold by themselves. The |
|||
fonts, including any derivative works, can be bundled, embedded, |
|||
redistributed and/or sold with any software provided that any reserved |
|||
names are not used by derivative works. The fonts and derivatives, |
|||
however, cannot be released under any other type of license. The |
|||
requirement for fonts to remain under this license does not apply |
|||
to any document created using the fonts or their derivatives. |
|||
|
|||
DEFINITIONS |
|||
"Font Software" refers to the set of files released by the Copyright |
|||
Holder(s) under this license and clearly marked as such. This may |
|||
include source files, build scripts and documentation. |
|||
|
|||
"Reserved Font Name" refers to any names specified as such after the |
|||
copyright statement(s). |
|||
|
|||
"Original Version" refers to the collection of Font Software components as |
|||
distributed by the Copyright Holder(s). |
|||
|
|||
"Modified Version" refers to any derivative made by adding to, deleting, |
|||
or substituting -- in part or in whole -- any of the components of the |
|||
Original Version, by changing formats or by porting the Font Software to a |
|||
new environment. |
|||
|
|||
"Author" refers to any designer, engineer, programmer, technical |
|||
writer or other person who contributed to the Font Software. |
|||
|
|||
PERMISSION & CONDITIONS |
|||
Permission is hereby granted, free of charge, to any person obtaining |
|||
a copy of the Font Software, to use, study, copy, merge, embed, modify, |
|||
redistribute, and sell modified and unmodified copies of the Font |
|||
Software, subject to the following conditions: |
|||
|
|||
1) Neither the Font Software nor any of its individual components, |
|||
in Original or Modified Versions, may be sold by itself. |
|||
|
|||
2) Original or Modified Versions of the Font Software may be bundled, |
|||
redistributed and/or sold with any software, provided that each copy |
|||
contains the above copyright notice and this license. These can be |
|||
included either as stand-alone text files, human-readable headers or |
|||
in the appropriate machine-readable metadata fields within text or |
|||
binary files as long as those fields can be easily viewed by the user. |
|||
|
|||
3) No Modified Version of the Font Software may use the Reserved Font |
|||
Name(s) unless explicit written permission is granted by the corresponding |
|||
Copyright Holder. This restriction only applies to the primary font name as |
|||
presented to the users. |
|||
|
|||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font |
|||
Software shall not be used to promote, endorse or advertise any |
|||
Modified Version, except to acknowledge the contribution(s) of the |
|||
Copyright Holder(s) and the Author(s) or with their explicit written |
|||
permission. |
|||
|
|||
5) The Font Software, modified or unmodified, in part or in whole, |
|||
must be distributed entirely under this license, and must not be |
|||
distributed under any other license. The requirement for fonts to |
|||
remain under this license does not apply to any document created |
|||
using the Font Software. |
|||
|
|||
TERMINATION |
|||
This license becomes null and void if any of the above conditions are |
|||
not met. |
|||
|
|||
DISCLAIMER |
|||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF |
|||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT |
|||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE |
|||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
|||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL |
|||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM |
|||
OTHER DEALINGS IN THE FONT SOFTWARE. |
Before Width: | Height: | Size: 182 B |
Before Width: | Height: | Size: 235 B |
After Width: | Height: | Size: 953 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 986 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 478 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 615 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 957 B |
After Width: | Height: | Size: 956 B |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 931 B |
After Width: | Height: | Size: 816 B |
After Width: | Height: | Size: 592 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 450 B |
After Width: | Height: | Size: 937 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 726 B |
After Width: | Height: | Size: 484 B |
After Width: | Height: | Size: 975 B |
After Width: | Height: | Size: 577 B |
After Width: | Height: | Size: 785 B |
After Width: | Height: | Size: 490 B |
After Width: | Height: | Size: 782 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 391 B |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 895 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 696 B |
After Width: | Height: | Size: 517 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 970 B |
After Width: | Height: | Size: 388 B |
After Width: | Height: | Size: 887 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 846 B |
After Width: | Height: | Size: 582 B |
After Width: | Height: | Size: 581 B |
After Width: | Height: | Size: 1006 B |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 969 B |
After Width: | Height: | Size: 920 B |
After Width: | Height: | Size: 685 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 508 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 892 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 811 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 990 B |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 666 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 832 B |
After Width: | Height: | Size: 693 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 842 B |
After Width: | Height: | Size: 422 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 956 B |
After Width: | Height: | Size: 748 B |
After Width: | Height: | Size: 803 B |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 763 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 790 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 793 B |
After Width: | Height: | Size: 953 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 829 B |
After Width: | Height: | Size: 869 B |
After Width: | Height: | Size: 859 B |
After Width: | Height: | Size: 797 B |