/*
* PixelED.js - simple pixel-like editor for the browser
*
* This project is primarily just practice for The Odin Project's curriculum.
* (Found at https://theodinproject.com/) It claims this project is difficult,
* but the requirements are quite well-defined, so it doesn't strike me as
* difficult. Just tedious.
*
* This project was a good fit for TOP because it shows the student the
* difficulties in building intuitive and effective interfaces in the Web
* environment.
*
* Scope:
* [x] Erase button (sets all canvas pixels to white)
* [x] Black button, sets current color to black
* [x] White button, sets current color to white
* [x] 'Current color' indicator, clicking color box brings up dialog.
* [-] Reset button (sets canvas to 16x16, current color to black)
* '-> Refreshing the page does the same damn thing. YAGNI
* [x] Range slider for canvas size, with numeric indicator
* [x] Rainbow drawing mode that randomizes the color on every draw
* [x] Darken drawing mode that darkens the color by 10% on every draw
* '-> BONUS: Added a Lighten mode based on the reverse of Darken
* [x] 'Clear' button, to make the entire canvas white
*/
/* GOALS
*
* [*] discrete drawing states
* [*] click-and-drag to draw, not hover
* [ ] right click to copy color under cursor (and switch to it)
*
* STRETCH GOAL
*
* [ ] better UI layout
* [ ] possible export to PNG or GIF format (!!! :D)
*/
/* disable right click's default behavior */
document.body.addEventListener("contextmenu",
(e) => {
e.preventDefault();
}
);
/* Target element for the application */
const root = document.getElementById('app_area');
const dstates = Object.freeze({
BLACK: 0,
WHITE: 1,
CUSTOM: 2,
RAINBOW: 3,
DARKEN: 4,
LIGHTEN: 5
});
current_color = "#000000";
custom_color = "#2564b8";
draw_state = dstates["BLACK"];
do_draw = false;
function clean_element(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
function resetCanvas(size) {
clean_element(grid);
grid.style.gridTemplateColumns = `repeat(${size}, 1fr)`;
rebuildCanvas(size);
}
function rebuildCanvas(size) {
size = lintCanvasSize(size);
for (i=0; i<(size*size); i++) {
box = grid.appendChild(document.createElement("div"));
box.addEventListener("mouseover", function(e) {
if (do_draw) {
drawColor(e.target, current_color);
}
});
box.addEventListener("mousedown", function(e) {
console.log(e);
if (e.which == 3 || e.button == 2) {
console.log(e);
setCustom();
custom_color = rgb_to_hex(e.target.style.backgroundColor);
document.querySelector("#custom_color").value = custom_color;
current_color = custom_color;
} else {
do_draw = true;
drawColor(e.target, current_color);
}
}, { "passive": false, "capture": true } );
box.addEventListener("mouseup", function(e) {
do_draw = false;
}, { "passive": false } );
}
}
function clearCanvas() {
pixels = document.querySelectorAll("#grid_container div");
pixels.forEach(function(i) {
i.removeAttribute("style");
});
}
function handler() {
initialSetup();
}
function initialSetup() {
workspace = document.createElement("div");
workspace.id = ["workspace"];
/* generate a 16x16 grid of divs */
grid = document.createElement("div");
grid.id = ["grid_container"];
/* turn the pen off when you leave the canvas */
grid.addEventListener("mouseleave",
(e) => {
do_draw = false;
}, { "passive": false }
);
grid_caption = document.createElement("div");
grid_caption.id = ["grid_caption"];
grid_caption.textContent = 'an app by ZLG';
resetCanvas(16);
/* we should have 256 divs appended to the grid. */
toolbox = document.createElement("div");
toolbox.id = ["toolbox"];
/* This is somewhat hackish, but takes up fewer lines of code than accomplishing
* the same thing in pure JS. It's all scaffolding anyway. */
toolbox.innerHTML = `
pixelED
black
white
custom
`;
/* elements must be added to the page in order to be queried */
root.appendChild(toolbox);
root.appendChild(workspace);
workspace.appendChild(grid);
workspace.appendChild(grid_caption);
/* initial settings */
setBlack();
document.querySelector("#custom_color").value = custom_color;
/* add events, AFTER added to DOM above */
document.getElementById("rainbow").addEventListener("click", setRainbow);
document.getElementById("darken").addEventListener("click", setDarken);
document.getElementById("lighten").addEventListener("click", setLighten);
document.getElementById("erase").addEventListener("click", clearCanvas);
/* The canvas slider */
document.querySelector("#canvas_size").addEventListener(
"input",
(e) => {
let size = lintCanvasSize(e.target.value);
document.querySelector('#canvas_opts input[type="text"]').value = size;
e.target.value = size;
resetCanvas(size);
}
);
document.querySelector('#canvas_opts > input[type="text"]').addEventListener(
"change",
(e) => {
let size = lintCanvasSize(e.target.value);
document.querySelector("#canvas_size").value = size;
e.target.value = size;
resetCanvas(size);
}
);
document.querySelector('#canvas_opts > input[type="text"]').addEventListener(
"keydown",
(e) => {
let size = lintCanvasSize(e.target.value);
/* todo switch case */
if (e.key == "ArrowDown") {
if (size > 8) {
size -= 1;
e.target.value = size;
document.querySelector("#canvas_size").value = size;
resetCanvas(size);
}
// parseInt(e.target.value) -= 1;
} else if (e.key == "ArrowUp") {
if (size < 100) {
size += 1;
e.target.value = size;
document.querySelector("#canvas_size").value = size;
resetCanvas(size);
}
// parseInt(e.target.value) += 1;
}
}
);
/* The Black color option */
document.querySelectorAll('#black_pick > span').forEach(
(i) => {
i.addEventListener("click", setBlack);
}
);
document.querySelector('#black_pick').addEventListener("click", setBlack);
/* The White color option */
document.querySelectorAll('#white_pick > span').forEach(
(i) => {
i.addEventListener("click", setWhite);
}
);
document.querySelector('#white_pick').addEventListener("click", setWhite);
/* The Custom color option */
document.querySelector('#custom_color').addEventListener("input", setCustom);
document.querySelector('#custom_pick > span').addEventListener("click", setCustom);
document.querySelector('#custom_pick').addEventListener("click", setCustom);
}
function drawColor(e, c) {
switch (draw_state) {
case dstates["BLACK"]:
e.style.backgroundColor = "#000000";
break;
case dstates["WHITE"]:
e.style.backgroundColor = "#ffffff";
break;
case dstates["CUSTOM"]:
current_color = document.querySelector("#custom_color").value;
e.style.backgroundColor = current_color;
break;
case dstates["RAINBOW"]:
e.style.backgroundColor = gen_rainbow();
break;
case dstates["DARKEN"]:
e.style.backgroundColor = gen_darken(e.style.backgroundColor);
break;
case dstates["LIGHTEN"]:
e.style.backgroundColor = gen_lighten(e.style.backgroundColor);
break;
}
}
function setRainbow() {
draw_state = dstates["RAINBOW"];
clearSelected();
document.querySelector("#rainbow").classList.toggle("selected");
}
function setDarken() {
draw_state = dstates["DARKEN"];
clearSelected();
document.querySelector("#darken").classList.toggle("selected");
}
function setLighten() {
draw_state = dstates["LIGHTEN"];
clearSelected();
document.querySelector("#lighten").classList.toggle("selected");
}
function lintCanvasSize(s) {
s = parseInt(s);
if (s < 8) {
s = 8;
} else if(s > 100) {
s = 100;
}
return s;
}
function clearSelected() {
document.querySelectorAll(".selected").forEach(
(i) => {
i.classList.toggle("selected");
}
);
}
function getRGBTriplet(str) {
if (str[0] == "#") {
return hex_to_rgb(str);
}
return str.match(new RegExp("rgb\\((.*)\\)"))[1].split(", ");
}
/* transform a hex color into rgb(x, y, z) format */
function hex_to_rgb(str) {
/* there are two main formats I want to support, though the third would be best, too:
*
* - "#abcdef": the typical hex color
* - "#ade" : shorthand for "#aaddee"
* - "#abcdef99": an alpha-aware hexcode, whose fourth set represents opacity
*/
if (str[0] == "#") {
str = str.substr(1);
console.log(str);
}
switch (str.length) {
case 3:
/* shorthand hex */
break;
case 6:
/* standard hex */
let red = parseInt(str.substr(0,2),16);
let grn = parseInt(str.substr(2,2),16);
let blu = parseInt(str.substr(4,2),16);
return [red, grn, blu];
break;
case 8:
/* 32-bit hex color */
break;
}
}
function rgb_to_hex(str) {
if (str.length == 0) {
return "#ffffff";
}
let trp = getRGBTriplet(str);
let red = parseInt(trp[0],10).toString(16).padStart(2, '0');
let grn = parseInt(trp[1],10).toString(16).padStart(2, '0');
let blu = parseInt(trp[2],10).toString(16).padStart(2, '0');
return `#${red}${grn}${blu}`;
}
function setBlack() {
clearSelected();
document.querySelector("#black_pick").classList.toggle("selected");
draw_state = dstates["BLACK"];
}
function setWhite() {
clearSelected();
document.querySelector("#white_pick").classList.toggle("selected");
draw_state = dstates["WHITE"];
}
/* You have to click the itself to change the color. */
function setCustom() {
clearSelected();
document.querySelector("#custom_pick").classList.toggle("selected");
draw_state = dstates["CUSTOM"];
}
/* generate a random color and return its six digit hex code, with octothorpe (#) */
function gen_rainbow() {
let r = Math.floor(Math.random()*255);
let g = Math.floor(Math.random()*255);
let b = Math.floor(Math.random()*255);
let hr = r.toString(16).padStart(2, '0');
let hg = g.toString(16).padStart(2, '0');
let hb = b.toString(16).padStart(2, '0');
current_color = `#${hr}${hg}${hb}`;
document.querySelector("#custom_color").value = current_color;
return current_color;
}
/* accept a given 'rgb(r, g, b)' sequence, darken each color channel by 26, and return it */
function gen_darken(c) {
/* this happens on un-touched pixels, generally white by convention */
if (!c) {
return "rgb(224, 224, 224)";
}
let nc_array = [];
let c_array = getRGBTriplet(c);
c_array.forEach((i) => {
let num = parseInt(i);
num -= 26;
if (num < 0) {
num = 0;
}
nc_array.push(num);
});
let is = nc_array.join(", ");
return `rgb(${is})`;
}
/* accept a given 'rgb(r, g, b)' sequence, lighten each color channel by 26, and return it */
function gen_lighten(c) {
/* this happens on un-touched pixels, generally white by convention */
if (!c) {
return "rgb(255, 255, 255)";
}
let nc_array = [];
let c_array = getRGBTriplet(c);
c_array.forEach((i) => {
let num = parseInt(i);
num += 26;
if (num > 255) {
num = 255;
}
nc_array.push(num);
});
let is = nc_array.join(", ");
return `rgb(${is})`;
}