summaryrefslogtreecommitdiff
path: root/pixeled.js
diff options
context:
space:
mode:
authorzlg <zlg@zlg.space>2023-08-29 06:28:04 -0700
committerzlg <zlg@zlg.space>2023-08-29 06:28:04 -0700
commite113ad82ea880ddc8c2e196755e9d9e236ef95b8 (patch)
treeea071345512f7dced8e2f16b377e21fb7af2a1c9 /pixeled.js
downloadpixeled-js-e113ad82ea880ddc8c2e196755e9d9e236ef95b8.tar.gz
pixeled-js-e113ad82ea880ddc8c2e196755e9d9e236ef95b8.tar.bz2
pixeled-js-e113ad82ea880ddc8c2e196755e9d9e236ef95b8.tar.xz
pixeled-js-e113ad82ea880ddc8c2e196755e9d9e236ef95b8.zip
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.
Diffstat (limited to 'pixeled.js')
-rw-r--r--pixeled.js430
1 files changed, 430 insertions, 0 deletions
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 = `
+ <header>pixelED</header>
+ <section id="color_picker">
+ <div id="black_pick">
+ <span class="swatch"></span>
+ <span class="sw_label">black</span>
+ </div>
+ <div id="white_pick">
+ <span class="swatch"></span>
+ <span class="sw_label">white</span>
+ </div>
+ <div id="custom_pick">
+ <input class="swatch" type="color" value="" name="custom_color" id="custom_color"></input>
+ <span class="sw_label">custom</span>
+ </div>
+ </section>
+ <section id="canvas_opts">
+ <label>canvas size</label>
+ <input type="range" min="8" max="100" value="16" name="canvas_size" id="canvas_size" list="sizes"></input>
+ <input type="text" value="16"></input>
+ </section>
+ <button id="rainbow">rainbow</button>
+ <button id="darken">darken</button>
+ <button id="lighten">lighten</button>
+ <button id="erase">erase</button>
+ <datalist id="sizes">
+ <option value="16"></option>
+ <option value="32"></option>
+ <option value="48"></option>
+ <option value="64"></option>
+ <option value="80"></option>
+ </datalist>
+ `;
+
+ /* 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 <input> 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})`;
+}