summaryrefslogtreecommitdiff
path: root/pixeled.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pixeled.js318
1 files changed, 268 insertions, 50 deletions
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 = `
<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: */