chickenboard - js/drawing.js
/**
* The source for ChickenBoard
* Written by Luke Jones
*
* ██ ██ ██ ██ ██ ████████ ████████ ██ ██ ██ ██ ██ ██ ████████ ██ ██ ████████ ████████
* ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ████ ██ ██ ████ ██ ██ ██
* ██ ██ ██ ████ ████████ ██ ██ ██ ████ ██ ██ ██ ████ ████████ ██ ████ ████████ ██
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
* ████████ ████████ ██ ██ ████████ ████████ ██ ██ ████████ ██ ██ ██ ████████ ██ ██ ██ ████████ ██
* https://www.lukeonline.net/ChickenBoard
* V1.0
*/
/*
Math Functions
*/
const MathL = {
clamp: function (v, mn, mx) {
return Math.max(mn, Math.min(v, mx));
},
// Check if value is in range
rangeCheck: function (v, mn, mx) {
return (v <= mx && v >= mn);
},
// Linear Interpolation
lerp: function (va, vb, ratio) {
return va + ratio * (vb - va);
}
}
// Initialise Program
const canvas = document.getElementById("drawing_canvas");
const overlay = document.getElementById("overlay_canvas");
const gtx = canvas.getContext("2d");
const otx = overlay.getContext("2d");
const canv_width = canvas.width, canv_height = canvas.height;
const size_selector = document.getElementById("tool_size_selector");
const size_selector_container = document.getElementById("tool_size_selector_container");
const spray_strength_selector = document.getElementById("spray_settings_container");
const colour_selector = document.getElementById("colour_selector");
let isDrawingLine = false, lineA = [], lineB = [];
let canvasBounds;
// Initialise usage Variables
const tools = { // Tool types
BRUSH: 'BRUSH',
ERASER: 'ERASER',
PENCIL: 'PENCIL',
SPRAY: 'SPRAY',
LINE: 'LINE',
PICKER: 'PICKER',
FILL: 'FILL'
}
let toolSize = 8; // Tool size, in pixels
let primaryColour = "#000"; // Current selected colour
let secondaryColour = "#fff"; // Secondary colour (unused)
let spray_strength = 1; // Spray strength, either 1, 2 or 3
let mouseHeld = false; // Is mouse held?
let mouseClicked = false; // Has mouse just been clicked?
let mousepos = [0,0]; // Current mouse position (x, y)
let prevMouseMos = [0,0]; // Mouse position in the last frame (x, y)
/*
Initialise board appearance
*/
clearCanvas(gtx, "#fff");
clearCanvas(otx, "rgba(0,0,0,0)");
/*
Checks to see the mouse and engine state before
beginning the action chain, if needed.
*/
function runMotion() {
// Make sure tool size is up to date
toolSize = MathL.clamp(size_selector.value,1,256);
setPrimaryColour(colour_selector.value);
updateOverlay();
if(mouseHeld) {
if(mouseInCanvas()){
startAction();
}
}
}
/*
Executes the beginning of the "action" chain where what happens under the users
pointer is determined by current values, and is executed accordingly.
Called when mouse is pressed
*/
function startAction() {
if (currentTool === tools.LINE) {
if (!mouseClicked) return;
if (!isDrawingLine) {
isDrawingLine = true;
lineA = [...mousepos];
} else {
isDrawingLine = false;
lineB = [...mousepos];
gtx.beginPath();
gtx.lineWidth = toolSize;
gtx.lineCap = "round";
gtx.moveTo(lineA[0], lineA[1]);
gtx.lineTo(lineB[0], lineB[1]);
gtx.stroke();
lineA = [];
lineB = [];
}
mouseClicked = false;
return;
}
if (currentTool === tools.ERASER) {
gtx.fillStyle = secondaryColour;
gtx.strokeStyle = secondaryColour;
} else {
gtx.fillStyle = primaryColour;
gtx.strokeStyle = primaryColour;
}
function act() {
switch (currentTool) {
case tools.PENCIL:
gtx.fillRect(mousepos[0] - toolSize / 2, mousepos[1] - toolSize / 2, toolSize, toolSize);
break;
case tools.ERASER:
case tools.BRUSH:
gtx.beginPath();
gtx.lineWidth = toolSize;
gtx.lineCap = "round";
gtx.moveTo(prevMouseMos[0], prevMouseMos[1]);
gtx.lineTo(mousepos[0], mousepos[1]);
gtx.stroke();
break;
case tools.SPRAY:
for (let s = 0; s <= Math.pow(spray_strength, 3); s++) {
let dist = Math.random() * (toolSize / 2);
let ang = Math.random() * 2 * Math.PI;
let dot = [
mousepos[0] + dist * Math.cos(ang),
mousepos[1] + dist * Math.sin(ang),
];
gtx.fillRect(dot[0], dot[1], 1, 1);
}
break;
case tools.PICKER:
setColourPicker(rgbaToHex(getColorAt(mousepos[0], mousepos[1])));
break;
default:
break;
}
}
act();
mouseClicked = false;
}
/*
Queries the user before clearing the canvas.
- context is the canvas context to clear
- col is the colour to clear it with
*/
function clearCanvasRequest(context, col) {
if(confirm("Clear the Canvas? This cannot be undone!")) {
clearCanvas(context, col);
}
}
/*
Clearing the given canvas with the given colour.
- context is the canvas context to clear
- col is the colour to clear it with. If transparent (rgba(0,0,0,0)) it will just wipe it clear.
*/
function clearCanvas(context, col) {
context.fillStyle = col;
if(col === "rgba(0,0,0,0)") {
context.clearRect(0,0,canv_width,canv_height);
} else {
context.fillRect(0,0,canv_width,canv_height);
}
}
/*
Updates the overlay canvas when called. Used for things like paint
previews, brush position, e.g.
*/
function updateOverlay() {
clearCanvas(otx, "rgba(0,0,0,0)")
otx.strokeStyle = "#666";
otx.lineWidth = 2;
switch (currentTool) {
case "ERASER":
case "BRUSH":
case "SPRAY":
otx.beginPath();
otx.arc(mousepos[0], mousepos[1], toolSize/2, 0, 2 * Math.PI);
otx.stroke();
break;
case "PENCIL":
otx.strokeRect(mousepos[0]-(toolSize/2), mousepos[1]-(toolSize/2), toolSize, toolSize);
break;
case "LINE":
if(isDrawingLine){
otx.lineWidth = toolSize;
otx.strokeStyle = "rgba(128,128,128,0.5)";
otx.beginPath();
otx.lineCap = "round";
otx.moveTo(lineA[0],lineA[1]);
otx.lineTo(mousepos[0],mousepos[1]);
otx.stroke();
otx.strokeStyle = "#666";
otx.lineWidth = 2;
}
case "PICKER":
case "FILL":
otx.beginPath();
otx.moveTo(mousepos[0]-12, mousepos[1]);
otx.lineTo(mousepos[0]+12, mousepos[1]);
otx.stroke();
otx.beginPath();
otx.moveTo(mousepos[0], mousepos[1]-12);
otx.lineTo(mousepos[0], mousepos[1]+12);
otx.stroke();
break;
default:
break;
}
}
/*
Updates the sprayStrength to a set value
- str is the new spray strength, either 1, 2 or 3 - weak, light and heavy respectively
*/
function sprayStrength(str) {
spray_strength = MathL.clamp(str,1,3);
}
/*
Returns array [R,G,B,A] of pixel at xy
- x position of the pixel to query
- y position of the pixel to query
*/
function getColorAt(x, y) {
const data = gtx.getImageData(x, y, 1, 1).data;
return { r: data[0], g: data[1], b: data[2], a: data[3] };
}
/*
Returns an rgba value as a hex colour value.
- rgba is the colour to convert to hex.
*/
function rgbaToHex(rgba) {
const { r, g, b } = rgba;
const toHex = (value) => value.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/*
Checks to see if two colours match.
*/
function colorsMatch(c1, c2) {
return c1.r === c2.r && c1.g === c2.g && c1.b === c2.b && c1.a === c2.a;
}
/*
Sets the colour picker selector to a given colour value
- colour is the colour to set the colour picker to.
*/
function setColourPicker(colour){
colour_selector.value = colour;
}
/*
Sets the primary colour to a given colour value
- colour is the colour to set the primary colour to.
*/
function setPrimaryColour (colour) {
primaryColour = colour;
}
/*
- - UNUSED
Starts the fill algorithm
- x is the x position of the pixel to fill from
- y is the y position of the pixel to fill from
*/
function startFill(x, y) {
let startColor = getColorAt(x, y);
let visited = new Set();
let queue = [];
queue.push([x, y]);
while (queue.length > 0) {
let [startX, startY] = queue.shift();
if (visited.has(`${startX},${startY}`)) continue;
visited.add(`${startX},${startY}`);
if (!MathL.rangeCheck(startX, 0, canv_width) || !MathL.rangeCheck(startY, 0, canv_height)) continue;
let currentColor = getColorAt(startX, startY);
if (!colorsMatch(currentColor, startColor)) continue;
gtx.fillRect(startX, startY, 1, 1);
let left = startX;
while (left >= 0 && colorsMatch(getColorAt(left, startY), startColor)) {
visited.add(`${left},${startY}`);
gtx.fillRect(left, startY, 1, 1);
left--;
}
let right = startX + 1;
while (right < canv_width && colorsMatch(getColorAt(right, startY), startColor)) {
visited.add(`${right},${startY}`);
gtx.fillRect(right, startY, 1, 1);
right++;
}
if (startY - 1 >= 0) {
let newLeft = left + 1;
while (newLeft < right) {
if (colorsMatch(getColorAt(newLeft, startY - 1), startColor) && !visited.has(`${newLeft},${startY - 1}`)) {
queue.push([newLeft, startY - 1]);
}
newLeft++;
}
}
if (startY + 1 < canv_height) {
let newLeft = left + 1;
while (newLeft < right) {
if (colorsMatch(getColorAt(newLeft, startY + 1), startColor) && !visited.has(`${newLeft},${startY + 1}`)) {
queue.push([newLeft, startY + 1]);
}
newLeft++;
}
}
}
}
/*
Sets the tool to the given tool value and updates the interface and
functionality accordingly
- tool is the tool to set the currentTool to.
*/
function changeTool(tool) {
size_selector_container.style.display = "none";
spray_strength_selector.style.display = "none";
$(".tool_button").css = "";
let selected;
switch (tool) {
case 0:
currentTool = tools.BRUSH;
size_selector_container.style.display = "inline-block";
selected = document.getElementById("button_brush");
break;
case 1:
currentTool = tools.ERASER;
size_selector_container.style.display = "inline-block";
selected = document.getElementById("button_eraser");
break;
case 2:
currentTool = tools.PENCIL;
size_selector_container.style.display = "inline-block";
selected = document.getElementById("button_pencil");
break;
case 3:
currentTool = tools.SPRAY;
size_selector_container.style.display = "inline-block";
spray_strength_selector.style.display = "inline-block";
selected = document.getElementById("button_spray");
break;
case 4:
currentTool = tools.LINE;
size_selector_container.style.display = "inline-block";
selected = document.getElementById("button_line");
break;
case 5:
currentTool = tools.PICKER;
selected = document.getElementById("button_picker");
break;
/*case 6:
currentTool = tools.FILL;
break;*/
default:
break;
}
selected.css = "border: '2px inset #bbccff'; borderRadius: '4px'; background: '#779'";
}
/*
Loads any bitmap image into the canvas
*/
function loadInImage() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
const img = new Image();
img.src = e.target.result;
img.onload = function () {
gtx.drawImage(img, 0,0,canv_width,canv_height);
};
};
reader.readAsDataURL(file);
}
}
/*
Provides a download dialog for the current canvas viewport
*/
function saveAsImage() {
if (canvas) {
const dataURL = canvas.toDataURL("image/png");
const link = document.createElement("a");
link.href = dataURL;
link.download = "canvas.png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
console.error("Canvas element not found!");
}
}
/*
Check to ensure the mouse is inside the canvas
*/
function mouseInCanvas() {
return (
MathL.rangeCheck(mousepos[0], 0, canv_width) &&
MathL.rangeCheck(mousepos[1], 0, canv_height)
);
}
/*
Update mouse variables on movement
*/
document.onmousemove = function (event) {
prevMouseMos = mousepos;
canvasBounds = canvas.getBoundingClientRect();
mousepos = [Math.trunc(event.clientX - canvasBounds.left), Math.trunc(event.clientY - canvasBounds.top)];
runMotion();
if (mouseHeld && mouseInCanvas()) {
startAction();
}
};
document.onmousedown = function (event) {
mouseHeld = true;
mouseClicked = true;
prevMouseMos = mousepos;
canvasBounds = canvas.getBoundingClientRect();
mousepos = [Math.trunc(event.clientX - canvasBounds.left), Math.trunc(event.clientY - canvasBounds.top)];
if (mouseInCanvas()) {
startAction();
}
};
document.onmouseup = function () {
mouseHeld = false;
};
changeTool(0);