diff options
Diffstat (limited to '')
-rw-r--r-- | pixeled.js | 318 |
1 files changed, 268 insertions, 50 deletions
@@ -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: */ |