diff options
-rw-r--r-- | js/pixel-art/app.js | 639 | ||||
-rw-r--r-- | js/pixel-art/index.html | 2 |
2 files changed, 370 insertions, 271 deletions
diff --git a/js/pixel-art/app.js b/js/pixel-art/app.js index 5a17ba1..9e501fe 100644 --- a/js/pixel-art/app.js +++ b/js/pixel-art/app.js @@ -1,344 +1,443 @@ -// mostly from https://eloquentjavascript.net/19_paint.html - -class Picture { - constructor(width, height, pixels) { - this.width = width; - this.height = height; - this.pixels = pixels; - } - static empty(width, height, color) { - let pixels = new Array(width * height).fill(color); - return new Picture(width, height, pixels); - } - pixel(x, y) { - return this.pixels[x + y * this.width]; - } - draw(pixels) { - let copy = this.pixels.slice(); - for (let {x, y, color} of pixels) { - copy[x + y * this.width] = color; - } - return new Picture(this.width, this.height, copy); - } +// Inspired by https://eloquentjavascript.net/19_paint.html + +class Picture { // Honestly? I hate classes in javascript, if I were doing this not from a book I wouldn't do this like this + constructor(width, height, pixels) { + this.width = width; + this.height = height; + this.pixels = pixels; + } + static empty(width, height, color) { + let pixels = new Array(width * height).fill(color); + return new Picture(width, height, pixels); + } + pixel(x, y) { + return this.pixels[x + y * this.width]; + } + draw(pixels) { + let copy = this.pixels.slice(); + for (let { + x, + y, + color + } + of pixels) { + copy[x + y * this.width] = color; + } + return new Picture(this.width, this.height, copy); + } } function updateState(state, action) { - return Object.assign({}, state, action); + return { + ...state, + ...action + }; } function elt(type, props, ...children) { - let dom = document.createElement(type); - if (props) Object.assign(dom, props); - for (let child of children) { - if (typeof child != "string") dom.appendChild(child); - else dom.appendChild(document.createTextNode(child)); - } - return dom; + let dom = document.createElement(type); + if (props) Object.assign(dom, props); + for (let child of children) { + if (typeof child != "string") dom.appendChild(child); + else dom.appendChild(document.createTextNode(child)); + } + return dom; } const scale = 10; class PictureCanvas { - constructor(picture, pointerDown) { - this.dom = elt("canvas", { - onmousedown: event => this.mouse(event, pointerDown), - ontouchstart: event => this.touch(event, pointerDown) - }); - this.syncState(picture); - } - syncState(picture) { - if (this.picture == picture) return; - this.picture = picture; - drawPicture(this.picture, this.dom, scale); - } + constructor(picture, pointerDown) { + this.dom = elt("canvas", { + onmousedown: event => this.mouse(event, pointerDown), + ontouchstart: event => this.touch(event, pointerDown) + }); + this.syncState(picture); + } + syncState(picture) { + if (this.picture == picture) return; + this.picture = picture; + drawPicture(this.picture, this.dom, scale); + } } function drawPicture(picture, canvas, scale) { - canvas.width = picture.width * scale; - canvas.height = picture.height * scale; - let cx = canvas.getContext("2d"); - - for (let y = 0; y < picture.height; y++) { - for (let x = 0; x < picture.width; x++) { - cx.fillStyle = picture.pixel(x, y); - cx.fillRect(x * scale, y * scale, scale, scale); - } - } + canvas.width = picture.width * scale; + canvas.height = picture.height * scale; + let cx = canvas.getContext("2d"); + + picture.pixels.forEach((color, index) => { + let x = index % picture.width; + let y = Math.floor(index / picture.width); + cx.fillStyle = color; + cx.fillRect(x * scale, y * scale, scale, scale); + }); } PictureCanvas.prototype.mouse = function(downEvent, onDown) { - if (downEvent.button != 0) return; - let pos = pointerPosition(downEvent, this.dom); - let onMove = onDown(pos); - if (!onMove) return; - let move = moveEvent => { - if (moveEvent.buttons == 0) { - this.dom.removeEventListener("mousemove", move); - } else { - let newPos = pointerPosition(moveEvent, this.dom); - if (newPos.x == pos.x && newPos.y == pos.y) return; - pos = newPos; - onMove(newPos); - } - }; - this.dom.addEventListener("mousemove", move); + if (downEvent.button != 0) return; + let pos = pointerPosition(downEvent, this.dom); + let onMove = onDown(pos); + if (!onMove) return; + let move = moveEvent => { + if (moveEvent.buttons == 0) { + this.dom.removeEventListener("mousemove", move); + } else { + let newPos = pointerPosition(moveEvent, this.dom); + if (newPos.x == pos.x && newPos.y == pos.y) return; + pos = newPos; + onMove(newPos); + } + }; + this.dom.addEventListener("mousemove", move); }; function pointerPosition(pos, domNode) { - let rect = domNode.getBoundingClientRect(); - return {x: Math.floor((pos.clientX - rect.left) / scale), - y: Math.floor((pos.clientY - rect.top) / scale)}; + let rect = domNode.getBoundingClientRect(); + return { + x: Math.floor((pos.clientX - rect.left) / scale), + y: Math.floor((pos.clientY - rect.top) / scale) + }; } PictureCanvas.prototype.touch = function(startEvent, - onDown) { - let pos = pointerPosition(startEvent.touches[0], this.dom); - let onMove = onDown(pos); - startEvent.preventDefault(); - if (!onMove) return; - let move = moveEvent => { - let newPos = pointerPosition(moveEvent.touches[0], - this.dom); - if (newPos.x == pos.x && newPos.y == pos.y) return; - pos = newPos; - onMove(newPos); - }; - let end = () => { - this.dom.removeEventListener("touchmove", move); - this.dom.removeEventListener("touchend", end); - }; - this.dom.addEventListener("touchmove", move); - this.dom.addEventListener("touchend", end); + onDown) { + let pos = pointerPosition(startEvent.touches[0], this.dom); + let onMove = onDown(pos); + startEvent.preventDefault(); + if (!onMove) return; + let move = moveEvent => { + let newPos = pointerPosition(moveEvent.touches[0], + this.dom); + if (newPos.x == pos.x && newPos.y == pos.y) return; + pos = newPos; + onMove(newPos); + }; + let end = () => { + this.dom.removeEventListener("touchmove", move); + this.dom.removeEventListener("touchend", end); + }; + this.dom.addEventListener("touchmove", move); + this.dom.addEventListener("touchend", end); }; class PixelEditor { - constructor(state, config) { - let {tools, controls, dispatch} = config; - this.state = state; - - this.canvas = new PictureCanvas(state.picture, pos => { - let tool = tools[this.state.tool]; - let onMove = tool(pos, this.state, dispatch); - if (onMove) return pos => onMove(pos, this.state); - }); - this.controls = controls.map( - Control => new Control(state, config)); - this.dom = elt("div", {}, this.canvas.dom, elt("br"), - ...this.controls.reduce( - (a, c) => a.concat(" ", c.dom), [])); - } - syncState(state) { - this.state = state; - this.canvas.syncState(state.picture); - for (let ctrl of this.controls) ctrl.syncState(state); - } + constructor(state, config) { + let { + tools, + controls, + dispatch + } = config; + this.state = state; + + this.canvas = new PictureCanvas(state.picture, pos => { + let tool = tools[this.state.tool]; + let onMove = tool(pos, this.state, dispatch); + if (onMove) return pos => onMove(pos, this.state); + }); + this.controls = controls.map( + Control => new Control(state, config)); + this.dom = elt("div", {}, this.canvas.dom, elt("br"), + ...this.controls.reduce( + (a, c) => a.concat(" ", c.dom), [])); + } + syncState(state) { + this.state = state; + this.canvas.syncState(state.picture); + for (let ctrl of this.controls) ctrl.syncState(state); + } } class ToolSelect { - constructor(state, {tools, dispatch}) { - this.select = elt("select", { - onchange: () => dispatch({tool: this.select.value}) - }, ...Object.keys(tools).map(name => elt("option", { - selected: name == state.tool - }, name))); - this.dom = elt("label", null, "🖌 Tool: ", this.select); - } - syncState(state) { this.select.value = state.tool; } + constructor(state, { + tools, + dispatch + }) { + this.select = elt("select", { + onchange: () => dispatch({ + tool: this.select.value + }) + }, ...Object.keys(tools).map(name => elt("option", { + selected: name == state.tool + }, name))); + this.dom = elt("label", null, "🖌 Tool: ", this.select); + } + syncState(state) { + this.select.value = state.tool; + } } class ColorSelect { - constructor(state, {dispatch}) { - this.input = elt("input", { - type: "color", - value: state.color, - onchange: () => dispatch({color: this.input.value}) - }); - this.dom = elt("label", null, "🎨 Color: ", this.input); - } - syncState(state) { this.input.value = state.color; } + constructor(state, { + dispatch + }) { + this.input = elt("input", { + type: "color", + value: state.color, + onchange: () => dispatch({ + color: this.input.value + }) + }); + this.dom = elt("label", null, "🎨 Color: ", this.input); + } + syncState(state) { + this.input.value = state.color; + } } function draw(pos, state, dispatch) { - function drawPixel({x, y}, state) { - let drawn = {x, y, color: state.color}; - dispatch({picture: state.picture.draw([drawn])}); - } - drawPixel(pos, state); - return drawPixel; + function drawPixel({ + x, + y + }, state) { + let drawn = { + x, + y, + color: state.color + }; + dispatch({ + picture: state.picture.draw([drawn]) + }); + } + drawPixel(pos, state); + return drawPixel; } function rectangle(start, state, dispatch) { - function drawRectangle(pos) { - let xStart = Math.min(start.x, pos.x); - let yStart = Math.min(start.y, pos.y); - let xEnd = Math.max(start.x, pos.x); - let yEnd = Math.max(start.y, pos.y); - let drawn = []; - for (let y = yStart; y <= yEnd; y++) { - for (let x = xStart; x <= xEnd; x++) { - drawn.push({x, y, color: state.color}); - } - } - dispatch({picture: state.picture.draw(drawn)}); - } - drawRectangle(start); - return drawRectangle; + function drawRectangle(pos) { + let xStart = Math.min(start.x, pos.x); + let yStart = Math.min(start.y, pos.y); + let xEnd = Math.max(start.x, pos.x); + let yEnd = Math.max(start.y, pos.y); + let drawn = []; + for (let y = yStart; y <= yEnd; y++) { + for (let x = xStart; x <= xEnd; x++) { + drawn.push({ + x, + y, + color: state.color + }); + } + } + dispatch({ + picture: state.picture.draw(drawn) + }); + } + drawRectangle(start); + return drawRectangle; } -const around = [{dx: -1, dy: 0}, {dx: 1, dy: 0}, - {dx: 0, dy: -1}, {dx: 0, dy: 1}]; - -function fill({x, y}, state, dispatch) { - let targetColor = state.picture.pixel(x, y); - let drawn = [{x, y, color: state.color}]; - for (let done = 0; done < drawn.length; done++) { - for (let {dx, dy} of around) { - let x = drawn[done].x + dx, y = drawn[done].y + dy; - if (x >= 0 && x < state.picture.width && - y >= 0 && y < state.picture.height && - state.picture.pixel(x, y) == targetColor && - !drawn.some(p => p.x == x && p.y == y)) { - drawn.push({x, y, color: state.color}); - } - } - } - dispatch({picture: state.picture.draw(drawn)}); +const around = [{ + dx: -1, + dy: 0 + }, { + dx: 1, + dy: 0 + }, + { + dx: 0, + dy: -1 + }, { + dx: 0, + dy: 1 + } +]; + +function fill({ + x, + y +}, state, dispatch) { + let targetColor = state.picture.pixel(x, y); + let drawn = [{ + x, + y, + color: state.color + }]; + for (let done = 0; done < drawn.length; done++) { + for (let { + dx, + dy + } + of around) { + let x = drawn[done].x + dx, + y = drawn[done].y + dy; + if (x >= 0 && x < state.picture.width && + y >= 0 && y < state.picture.height && + state.picture.pixel(x, y) == targetColor && + !drawn.some(p => p.x == x && p.y == y)) { + drawn.push({ + x, + y, + color: state.color + }); + } + } + } + dispatch({ + picture: state.picture.draw(drawn) + }); } function pick(pos, state, dispatch) { - dispatch({color: state.picture.pixel(pos.x, pos.y)}); + dispatch({ + color: state.picture.pixel(pos.x, pos.y) + }); } class SaveButton { - constructor(state) { - this.picture = state.picture; - this.dom = elt("button", { - onclick: () => this.save() - }, "💾 Save"); - } - save() { - let canvas = elt("canvas"); - drawPicture(this.picture, canvas, 1); - let link = elt("a", { - href: canvas.toDataURL(), - download: "pixelart.png" - }); - document.body.appendChild(link); - link.click(); - link.remove(); - } - syncState(state) { this.picture = state.picture; } + constructor(state) { + this.picture = state.picture; + this.dom = elt("button", { + onclick: () => this.save() + }, "💾 Save"); + } + save() { + let canvas = elt("canvas"); + drawPicture(this.picture, canvas, 1); + let link = elt("a", { + href: canvas.toDataURL(), + download: "pixelart.png" + }); + document.body.appendChild(link); + link.click(); + link.remove(); + } + syncState(state) { + this.picture = state.picture; + } } class LoadButton { - constructor(_, {dispatch}) { - this.dom = elt("button", { - onclick: () => startLoad(dispatch) - }, "📁 Load"); - } - syncState() {} + constructor(_, { + dispatch + }) { + this.dom = elt("button", { + onclick: () => startLoad(dispatch) + }, "📁 Load"); + } + syncState() {} } function startLoad(dispatch) { - let input = elt("input", { - type: "file", - onchange: () => finishLoad(input.files[0], dispatch) - }); - document.body.appendChild(input); - input.click(); - input.remove(); + let input = elt("input", { + type: "file", + onchange: () => finishLoad(input.files[0], dispatch) + }); + document.body.appendChild(input); + input.click(); + input.remove(); } function finishLoad(file, dispatch) { - if (file == null) return; - let reader = new FileReader(); - reader.addEventListener("load", () => { - let image = elt("img", { - onload: () => dispatch({ - picture: pictureFromImage(image) - }), - src: reader.result - }); - }); - reader.readAsDataURL(file); + if (file == null) return; + let reader = new FileReader(); + reader.addEventListener("load", () => { + let image = elt("img", { + onload: () => dispatch({ + picture: pictureFromImage(image) + }), + src: reader.result + }); + }); + reader.readAsDataURL(file); } function pictureFromImage(image) { - let width = Math.min(100, image.width); - let height = Math.min(100, image.height); - let canvas = elt("canvas", {width, height}); - let cx = canvas.getContext("2d"); - cx.drawImage(image, 0, 0); - let pixels = []; - let {data} = cx.getImageData(0, 0, width, height); - - function hex(n) { - return n.toString(16).padStart(2, "0"); - } - for (let i = 0; i < data.length; i += 4) { - let [r, g, b] = data.slice(i, i + 3); - pixels.push("#" + hex(r) + hex(g) + hex(b)); - } - return new Picture(width, height, pixels); + let width = Math.min(100, image.width); + let height = Math.min(100, image.height); + let canvas = elt("canvas", { + width, + height + }); + let cx = canvas.getContext("2d"); + cx.drawImage(image, 0, 0); + let pixels = []; + let { + data + } = cx.getImageData(0, 0, width, height); + + function hex(n) { + return n.toString(16).padStart(2, "0"); + } + for (let i = 0; i < data.length; i += 4) { + let [r, g, b] = data.slice(i, i + 3); + pixels.push("#" + hex(r) + hex(g) + hex(b)); + } + return new Picture(width, height, pixels); } function historyUpdateState(state, action) { - if (action.undo == true) { - if (state.done.length == 0) return state; - return Object.assign({}, state, { - picture: state.done[0], - done: state.done.slice(1), - doneAt: 0 - }); - } else if (action.picture && - state.doneAt < Date.now() - 1000) { - return Object.assign({}, state, action, { - done: [state.picture, ...state.done], - doneAt: Date.now() - }); - } else { - return Object.assign({}, state, action); - } + if (action.undo == true) { + if (state.done.length == 0) return state; + return { + ...state, + picture: state.done[0], + done: state.done.slice(1), + doneAt: 0 + }; + } else if (action.picture && state.doneAt < Date.now() - 1000) { + return { + ...state, + ...action, + done: [state.picture, ...state.done], + doneAt: Date.now() + }; + } else { + return { + ...state, + ...action + }; + } } class UndoButton { - constructor(state, {dispatch}) { - this.dom = elt("button", { - onclick: () => dispatch({undo: true}), - disabled: state.done.length == 0 - }, "⮪ Undo"); - } - syncState(state) { - this.dom.disabled = state.done.length == 0; - } + constructor(state, { + dispatch + }) { + this.dom = elt("button", { + onclick: () => dispatch({ + undo: true + }), + disabled: state.done.length == 0 + }, "⮪ Undo"); + } + syncState(state) { + this.dom.disabled = state.done.length == 0; + } } const startState = { - tool: "draw", - color: "#000000", - picture: Picture.empty(16, 16, "#f0f0f0"), - done: [], - doneAt: 0 + tool: "draw", + color: "#000000", + picture: Picture.empty(16, 16, "#f0f0f0"), + done: [], + doneAt: 0 }; -const baseTools = {draw, fill, rectangle, pick}; +const baseTools = { + draw, + fill, + rectangle, + pick +}; const baseControls = [ - ToolSelect, ColorSelect, SaveButton, LoadButton, UndoButton + ToolSelect, ColorSelect, SaveButton, LoadButton, UndoButton ]; -function startPixelEditor({state = startState, - tools = baseTools, - controls = baseControls}) { - let app = new PixelEditor(state, { - tools, - controls, - dispatch(action) { - state = historyUpdateState(state, action); - app.syncState(state); - } - }); - return app.dom; -} - +function startPixelEditor({ + state = startState, + tools = baseTools, + controls = baseControls +}) { + let app = new PixelEditor(state, { + tools, + controls, + dispatch(action) { + state = historyUpdateState(state, action); + app.syncState(state); + } + }); + return app.dom; +} \ No newline at end of file diff --git a/js/pixel-art/index.html b/js/pixel-art/index.html index 3878ad7..abae244 100644 --- a/js/pixel-art/index.html +++ b/js/pixel-art/index.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html> +<html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> |