about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <hi@eli.li>2024-01-29 14:14:18 -0500
committerelioat <hi@eli.li>2024-01-29 14:14:18 -0500
commit0d7b8bddebc31146192de8761c262ee3ed9c6824 (patch)
tree3a00586b02890ba0db7e6c056080bf1e1aa3e9e1
parente3c83fd71f5bb82407c5578195cda7780ae922c0 (diff)
downloadtour-0d7b8bddebc31146192de8761c262ee3ed9c6824.tar.gz
*
-rw-r--r--js/pixel-art/app.js639
-rw-r--r--js/pixel-art/index.html2
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">