/* * 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})`; }