From e113ad82ea880ddc8c2e196755e9d9e236ef95b8 Mon Sep 17 00:00:00 2001 From: zlg Date: Tue, 29 Aug 2023 06:28:04 -0700 Subject: Initial commit This pixel editor is a sort of 'proof of concept' for a Javascript app. It has support for three primary colors (black, white, custom), a darken and lighten tool, a rainbow tool, and you can right click to capture a color in the custom color box. Quite handy for simple little doodles. Future work will be done mostly in the CSS for the UI layout. --- pixeled.js | 430 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 pixeled.js (limited to 'pixeled.js') diff --git a/pixeled.js b/pixeled.js new file mode 100644 index 0000000..731f2e0 --- /dev/null +++ b/pixeled.js @@ -0,0 +1,430 @@ +/* + * 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})`; +} -- cgit v1.2.3-54-g00ecf