diff options
-rw-r--r-- | pixeled.css | 177 | ||||
-rw-r--r-- | pixeled.js | 318 | ||||
-rw-r--r-- | ui/black-pen.png | bin | 0 -> 626 bytes | |||
-rw-r--r-- | ui/blank-canvas.png | bin | 0 -> 645 bytes | |||
-rw-r--r-- | ui/clear-screen.png | bin | 0 -> 760 bytes | |||
-rw-r--r-- | ui/custom-color-mask.png | bin | 0 -> 1163 bytes | |||
-rw-r--r-- | ui/custom-color.png | bin | 0 -> 773 bytes | |||
-rw-r--r-- | ui/cyan-button-pressed.png | bin | 0 -> 654 bytes | |||
-rw-r--r-- | ui/cyan-button.png | bin | 0 -> 7164 bytes | |||
-rw-r--r-- | ui/darken-tool.png | bin | 0 -> 608 bytes | |||
-rw-r--r-- | ui/eraser.png | bin | 0 -> 757 bytes | |||
-rw-r--r-- | ui/green-button-pressed.png | bin | 0 -> 654 bytes | |||
-rw-r--r-- | ui/green-button.png | bin | 0 -> 6899 bytes | |||
-rw-r--r-- | ui/grid-toggle.png | bin | 0 -> 565 bytes | |||
-rw-r--r-- | ui/lighten-tool.png | bin | 0 -> 625 bytes | |||
-rw-r--r-- | ui/rainbow-pen.png | bin | 0 -> 694 bytes | |||
-rw-r--r-- | ui/save-file.png | bin | 0 -> 797 bytes | |||
-rw-r--r-- | ui/white-pen.png | bin | 0 -> 635 bytes | |||
-rw-r--r-- | ui/yellow-button-pressed.png | bin | 0 -> 657 bytes | |||
-rw-r--r-- | ui/yellow-button.png | bin | 0 -> 7348 bytes |
20 files changed, 387 insertions, 108 deletions
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; } @@ -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 = ` <header>pixelED</header> - <section id="color_picker"> + <section id="tool_palette"> <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> + <div id="rainbow"> + <span class="swatch"></span> + </div> + <div id="darken"> + <span class="swatch"></span> + </div> + <div id="lighten"> + <span class="swatch"></span> + </div> + <div id="clear_canvas"> + <span class="swatch"></span> + </div> + <div id="grid_toggle" class="active"> + <span class="swatch"></span> </div> </section> <section id="canvas_opts"> @@ -160,35 +199,46 @@ function initialSetup() { <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> - <button id="grid_toggle">grid on</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); + + /* 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<this.#pixel_data.length; i+=3) { + let grid_index = i / 3; + let pixel = getRGBTriplet(grid_area.children[grid_index].style.backgroundColor); + this.#pixel_view.setUint8(i,pixel[0]); + this.#pixel_view.setUint8(i+1,pixel[1]); + this.#pixel_view.setUint8(i+2,pixel[2]); + } + } + } + + // make the global color table, and add color indices to the image data section + build_gct() { + // setup the gct naively + this.theGCT = new Array(); + this.theIDS = new Array(); + grid.childNodes.forEach( + (p) => { + 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<this.#header.length; i++) { + this.raw_binary[i] = this.#header[i].charCodeAt(0); + offset++; + } + // write LS width and height + this.raw_binary[offset] = (this.#width & 255); + offset++; + this.raw_binary[offset] = (this.#width) >> 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 Binary files differnew file mode 100644 index 0000000..3d6af14 --- /dev/null +++ b/ui/black-pen.png diff --git a/ui/blank-canvas.png b/ui/blank-canvas.png Binary files differnew file mode 100644 index 0000000..b086ca7 --- /dev/null +++ b/ui/blank-canvas.png diff --git a/ui/clear-screen.png b/ui/clear-screen.png Binary files differnew file mode 100644 index 0000000..b88c30b --- /dev/null +++ b/ui/clear-screen.png diff --git a/ui/custom-color-mask.png b/ui/custom-color-mask.png Binary files differnew file mode 100644 index 0000000..7eebcb3 --- /dev/null +++ b/ui/custom-color-mask.png diff --git a/ui/custom-color.png b/ui/custom-color.png Binary files differnew file mode 100644 index 0000000..c35ee4c --- /dev/null +++ b/ui/custom-color.png diff --git a/ui/cyan-button-pressed.png b/ui/cyan-button-pressed.png Binary files differnew file mode 100644 index 0000000..13af424 --- /dev/null +++ b/ui/cyan-button-pressed.png diff --git a/ui/cyan-button.png b/ui/cyan-button.png Binary files differnew file mode 100644 index 0000000..4fe6b62 --- /dev/null +++ b/ui/cyan-button.png diff --git a/ui/darken-tool.png b/ui/darken-tool.png Binary files differnew file mode 100644 index 0000000..2bc005c --- /dev/null +++ b/ui/darken-tool.png diff --git a/ui/eraser.png b/ui/eraser.png Binary files differnew file mode 100644 index 0000000..d4d01b0 --- /dev/null +++ b/ui/eraser.png diff --git a/ui/green-button-pressed.png b/ui/green-button-pressed.png Binary files differnew file mode 100644 index 0000000..054ab8d --- /dev/null +++ b/ui/green-button-pressed.png diff --git a/ui/green-button.png b/ui/green-button.png Binary files differnew file mode 100644 index 0000000..d37ce9a --- /dev/null +++ b/ui/green-button.png diff --git a/ui/grid-toggle.png b/ui/grid-toggle.png Binary files differnew file mode 100644 index 0000000..1844aac --- /dev/null +++ b/ui/grid-toggle.png diff --git a/ui/lighten-tool.png b/ui/lighten-tool.png Binary files differnew file mode 100644 index 0000000..4a65072 --- /dev/null +++ b/ui/lighten-tool.png diff --git a/ui/rainbow-pen.png b/ui/rainbow-pen.png Binary files differnew file mode 100644 index 0000000..26ce0b8 --- /dev/null +++ b/ui/rainbow-pen.png diff --git a/ui/save-file.png b/ui/save-file.png Binary files differnew file mode 100644 index 0000000..2df1a01 --- /dev/null +++ b/ui/save-file.png diff --git a/ui/white-pen.png b/ui/white-pen.png Binary files differnew file mode 100644 index 0000000..ef36c29 --- /dev/null +++ b/ui/white-pen.png diff --git a/ui/yellow-button-pressed.png b/ui/yellow-button-pressed.png Binary files differnew file mode 100644 index 0000000..fe637d2 --- /dev/null +++ b/ui/yellow-button-pressed.png diff --git a/ui/yellow-button.png b/ui/yellow-button.png Binary files differnew file mode 100644 index 0000000..bb34269 --- /dev/null +++ b/ui/yellow-button.png |