summaryrefslogtreecommitdiff
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
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.
-rw-r--r--.gitignore1
-rw-r--r--index.html12
-rw-r--r--mockup.xcfbin0 -> 280260 bytes
-rw-r--r--pixeled.css147
-rw-r--r--pixeled.js430
5 files changed, 590 insertions, 0 deletions
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>PixelED: make simple graphics in your browser!</title>
+ <link rel="stylesheet" type="text/css" href="./pixeled.css">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8">
+ </head>
+ <body onload="handler();">
+ <section id="app_area"></section>
+ <script type="text/javascript" src="./pixeled.js"></script>
+ </body>
+</html>
diff --git a/mockup.xcf b/mockup.xcf
new file mode 100644
index 0000000..ede66a2
--- /dev/null
+++ b/mockup.xcf
Binary files 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 = `
+ <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})`;
+}