From e113ad82ea880ddc8c2e196755e9d9e236ef95b8 Mon Sep 17 00:00:00 2001 From: zlg Date: Tue, 29 Aug 2023 06:28:04 -0700 Subject: 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. --- .gitignore | 1 + index.html | 12 ++ mockup.xcf | Bin 0 -> 280260 bytes pixeled.css | 147 +++++++++++++++++++++ pixeled.js | 430 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 590 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 mockup.xcf create mode 100644 pixeled.css create mode 100644 pixeled.js 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 @@ + + + + PixelED: make simple graphics in your browser! + + + + +
+ + + diff --git a/mockup.xcf b/mockup.xcf new file mode 100644 index 0000000..ede66a2 Binary files /dev/null and b/mockup.xcf 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 = ` +
pixelED
+
+
+ + black +
+
+ + white +
+
+ + custom +
+
+
+ + + +
+ + + + + + + + + + + + `; + + /* 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 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})`; +} -- cgit v1.2.3-54-g00ecf >+19 Unlike the old version of vgstash, the new one does not accept row IDs as arguments for removal. Instead, it accepts two mandatory arguments: the title of the game, and the system it's on. This is in line with the database itself, using the title and system as primary keys. 2018-10-06Remove ID field from DBzlg3-38/+46 The sqlite database already uses a game's title and system as the primary keys. Row IDs are redundant. 2018-10-06cli: change "Status" heading to "Progress"zlg2-36/+40 2018-09-29Bump to 0.3alpha5 for PyPIzlg1-1/+1 2018-09-29cli: Add pretty printing to 'list' commandzlg3-17/+107 Also add the "--width" option to specify the maximum width of the table. 2018-09-08setup.py: Bump to alpha4 for PyPIzlg1-1/+1 2018-09-08cli: add '--raw' option to list commandzlg2-9/+45 Add '--raw' option to the list command, in addition to proper note expansion. Newline characters in notes are escaped to be friendly to scripting. This option may be shortened to '-r' at the user's convenience. In raw output mode, the information is formatted in plain pipe-delimited strings, one line per row: title|system|ownership|progress|notes ownership and progress are printed in their numeric form, consistent with the OWNERSHIP and PROGRESS dictionaries in the vgstash package. An empty notes field will result in a line ending with a pipe and no whitespace following it. 2018-09-08Add remaining filters to vgstash packagezlg1-2/+11 2018-09-04Update LICENSE to match setup.pyzlg1-80/+67 Whoops. 2018-09-03Branch off from master with pytest, tox, clickzlg16-778/+779 This commit is huge, but contains everything needed for a "proper" build system built on pytest + tox and a CLI built with click. For now, this branch will contain all new vgstash development activity until it reaches feature parity with master. The CLI is installed to pip's PATH. Only the 'init', 'add', and 'list' commands work, with only two filters. This is pre-alpha software, and is therefore not stable yet. 2018-03-18Flesh out filter types and ownership statuszlg3-82/+144 It's time for a refactor to a module; the functionality and interface are clashing. 2018-03-18README.mdown: break line correctlyzlg1-1/+1 2018-03-18add 'playlog' list filterzlg2-2/+9 This filter is used to get an idea of which games you're currently playing through, so you can prioritize games to play when you're bored and detect it when you've beaten a game but haven't marked it as such. 2018-03-13Update helpers a bitzlg1-2/+9 At present, user modification is needed to make these seamless. vgup() may need to be axed in favor of telling the user to make an alias. 2018-03-13Make VGSTASH_DB_LOCATION point to a filezlg2-21/+20 It used to point to a directory, which would then look for .vgstash.db. This behavior was kind of backwards and I don't remember why I did it that way. This change gives users more control over where they put their DB. Be sure to update your environment variable if you have it set! 2016-11-18Remove settings from helpers.shZe Libertine Gamer1-5/+0 Sourcing them in .bash_profile screws up login if they're set. 2016-11-15Correct phrasing in README.Ze Libertine Gamer1-4/+4 2016-11-13DerpZe Libertine Gamer1-0/+1 2016-11-03Improve error handling in shell scriptsZe Libertine Gamer4-3/+23 2016-10-24Correct run_again, add recursionZe Libertine Gamer1-0/+4 Loops and functions -- oh my, what a useful combination. :) 2016-10-21Add quotes to correct behavior for arglistZe Libertine Gamer1-1/+1 2016-10-14updater.sh: add recursion, error handlingZe Libertine Gamer1-43/+101 2016-10-14Correct pipe-handling behaviorZe Libertine Gamer1-1/+9 2016-10-12Clarify a method to move between platformsZe Libertine Gamer1-2/+5 Also correct a typo.