From 0d7b8bddebc31146192de8761c262ee3ed9c6824 Mon Sep 17 00:00:00 2001 From: elioat Date: Mon, 29 Jan 2024 14:14:18 -0500 Subject: * --- js/pixel-art/app.js | 639 ++++++++++++++++++++++++++++-------------------- 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 @@ - + -- cgit 1.4.1-2-gfad0 ^
50f9f94b ^
1fc1d8af ^


b223937f ^
03413d1c ^
1fc1d8af ^




















1367261d ^
1fc1d8af ^







e087f6d4
2d161b7d ^
1fc1d8af ^










2d161b7d ^
e087f6d4
1fc1d8af ^



































e087f6d4


1fc1d8af ^



d326f24d ^
1fc1d8af ^



d326f24d ^
1fc1d8af ^

d326f24d ^
1fc1d8af ^

d326f24d ^
1fc1d8af ^



d326f24d ^
1fc1d8af ^
d326f24d ^
1fc1d8af ^

d326f24d ^
1fc1d8af ^


2d161b7d ^
1fc1d8af ^






b223937f ^
d61bc4e5 ^
1fc1d8af ^
b223937f ^
327b7c16 ^
1fc1d8af ^
383b3f2d ^

1fc1d8af ^
383b3f2d ^
a2a1ab1e ^
1fc1d8af ^



a2a1ab1e ^
1fc1d8af ^



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290