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. --- .gitignore | 1 + index.html | 12 ++ mockup.xcf | Bin 0 -> 280260 bytes pixeled.css | 147 +++++++++++++++++++++ pixeled.js | 430 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 590 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 mockup.xcf create mode 100644 pixeled.css create mode 100644 pixeled.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45d62d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.sw? diff --git a/index.html b/index.html new file mode 100644 index 0000000..916aa02 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + PixelED: make simple graphics in your browser! + + + + +
+ + + diff --git a/mockup.xcf b/mockup.xcf new file mode 100644 index 0000000..ede66a2 Binary files /dev/null and b/mockup.xcf differ diff --git a/pixeled.css b/pixeled.css new file mode 100644 index 0000000..6e2da15 --- /dev/null +++ b/pixeled.css @@ -0,0 +1,147 @@ +html, body { + margin: 0; + padding: 0; + user-select: none; + font-size: 1vmin; + color: #fff; +} + +button { + border: 0; + font-size: inherit; + font-family: inherit; + color: inherit; + font-size: 3rem; + margin-collapse: collapse; + text-shadow: 0 0 3px #000; +} + +#app_area { + display: grid; + grid-template-areas: + "bar editor"; + grid-template-columns: minmax(50rem,30vw) auto; + height: 100vh; + width: 100vw; +} + +/* The area behind the grid */ +#workspace { + background: #1e3526 linear-gradient(to right, #000000 -7%, transparent 8%); + grid-area: editor; + display: grid; + overflow: scroll; + grid-template-rows: calc(100% - 7rem) 1fr; +} + +/* the canvas we're drawing on in the app */ +#grid_container { + display: grid; + grid-template-columns: repeat(16, 1fr); + border: 6px solid #000; + /* gap: 1px; */ + background: #888; + aspect-ratio: 1; + box-shadow: 0 0 3.2rem #000; + z-index: 1; + height: 75vmin; + align-self: center; + justify-self: center; +} + +/* 'pixels' on the canvas itself */ +#grid_container > div { + background: white; + aspect-ratio: 1; + border-bottom: 1px solid #888; + border-right: 1px solid #888; + border-collapse: collapse; +} + +#grid_caption { + align-self: center; + justify-self: center; + font-size: 4rem; +} + +#toolbox { + grid-area: bar; + background: #00762a; + border-right: 3px solid #0b3a1d; + display: grid; + box-sizing: border-box; + padding: 0 1em 1em 1em; + font-size: 3rem; + font-family: "PT Mono", "Envy Code R", "Liberation Mono", "DejaVu Sans", monospace; + font-weight: bold; + z-index: 2; + gap: 2rem; + grid-template-rows: 10vh 15vh 20vh 1fr 1fr 1fr 1fr +} + +#toolbox > header { + margin: 0; + padding: 0; + font-size: 3em; + text-align: center; + text-shadow: 0 0 3px #000; + align-self: center; + justify-self: center; +} + +#color_picker { + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +#color_picker > div { + display: grid; + grid-template-rows: 70% 30%; + text-align: center; + font-weight: bold; + font-size: 1.3em; +} + +#color_picker > div.selected { + background: rgba(255,255,255, 0.4); +} + +#color_picker > div > .swatch { + aspect-ratio: 1; + margin: 0.4em auto 0.2em auto; + display: block; + height: 4rem; +} + +#black_pick > .swatch { + background: #000; +} + +#white_pick > .swatch { + background: #fff; +} + +#custom_pick > .swatch { +} + +#canvas_opts { + display: grid; + grid-template-rows: 2.5rem 1fr 1fr; +} + +#rainbow { + background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); + border: 2px solid rgba(0,0,0,0.4); +} + +#darken { + background: #0b3a1d; +} + +#lighten { + background: #20964a; +} + +button.selected { + border: 4px solid #fff !important; +} 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