From 7ce4d6f86f6b2233feb7cf332b0e236c76b7ff53 Mon Sep 17 00:00:00 2001 From: zlg Date: Thu, 29 Feb 2024 23:07:52 -0800 Subject: Giant refactor to modern-ish interface Completely changes the interface to something more akin to a modern image editor, with a palette of icons for tools. New icons are in place, with a tool bar that stays inside the viewport at all times. Additionally, drawing behavior more closely matches that of The GIMP, et al in how it handles clicks. Due to how new I am to Javascript and the relative complexity of implementing a pixel editor, I may not offer a mobile version at this time. --- pixeled.css | 177 ++++++++++++++++-------- pixeled.js | 318 ++++++++++++++++++++++++++++++++++++------- ui/black-pen.png | Bin 0 -> 626 bytes ui/blank-canvas.png | Bin 0 -> 645 bytes ui/clear-screen.png | Bin 0 -> 760 bytes ui/custom-color-mask.png | Bin 0 -> 1163 bytes ui/custom-color.png | Bin 0 -> 773 bytes ui/cyan-button-pressed.png | Bin 0 -> 654 bytes ui/cyan-button.png | Bin 0 -> 7164 bytes ui/darken-tool.png | Bin 0 -> 608 bytes ui/eraser.png | Bin 0 -> 757 bytes ui/green-button-pressed.png | Bin 0 -> 654 bytes ui/green-button.png | Bin 0 -> 6899 bytes ui/grid-toggle.png | Bin 0 -> 565 bytes ui/lighten-tool.png | Bin 0 -> 625 bytes ui/rainbow-pen.png | Bin 0 -> 694 bytes ui/save-file.png | Bin 0 -> 797 bytes ui/white-pen.png | Bin 0 -> 635 bytes ui/yellow-button-pressed.png | Bin 0 -> 657 bytes ui/yellow-button.png | Bin 0 -> 7348 bytes 20 files changed, 387 insertions(+), 108 deletions(-) create mode 100644 ui/black-pen.png create mode 100644 ui/blank-canvas.png create mode 100644 ui/clear-screen.png create mode 100644 ui/custom-color-mask.png create mode 100644 ui/custom-color.png create mode 100644 ui/cyan-button-pressed.png create mode 100644 ui/cyan-button.png create mode 100644 ui/darken-tool.png create mode 100644 ui/eraser.png create mode 100644 ui/green-button-pressed.png create mode 100644 ui/green-button.png create mode 100644 ui/grid-toggle.png create mode 100644 ui/lighten-tool.png create mode 100644 ui/rainbow-pen.png create mode 100644 ui/save-file.png create mode 100644 ui/white-pen.png create mode 100644 ui/yellow-button-pressed.png create mode 100644 ui/yellow-button.png diff --git a/pixeled.css b/pixeled.css index 5f02f19..1cef83a 100644 --- a/pixeled.css +++ b/pixeled.css @@ -2,36 +2,26 @@ html, body { margin: 0; padding: 0; user-select: none; - font-size: 1vmin; + font-size: 10px; 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; +body { + overflow: hidden; } #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; + background: #1e3526 linear-gradient(to right, #000000 -7%, transparent 8%, transparent 92%, #000000 107%); display: grid; overflow: scroll; - grid-template-rows: calc(100% - 7rem) 1fr; + z-index: 1; } /* the canvas we're drawing on in the app */ @@ -43,7 +33,7 @@ button { background: #888; aspect-ratio: 1; box-shadow: 0 0 3.2rem #000; - z-index: 1; + z-index: 10; height: 75vmin; align-self: center; justify-self: center; @@ -61,93 +51,164 @@ button { 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; + background: #00000088; + border: 1px solid #000; + border-radius: 8px; display: grid; box-sizing: border-box; - padding: 0 1em 1em 1em; - font-size: 3rem; + box-shadow: 0 0 16px #000; + padding: 6px; + font-size: 14px; 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 1fr; + position: absolute; + z-index: 20; + gap: 8px; + grid-template-rows: 26px 1fr 60px; + top: 16px; + left: 16px; + width: 184px; } #toolbox > header { margin: 0; padding: 0; - font-size: 3em; + font-size: 1.3em; text-align: center; text-shadow: 0 0 3px #000; - align-self: center; - justify-self: center; + line-height: 26px; } -#color_picker { - display: grid; - grid-template-columns: 1fr 1fr 1fr; +#tool_palette { + display: flex; + flex-flow: row wrap; + gap: 3px; } -#color_picker > div { +#tool_palette div { + background: url("./ui/green-button.png") no-repeat top left transparent; display: grid; - grid-template-rows: 70% 30%; text-align: center; font-weight: bold; font-size: 1.3em; + image-rendering: crisp-edges; + flex-basis: 40px; + height: 48px; +} + +#tool_palette div:hover { + background-image: url("./ui/cyan-button.png"); +} + +#tool_palette > div.selected { + background: url("./ui/green-button-pressed.png") no-repeat top left; + image-rendering: crisp-edges; +} + +#tool_palette > div.active { + background: url("./ui/yellow-button-pressed.png") no-repeat top left; + image-rendering: crisp-edges; } -#color_picker > div.selected { - background: rgba(255,255,255, 0.4); +#tool_palette div.selected .swatch, +#tool_palette div.active .swatch { + margin-top: 9px; } -#color_picker > div > .swatch { +#tool_palette > div > .swatch { aspect-ratio: 1; - margin: 0.4em auto 0.2em auto; + margin: 4px auto 0 auto; display: block; - height: 4rem; + width: 32px; + height: 32px; } #black_pick > .swatch { - background: #000; + background: url("./ui/black-pen.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; } #white_pick > .swatch { - background: #fff; + background: url("./ui/eraser.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; } #custom_pick > .swatch { + mask: url("./ui/custom-color-mask.png") no-repeat center center; + mask-size: contain; + border: 0; + padding: 0; } -#canvas_opts { - display: grid; - grid-template-rows: 2.5rem 1fr 1fr; +#custom_color::-moz-color-swatch { + background: url("./ui/custom-color.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; + border: 0; + margin: 0; + padding: 0; +} + +#rainbow > .swatch { + background: url("./ui/rainbow-pen.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; +} + +#darken > .swatch { + background: url("./ui/darken-tool.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; +} + +#lighten > .swatch { + background: url("./ui/lighten-tool.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; +} + +#clear_canvas > .swatch { + background: url("./ui/blank-canvas.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; +} + +#grid_toggle > .swatch { + background: url("./ui/grid-toggle.png") no-repeat center center; + background-size: contain; + image-rendering: crisp-edges; } -#rainbow { - background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); - border: 2px solid rgba(0,0,0,0.4); +#canvas_opts { + display: grid; + grid-template-areas: + "lbl lbl" + "num slider" + ; + grid-template-columns: 2.5em auto; + gap: 0 4px; } -#darken { - background: #0b3a1d; +#canvas_opts > label { + grid-area: lbl; } -#lighten { - background: #20964a; +#canvas_opts input[type=range] { + grid-area: slider; + width: 100%; } -button.selected { - border: 4px solid #fff !important; +#canvas_opts input[type=text] { + grid-area: num; + text-align: center; + background: #fff8; + border: 0; + border-radius: 5px; } -#btn_overlay { +#canvas_opts input[type=text]:focus { + border: 1px solid #fff; } diff --git a/pixeled.js b/pixeled.js index fd6a890..8bf3a94 100644 --- a/pixeled.js +++ b/pixeled.js @@ -30,10 +30,39 @@ * [*] click-and-drag to draw, not hover * [*] right click to copy color under cursor (and switch to it) * - * STRETCH GOAL + * STRETCH GOALS * - * [ ] better UI layout - * [ ] possible export to PNG or GIF format (!!! :D) + * [*] better UI layout + * [ ] possible export to GIF format (!!! :D) + */ + +/* POST-COMPLETION + * + * This project has helped me understand what goes into making modern Web apps, + * and the value that some frameworks can bring when you're building something + * with somewhat complex UI language, like an editing tool. There is a lot of + * event and state syncing, for lack of a better explanation, which increases + * the complexity of the task you're carrying out. Web apps can be convenient + * to visit and use, but building them is a challenge. I think I would be more + * at home in a persistent ticking environment like a video game, or somewhat + * predictable backend code. The moving target of HTML5 and CSS3 and JS, while + * fun in some respects, adds to the ever-increasing complexity that developers + * face when building for the Web. + * + * I would prefer to build on a platform that is less prone to change. There's + * bound to be a middle ground between low-level tedium and a gargantuan + * virtual machine. + */ + +/* CREDITS + * + * Wikipedia for the GIF information + * MDN for endless Web and Javascript info + * The Command Line Fanatic, who wrote two great articles on GIF: + * https://commandlinefanatic.com/cgi-bin/showarticle.cgi?article=art010 + * https://commandlinefanatic.com/cgi-bin/showarticle.cgi?article=art011 + * + * Implementing GIF would have been more of a hassle without these resources. */ /* disable right click's default behavior */ @@ -56,6 +85,7 @@ current_color = "#000000"; custom_color = "#2564b8"; draw_state = dstates["BLACK"]; do_draw = false; +do_move = false; function clean_element(node) { while (node.firstChild) { @@ -79,7 +109,6 @@ function rebuildCanvas(size) { } }); box.addEventListener("mousedown", function(e) { - // console.log(e.target.style.backgroundColor); if (e.which == 3 || e.button == 2) { if (e.target.style.backgroundColor == 'rgb(0, 0, 0)') { setBlack(); @@ -92,21 +121,23 @@ function rebuildCanvas(size) { setCustom(); } } else { - do_draw = true; + // do_draw = true; drawColor(e.target, current_color); } }, { "passive": false, "capture": true } ); - box.addEventListener("mouseup", function(e) { - do_draw = false; - }, { "passive": false } ); + // 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"); - }); + if (window.confirm("Clear the entire canvas?")) { + pixels = document.querySelectorAll("#grid_container div"); + pixels.forEach(function(i) { + i.removeAttribute("style"); + }); + } } function handler() { @@ -122,15 +153,11 @@ function initialSetup() { grid.id = ["grid_container"]; grid.classList = ["show_grid"]; /* 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'; + // grid.addEventListener("mouseleave", + // (e) => { + // do_draw = false; + // }, { "passive": false } + // ); resetCanvas(16); /* we should have 256 divs appended to the grid. */ @@ -141,18 +168,30 @@ function initialSetup() { * the same thing in pure JS. It's all scaffolding anyway. */ toolbox.innerHTML = `
pixelED
-
+
- black
- white
- custom +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
@@ -160,35 +199,46 @@ function initialSetup() {
- - - - - - - - - - - - `; /* 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); + + /* the area behind the grid */ + workspace.addEventListener("mousedown", (e) => { + do_draw = true; + }); + /* the area behind the grid */ + workspace.addEventListener("mouseup", (e) => { + do_draw = false; + }); /* initial settings */ setBlack(); document.querySelector("#custom_color").value = custom_color; + /* click-and-drag for the toolbox */ + document.querySelector("#toolbox header").addEventListener("mousedown", (e) => { + e.preventDefault(); + x1 = e.clientX; + y1 = e.clientY; + xoff = e.clientX - e.target.parentNode.offsetLeft; + yoff = e.clientY - e.target.parentNode.offsetTop; + do_move = true; + document.addEventListener("mousemove", moveToolbox); + document.addEventListener("mouseup", (e) => { + e.preventDefault(); + do_move = false; + }, { once: true }); + }); + /* 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); + document.getElementById("clear_canvas").addEventListener("click", clearCanvas); /* The canvas slider */ document.querySelector("#canvas_size").addEventListener( @@ -253,7 +303,6 @@ function initialSetup() { /* 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); /* The Toggle Grid option */ @@ -304,11 +353,7 @@ function setLighten() { function toggleGrid(e) { document.querySelector("#grid_container").classList.toggle("show_grid"); - if (document.querySelector("#grid_container").classList.contains("show_grid")) { - e.target.textContent = "grid on"; - } else { - e.target.textContent = "grid off"; - } + document.querySelector("#grid_toggle").classList.toggle("active"); } function lintCanvasSize(s) { @@ -329,11 +374,22 @@ function clearSelected() { ); } -function getRGBTriplet(str) { - if (str[0] == "#") { - return hex_to_rgb(str); +function getRGBTriplet(s) { + console.log(s); + if (s.length == 0) { + return [255, 255, 255]; + } + if (s[0] == "#" || s.length == 6) { + return hex_to_rgb(s); } - return str.match(new RegExp("rgb\\((.*)\\)"))[1].split(", "); + triplet = s.match(new RegExp("rgb\\((.*)\\)"))[1].split(", "); + nums = new Array(); + triplet.forEach( + (c) => { + nums.push(Number.parseInt(c)); + } + ); + return nums; } /* transform a hex color into rgb(x, y, z) format */ @@ -346,7 +402,7 @@ function hex_to_rgb(str) { */ if (str[0] == "#") { str = str.substr(1); - console.log(str); + // console.log(str); } switch (str.length) { case 3: @@ -447,3 +503,165 @@ function gen_lighten(c) { let is = nc_array.join(", "); return `rgb(${is})`; } + +function moveToolbox(e) { + if (!do_move) { + return 0; + } + e = e || window.event; + e.preventDefault(); + let tb = document.querySelector("#toolbox"); + // console.log(`toolbar at ${tb.offsetLeft},${tb.offsetTop}`); + tb.style.left = `${e.clientX - xoff}px`; + tb.style.top = `${(e.clientY - yoff)}px`; + boundToolbox(); +} + +function myMid(lowb, num, highb) { + return Math.min(Math.max(lowb, num), highb); +} + +function boundToolbox() { + let tb = document.querySelector("#toolbox"); + let tbh = document.querySelector("#toolbox header"); + let gap = tbh.style.gap; + tb.style.left = `${myMid(0,tb.offsetLeft,(window.innerWidth - tb.offsetWidth))}px`; + tb.style.top = `${myMid(0,tb.offsetTop,(window.innerHeight - (tbh.offsetHeight + 16)))}px`; +} + +/* Implement a simple GIF class to facilitate easy downloading of user's work + * + * Limitations include: + * * no animation + * * no sub-images for complex palettes + * * no transparency + * * no palette correction should the colorspace run out + * + * In short, please only use this as a way to get pixelED art into another + * program. + */ +class GIFImage { + #header = "GIF89a"; + #width; + #height; + #pixel_data; + #pixel_view; + raw_binary; + theGCT; + theIDS; + + constructor(height, width, pdata) { + this.#width = width; + this.#height = height; + this.#pixel_data = new Uint8Array(3*(this.#width*this.#height)); + this.#pixel_view = new DataView(this.#pixel_data.buffer, 0); + this.fill_buffer(pdata); + this.build_gct(); + } + + // fill the data buffer with background color information from this element's children. + fill_buffer(grid_area) { + if (grid_area.children.length != (this.#pixel_data.length / 3) ) { + console.log("pixelED Error: Size mismatch between canvas and GIF buffer size!"); + return; + } else { + // every pixel has three values, thus we can know offsets + for (i=0; i { + let hex = rgb_to_hex(p.style.backgroundColor).substring(1); + if (! this.theGCT.includes(hex)) { + this.theGCT.push(hex); + } + let ci = this.theGCT.indexOf(hex); + this.theIDS.push(ci); + } + ); + } + + gct_to_binary() { + let buf = new Uint8Array(128*3); + for (i=0; (i/3) <= this.theGCT.length; i+=3) { + let rgb = getRGBTriplet(this.theGCT[i/3]); + buf[i] = rgb[0]; + buf[i+1] = rgb[1]; + buf[i+2] = rgb[2]; + } + return new DataView(buf.buffer, 0); + } + + ids_to_binary() { + let buf = new Uint8Array(this.theIDS.length); + for (i=0; i < buf.byteLength; i++) { + buf[i] = this.theIDS[i]; + } + return new DataView(buf.buffer, 0); + } + + // don't hurt me i'm still a babby + writeData() { + // we need to figure out the length of our information before we allocate a buffer. + let size = 0; + size += 6; // header: 'GIF89a' + size += 2; // logical screen width + size += 2; // logical screen height + size += 1; // GCT (Global Color Table) information + // -- u are here + size += 1; // background color index + size += 1; // pixel aspect ratio + size += this.gct_to_binary().byteLength; + // we'll try omitting the GCE + size += 1; // image descriptor ',' + size += 4; // upper-left coord of image in logical screen + size += 4; // image width and height + size += 1; // local color table bit + size += 1; // minimum LZW code size - 1 (7 for 8-bit indices) + size += 1; // CLEAR code (80) + size += this.ids_to_binary().byteLength; + size += 1; // STOP code (81) + size += 1; // last ';' in the file + console.log("we need "+size+" bytes"); + + // do the business + this.raw_binary = new Uint8Array(size); + let offset = 0; + // write header + for (i=0; i> 8; + offset++; + this.raw_binary[offset] = (this.#height & 255); + offset++; + this.raw_binary[offset] = (this.#height) >> 8; + offset++; + + // write BG index and aspect ratio + this.raw_binary[offset++] = 0; + this.raw_binary[offset++] = 0; + + let packed_field = 0b10110110; + + console.log(offset); + } +} + +/* vim: set foldmethod=syntax: */ diff --git a/ui/black-pen.png b/ui/black-pen.png new file mode 100644 index 0000000..3d6af14 Binary files /dev/null and b/ui/black-pen.png differ diff --git a/ui/blank-canvas.png b/ui/blank-canvas.png new file mode 100644 index 0000000..b086ca7 Binary files /dev/null and b/ui/blank-canvas.png differ diff --git a/ui/clear-screen.png b/ui/clear-screen.png new file mode 100644 index 0000000..b88c30b Binary files /dev/null and b/ui/clear-screen.png differ diff --git a/ui/custom-color-mask.png b/ui/custom-color-mask.png new file mode 100644 index 0000000..7eebcb3 Binary files /dev/null and b/ui/custom-color-mask.png differ diff --git a/ui/custom-color.png b/ui/custom-color.png new file mode 100644 index 0000000..c35ee4c Binary files /dev/null and b/ui/custom-color.png differ diff --git a/ui/cyan-button-pressed.png b/ui/cyan-button-pressed.png new file mode 100644 index 0000000..13af424 Binary files /dev/null and b/ui/cyan-button-pressed.png differ diff --git a/ui/cyan-button.png b/ui/cyan-button.png new file mode 100644 index 0000000..4fe6b62 Binary files /dev/null and b/ui/cyan-button.png differ diff --git a/ui/darken-tool.png b/ui/darken-tool.png new file mode 100644 index 0000000..2bc005c Binary files /dev/null and b/ui/darken-tool.png differ diff --git a/ui/eraser.png b/ui/eraser.png new file mode 100644 index 0000000..d4d01b0 Binary files /dev/null and b/ui/eraser.png differ diff --git a/ui/green-button-pressed.png b/ui/green-button-pressed.png new file mode 100644 index 0000000..054ab8d Binary files /dev/null and b/ui/green-button-pressed.png differ diff --git a/ui/green-button.png b/ui/green-button.png new file mode 100644 index 0000000..d37ce9a Binary files /dev/null and b/ui/green-button.png differ diff --git a/ui/grid-toggle.png b/ui/grid-toggle.png new file mode 100644 index 0000000..1844aac Binary files /dev/null and b/ui/grid-toggle.png differ diff --git a/ui/lighten-tool.png b/ui/lighten-tool.png new file mode 100644 index 0000000..4a65072 Binary files /dev/null and b/ui/lighten-tool.png differ diff --git a/ui/rainbow-pen.png b/ui/rainbow-pen.png new file mode 100644 index 0000000..26ce0b8 Binary files /dev/null and b/ui/rainbow-pen.png differ diff --git a/ui/save-file.png b/ui/save-file.png new file mode 100644 index 0000000..2df1a01 Binary files /dev/null and b/ui/save-file.png differ diff --git a/ui/white-pen.png b/ui/white-pen.png new file mode 100644 index 0000000..ef36c29 Binary files /dev/null and b/ui/white-pen.png differ diff --git a/ui/yellow-button-pressed.png b/ui/yellow-button-pressed.png new file mode 100644 index 0000000..fe637d2 Binary files /dev/null and b/ui/yellow-button-pressed.png differ diff --git a/ui/yellow-button.png b/ui/yellow-button.png new file mode 100644 index 0000000..bb34269 Binary files /dev/null and b/ui/yellow-button.png differ -- cgit v1.2.3-54-g00ecf