about summary refs log tree commit diff stats
path: root/js/pixel-art
diff options
context:
space:
mode:
Diffstat (limited to 'js/pixel-art')
-rw-r--r--js/pixel-art/app.js683
-rw-r--r--js/pixel-art/apple-icon-180x180.pngbin0 -> 11517 bytes
-rw-r--r--js/pixel-art/index.html27
-rw-r--r--js/pixel-art/pixel/app.js561
-rw-r--r--js/pixel-art/pixel/index.html213
5 files changed, 1107 insertions, 377 deletions
diff --git a/js/pixel-art/app.js b/js/pixel-art/app.js
index 9e501fe..816ee09 100644
--- a/js/pixel-art/app.js
+++ b/js/pixel-art/app.js
@@ -1,443 +1,388 @@
-// 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);
-	}
+// 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);
+  }
 }
 
 function updateState(state, action) {
-	return {
-		...state,
-		...action
-	};
+  return Object.assign({}, 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;
+const scale = 38;
 
 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");
-
-	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);
-	});
+  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);
+    }
+  }
 }
 
-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);
+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);
 };
 
 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);
+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);
 };
 
 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
-	}
+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 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(16, image.width);
+  let height = Math.min(16, 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 {
-			...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
-		};
-	}
+  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);
+  }
 }
 
-
 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
+  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
+  let app = new PixelEditor(state, {
+    tools,
+    controls,
+    dispatch(action) {
+      state = historyUpdateState(state, action);
+      app.syncState(state);
+    },
+  });
+  return app.dom;
+}
diff --git a/js/pixel-art/apple-icon-180x180.png b/js/pixel-art/apple-icon-180x180.png
new file mode 100644
index 0000000..22e9372
--- /dev/null
+++ b/js/pixel-art/apple-icon-180x180.png
Binary files differdiff --git a/js/pixel-art/index.html b/js/pixel-art/index.html
index abae244..87f240e 100644
--- a/js/pixel-art/index.html
+++ b/js/pixel-art/index.html
@@ -6,19 +6,30 @@
 	<title>pixel art</title>
 	<script type="text/javascript" src="app.js"></script>
 	<style type="text/css">
-		.editor {
-			max-width: 80em;
-			padding: 1em;
+		body {
+			padding: 1.75em 1em;
 			margin: 0 auto;
 			display: block;
 		}
 	</style>
+	<link rel="icon" href="apple-icon-180x180.png" type="image/x-icon">
 </head>
 <body>
-<div class="editor"></div>
-<script>
-  document.querySelector("div")
-    .appendChild(startPixelEditor({}));
-</script>
+	<!-- <input type="number" name="size" value="16" min="8" max="200"> -->
+	<div class="editor"></div>
+	<script>
+	  document.querySelector("div")
+	    .appendChild(startPixelEditor({}));
+	</script>
+	<p><a href="./pixel/index.html">Check out a more advanced pixel art tool that I made after this one.</a></p>
+	<p>
+		Gameboy colors:
+		<ul>
+			<li><span style="background-color: #071821;">&nbsp;&nbsp;&nbsp;&nbsp;</span>   <code>#071821</code></li>
+			<li><span style="background-color: #86c06c;">&nbsp;&nbsp;&nbsp;&nbsp;</span>   <code>#86c06c</code></li>
+			<li><span style="background-color: #e0f8cf;">&nbsp;&nbsp;&nbsp;&nbsp;</span>   <code>#e0f8cf</code></li>
+			<li><span style="background-color: #65ff00;">&nbsp;&nbsp;&nbsp;&nbsp;</span>   <code>#65ff00</code></li>
+		</ul>
+	</p>
 </body>
 </html>
\ No newline at end of file
diff --git a/js/pixel-art/pixel/app.js b/js/pixel-art/pixel/app.js
new file mode 100644
index 0000000..2d83997
--- /dev/null
+++ b/js/pixel-art/pixel/app.js
@@ -0,0 +1,561 @@
+const canvas = document.getElementById('canvas');
+const ctx = canvas.getContext('2d');
+const defaultGridWidth = 16;
+const defaultGridHeight = 16;
+let gridWidth = defaultGridWidth;
+let gridHeight = defaultGridHeight;
+let cellSize = 16;
+let colorHistory = [
+    '#000000',
+    '#ae8ce2',
+    '#2d5d9e',
+    '#43bef2',
+    '#99b213',
+    '#e5b42e',
+    '#c00f68',
+    '#ffffff'
+];
+let currentColor = '#000000';
+let grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null));
+let offsetX = 0;
+let offsetY = 0;
+const paletteToggle = document.getElementById('palette-toggle');
+const palette = document.getElementById('palette');
+let isPaletteVisible = true;
+let isDrawing = false;
+let lastX = null;
+let lastY = null;
+let lastCell = null;
+const MIN_CELL_SIZE = 4;
+const MAX_CELL_SIZE = 64;
+let canvases = [];
+let currentCanvasIndex = 0;
+let globalOffsetX = 0;
+let globalOffsetY = 0;
+
+canvas.addEventListener('mousedown', handleInputStart);
+canvas.addEventListener('mousemove', handleInputMove);
+canvas.addEventListener('mouseup', handleInputEnd);
+canvas.addEventListener('touchstart', handleInputStart);
+canvas.addEventListener('touchmove', handleInputMove);
+canvas.addEventListener('touchend', handleInputEnd);
+document.getElementById('colorPicker').addEventListener('input', handleColorChange);
+document.getElementById('gridWidth').addEventListener('change', updateGridSize);
+document.getElementById('gridHeight').addEventListener('change', updateGridSize);
+document.getElementById('resetBtn').addEventListener('click', handleReset);
+document.getElementById('exportBtn').addEventListener('click', exportToPNG);
+window.addEventListener('keydown', handlePan);
+paletteToggle.addEventListener('click', togglePalette);
+document.getElementById('zoomInBtn').addEventListener('click', () => handleZoom(1.25));
+document.getElementById('zoomOutBtn').addEventListener('click', () => handleZoom(0.75));
+document.getElementById('panUpBtn').addEventListener('click', () => handlePanButton('up'));
+document.getElementById('panDownBtn').addEventListener('click', () => handlePanButton('down'));
+document.getElementById('panLeftBtn').addEventListener('click', () => handlePanButton('left'));
+document.getElementById('panRightBtn').addEventListener('click', () => handlePanButton('right'));
+document.getElementById('centerViewBtn').addEventListener('click', resetView);
+document.getElementById('newCanvasBtn').addEventListener('click', addNewCanvas);
+document.getElementById('saveProjectBtn').addEventListener('click', saveProject);
+document.getElementById('loadProjectBtn').addEventListener('click', loadProject);
+
+
+resizeCanvas();
+loadFromLocalStorage();
+renderColorHistory();
+
+
+function addNewCanvas() {
+    canvases.push({
+        grid: Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)),
+        offsetX: 0,
+        offsetY: 0,
+        hasPixels: false
+    });
+    currentCanvasIndex = canvases.length - 1;
+    centerGrid();
+    drawGrid();
+    saveToLocalStorage();
+}
+
+function initializeGrid() {
+    if (canvases.length > 0) {
+        canvases[currentCanvasIndex].grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null));
+    }
+}
+
+function resizeCanvas() {
+    canvas.width = window.innerWidth;
+    canvas.height = window.innerHeight;
+    centerGrid();
+    drawGrid();
+}
+
+function centerGrid() {
+    if (canvases.length === 0) return;
+    
+    canvases.forEach((canvasData, index) => {
+        canvasData.offsetY = Math.max((canvas.height - (gridHeight * cellSize)) / 2, 0);
+        if (index === 0) {
+            canvasData.offsetX = Math.max((canvas.width - (gridWidth * cellSize * canvases.length) - (cellSize * (canvases.length - 1))) / 2, 0);
+        } else {
+            // Position each canvas one cell width apart
+            const previousCanvas = canvases[index - 1];
+            canvasData.offsetX = previousCanvas.offsetX + (gridWidth * cellSize) + cellSize;
+        }
+    });
+}
+
+function drawGrid() {
+    ctx.fillStyle = 'teal';
+    ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+    canvases.forEach((canvasData, index) => {
+        const xOffset = canvasData.offsetX + globalOffsetX;
+        
+        for (let x = 0; x < gridWidth; x++) {
+            for (let y = 0; y < gridHeight; y++) {
+                const cellX = x * cellSize + xOffset;
+                const cellY = y * cellSize + canvasData.offsetY + globalOffsetY;
+                
+                // Fill cell background
+                ctx.fillStyle = canvasData.grid[x][y] || '#f7f7f7';
+                ctx.fillRect(cellX, cellY, cellSize, cellSize);
+                
+                // Draw cell border
+                ctx.strokeStyle = '#888888';
+                ctx.strokeRect(cellX, cellY, cellSize, cellSize);
+                
+                // Draw diagonal line for empty cells
+                if (!canvasData.grid[x][y]) {
+                    ctx.beginPath();
+                    ctx.strokeStyle = '#bfbfbf';
+                    ctx.moveTo(cellX, cellY);
+                    ctx.lineTo(cellX + cellSize, cellY + cellSize);
+                    ctx.stroke();
+                    ctx.strokeStyle = '#888888';
+                }
+            }
+        }
+    });
+}
+
+function addToColorHistory(color) {
+    if (colorHistory.includes(color)) return;
+    if (colorHistory.length >= 10) colorHistory.shift();
+    colorHistory.push(color);
+    renderColorHistory();
+}
+
+function renderColorHistory() {
+    const historyDiv = document.getElementById('colorHistory');
+    historyDiv.innerHTML = '';
+    colorHistory.forEach(color => {
+        const colorDiv = document.createElement('div');
+        colorDiv.style.backgroundColor = color;
+        colorDiv.addEventListener('click', () => {
+            currentColor = color;
+            document.getElementById('colorPicker').value = color;
+        });
+        historyDiv.appendChild(colorDiv);
+    });
+}
+
+function handleColorChange() {
+    currentColor = document.getElementById('colorPicker').value;
+    addToColorHistory(currentColor);
+    saveToLocalStorage();
+}
+
+function handleReset() {
+    const confirmReset = confirm("Are you sure you want to reset? This will clear your drawing and settings.");
+    
+    if (confirmReset) {
+        gridWidth = defaultGridWidth;
+        gridHeight = defaultGridHeight;
+        cellSize = 16;
+        globalOffsetX = 0;
+        globalOffsetY = 0;
+        colorHistory = [];
+        renderColorHistory();
+        
+        canvases = [];
+        addNewCanvas();
+        
+        document.getElementById('gridWidth').disabled = false;
+        document.getElementById('gridHeight').disabled = false;
+        document.getElementById('gridWidth').value = gridWidth;
+        document.getElementById('gridHeight').value = gridHeight;
+        
+        localStorage.removeItem('pixelArtConfig');
+        
+        alert("Reset complete. You can now adjust the grid size until you place your first pixel.");
+    }
+}
+
+function handlePan(e) {
+    const step = cellSize;
+    if (canvases.length === 0) return;
+    
+    if (e.key === 'ArrowUp') globalOffsetY += step;
+    if (e.key === 'ArrowDown') globalOffsetY -= step;
+    if (e.key === 'ArrowLeft') globalOffsetX += step;
+    if (e.key === 'ArrowRight') globalOffsetX -= step;
+    drawGrid();
+}
+
+function updateGridSize() {
+    const newWidth = parseInt(document.getElementById('gridWidth').value);
+    const newHeight = parseInt(document.getElementById('gridHeight').value);
+    
+    // Validate input
+    if (newWidth <= 0 || newHeight <= 0 || newWidth > 100 || newHeight > 100) return;
+    
+    gridWidth = newWidth;
+    gridHeight = newHeight;
+    
+    // Update all existing canvases with new dimensions
+    canvases.forEach(canvasData => {
+        const newGrid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null));
+        
+        // Preserve existing pixel data where possible
+        const minWidth = Math.min(canvasData.grid.length, gridWidth);
+        const minHeight = Math.min(canvasData.grid[0].length, gridHeight);
+        
+        for (let x = 0; x < minWidth; x++) {
+            for (let y = 0; y < minHeight; y++) {
+                newGrid[x][y] = canvasData.grid[x][y];
+            }
+        }
+        
+        canvasData.grid = newGrid;
+    });
+    
+    centerGrid();
+    drawGrid();
+    saveToLocalStorage();
+}
+
+function saveToLocalStorage() {
+    const gridData = {
+        gridWidth,
+        gridHeight,
+        cellSize,
+        colorHistory,
+        currentColor,
+        canvases,
+        isPaletteVisible,
+        globalOffsetX,
+        globalOffsetY
+    };
+    localStorage.setItem('pixelArtConfig', JSON.stringify(gridData));
+}
+
+function loadFromLocalStorage() {
+    const savedData = localStorage.getItem('pixelArtConfig');
+    if (savedData) {
+        const gridData = JSON.parse(savedData);
+        gridWidth = gridData.gridWidth || defaultGridWidth;
+        gridHeight = gridData.gridHeight || defaultGridHeight;
+        cellSize = gridData.cellSize || 16;
+        colorHistory = gridData.colorHistory || [];
+        currentColor = gridData.currentColor || '#000000';
+        canvases = gridData.canvases || [];
+        globalOffsetX = gridData.globalOffsetX || 0;
+        globalOffsetY = gridData.globalOffsetY || 0;
+        
+        // Set input values
+        document.getElementById('gridWidth').value = gridWidth;
+        document.getElementById('gridHeight').value = gridHeight;
+        document.getElementById('colorPicker').value = currentColor;
+        
+        // Disable grid size inputs if there's saved data
+        document.getElementById('gridWidth').disabled = true;
+        document.getElementById('gridHeight').disabled = true;
+        
+        isPaletteVisible = gridData.isPaletteVisible ?? true;
+        if (!isPaletteVisible) {
+            palette.classList.add('hidden');
+            paletteToggle.classList.add('hidden');
+            paletteToggle.innerHTML = '🎨';
+        }
+    } else {
+        // No saved data, create default canvas
+        gridWidth = defaultGridWidth;
+        gridHeight = defaultGridHeight;
+        addNewCanvas();
+    }
+    
+    // Ensure there's at least one canvas
+    if (canvases.length === 0) {
+        addNewCanvas();
+    }
+    
+    centerGrid();
+    drawGrid();
+    renderColorHistory();
+}
+
+function exportToPNG() {
+    // Prompt for filename
+    const filename = prompt("Enter a name for your file(s)", "pixel-art");
+    if (!filename) return; // Cancelled
+    
+    // An array of promises for each canvas
+    const exportPromises = canvases.map((canvasData, index) => {
+        return new Promise(resolve => {
+            const tempCanvas = document.createElement('canvas');
+            const tempCtx = tempCanvas.getContext('2d');
+            tempCanvas.width = gridWidth * cellSize;
+            tempCanvas.height = gridHeight * cellSize;
+
+            // Draw the canvas content
+            for (let x = 0; x < gridWidth; x++) {
+                for (let y = 0; y < gridHeight; y++) {
+                    tempCtx.fillStyle = canvasData.grid[x][y] || 'transparent';
+                    tempCtx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
+                }
+            }
+
+            // Convert to data URL (trying to work around a webkit bug where blobs don't work so well)
+            const dataURL = tempCanvas.toDataURL('image/png');
+            const paddedNumber = String(index + 1).padStart(2, '0');
+            const finalFilename = canvases.length > 1 
+                ? `${filename}-${paddedNumber}.png`
+                : `${filename}.png`;
+
+            resolve({ dataURL, filename: finalFilename });
+        });
+    });
+
+    // Process exports sequentially with delay
+    Promise.all(exportPromises).then(exports => {
+        exports.forEach((exportData, index) => {
+            setTimeout(() => {
+                const link = document.createElement('a');
+                link.href = exportData.dataURL;
+                link.download = exportData.filename;
+                document.body.appendChild(link);
+                link.click();
+                document.body.removeChild(link);
+            }, index * 1000); // 1 second delay between each download
+        });
+    });
+}
+
+function handleInputStart(e) {
+    e.preventDefault();
+    isDrawing = true;
+    const coords = getInputCoordinates(e);
+    const cell = getCellFromCoords(coords);
+    
+    if (cell) {
+        lastCell = cell;
+        currentCanvasIndex = cell.canvasIndex;
+        
+        // Lock grid size on first pixel placement
+        if (!document.getElementById('gridWidth').disabled) {
+            document.getElementById('gridWidth').disabled = true;
+            document.getElementById('gridHeight').disabled = true;
+        }
+        
+        if (canvases[currentCanvasIndex].grid[cell.x][cell.y]) {
+            canvases[currentCanvasIndex].grid[cell.x][cell.y] = null;
+        } else {
+            canvases[currentCanvasIndex].grid[cell.x][cell.y] = currentColor;
+        }
+        drawGrid();
+        saveToLocalStorage();
+    }
+}
+
+function handleInputMove(e) {
+    e.preventDefault();
+    if (!isDrawing) return;
+    
+    const coords = getInputCoordinates(e);
+    const cell = getCellFromCoords(coords);
+    
+    if (cell && (!lastCell || cell.x !== lastCell.x || cell.y !== lastCell.y)) {
+        lastCell = cell;
+        // When dragging, always draw (don't erase)
+        canvases[currentCanvasIndex].grid[cell.x][cell.y] = currentColor;
+        drawGrid();
+        saveToLocalStorage();
+    }
+}
+
+function handleInputEnd(e) {
+    e.preventDefault();
+    isDrawing = false;
+    lastCell = null;
+}
+
+function getInputCoordinates(e) {
+    const rect = canvas.getBoundingClientRect();
+    if (e.touches) {
+        // Touch event
+        return {
+            x: e.touches[0].clientX - rect.left,
+            y: e.touches[0].clientY - rect.top
+        };
+    } else {
+        // Mouse event
+        return {
+            x: e.clientX - rect.left,
+            y: e.clientY - rect.top
+        };
+    }
+}
+
+function getCellFromCoords(coords) {
+    if (canvases.length === 0) return null;
+    
+    for (let i = 0; i < canvases.length; i++) {
+        const canvasData = canvases[i];
+        const canvasLeft = canvasData.offsetX + globalOffsetX;
+        const canvasRight = canvasLeft + (gridWidth * cellSize);
+        
+        if (coords.x >= canvasLeft && coords.x < canvasRight) {
+            const x = Math.floor((coords.x - canvasLeft) / cellSize);
+            const y = Math.floor((coords.y - (canvasData.offsetY + globalOffsetY)) / cellSize);
+            
+            if (x >= 0 && x < gridWidth && y >= 0 && y < gridHeight) {
+                return { x, y, canvasIndex: i };
+            }
+        }
+    }
+    return null;
+}
+
+function togglePalette() {
+    isPaletteVisible = !isPaletteVisible;
+    
+    if (isPaletteVisible) {
+        palette.classList.remove('hidden');
+        paletteToggle.classList.remove('hidden');
+        paletteToggle.innerHTML = '☰';
+    } else {
+        palette.classList.add('hidden');
+        paletteToggle.classList.add('hidden');
+        paletteToggle.innerHTML = '🎨';
+    }
+}
+
+function handleZoom(factor) {
+    const newCellSize = Math.max(MIN_CELL_SIZE, Math.min(MAX_CELL_SIZE, cellSize * factor));
+    
+    if (newCellSize === cellSize) return;
+    
+    cellSize = newCellSize;
+    
+    centerGrid();
+    
+    drawGrid();
+    saveToLocalStorage();
+}
+
+function handlePanButton(direction) {
+    if (canvases.length === 0) return;
+    
+    const step = cellSize;
+    switch(direction) {
+        case 'up':
+            globalOffsetY += step;
+            break;
+        case 'down':
+            globalOffsetY -= step;
+            break;
+        case 'left':
+            globalOffsetX += step;
+            break;
+        case 'right':
+            globalOffsetX -= step;
+            break;
+    }
+    drawGrid();
+}
+
+function resetView() {
+    cellSize = 16; // Reset to default zoom
+    globalOffsetX = 0;
+    globalOffsetY = 0;
+    if (canvases.length > 0) {
+        centerGrid();
+        drawGrid();
+        saveToLocalStorage();
+    }
+}
+
+function saveProject() {
+    const now = new Date();
+    const formattedDate = now.toISOString().slice(0, 16).replace('T', '-').replace(':', '-');
+    
+    const projectName = prompt("Enter a name for your project", formattedDate);
+    if (!projectName) return; // User cancelled
+    
+    // First save to localStorage to ensure all current state is saved
+    saveToLocalStorage();
+    
+    // Get the data from localStorage and add our special header
+    const projectData = JSON.parse(localStorage.getItem('pixelArtConfig'));
+    const exportData = {
+        __projectHeader: "pppppp_v1",  // Add special header
+        timestamp: new Date().toISOString(),
+        data: projectData
+    };
+    
+    // Create and trigger download
+    const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
+    const link = document.createElement('a');
+    link.href = URL.createObjectURL(blob);
+    link.download = `${projectName}.json`;
+    link.click();
+    URL.revokeObjectURL(link.href);
+}
+
+function loadProject() {
+    // AAAAH! Data loss!
+    const confirmLoad = confirm("Loading a project will replace your current work. Are you sure you want to proceed?");
+    if (!confirmLoad) return;
+    
+    // Create file input
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = '.json';
+    
+    fileInput.addEventListener('change', function(e) {
+        const file = e.target.files[0];
+        if (!file) return;
+        
+        const reader = new FileReader();
+        reader.onload = function(e) {
+            try {
+                const importedData = JSON.parse(e.target.result);
+                
+                // Check for our super special header
+                if (!importedData.__projectHeader || 
+                    importedData.__projectHeader !== "pppppp_v1") {
+                    throw new Error('This file is not a valid Pixel Art Project file');
+                }
+                
+                const projectData = importedData.data;
+                
+                // Validate the data has expected properties
+                if (!projectData.gridWidth || !projectData.gridHeight || !projectData.canvases) {
+                    throw new Error('Invalid project file format');
+                }
+                
+                // Save to localStorage
+                localStorage.setItem('pixelArtConfig', JSON.stringify(projectData));
+                
+                // Reload the page to apply changes
+                window.location.reload();
+                
+            } catch (error) {
+                alert('Error loading project file: ' + error.message);
+            }
+        };
+        reader.readAsText(file);
+    });
+    
+    fileInput.click();
+}
\ No newline at end of file
diff --git a/js/pixel-art/pixel/index.html b/js/pixel-art/pixel/index.html
new file mode 100644
index 0000000..7e3df56
--- /dev/null
+++ b/js/pixel-art/pixel/index.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>Pixel Pixel Pixel Pixel Pixel Pixel</title>
+    <meta name="description" content="Draw me a goblin, please.">
+    <style>
+        body, html {
+            margin: 0;
+            padding: 0;
+            overflow: hidden;
+            background-color: teal;
+        }
+
+        button {
+            padding: 0.5rem 0.75rem;
+            font-size: 1rem;
+            cursor: pointer;
+            min-height: 2.75rem;
+        }
+        
+        button.reset-btn {
+            cursor: pointer;
+            margin: 0;
+            padding: 0.25rem 0.5rem;
+            border: none;
+            background-color: tomato;
+            color: #fafafa;
+            min-height: 2rem;
+            font-size: 0.875rem;
+            border-radius: 0.25rem;
+            margin-top: 0.5rem;
+        }
+        
+        #canvas {
+            display: block;
+        }
+        
+        #palette {
+            position: fixed;
+            top: 0.625rem;
+            right: 0.625rem;
+            background-color: beige;
+            padding: 0.5rem;
+            border: 1px solid #ccc;
+            border-radius: 0.25rem;
+            width: 10.625rem;
+            transition: transform 0.3s ease-out;
+            max-height: 90vh;
+            overflow-y: auto;
+            scrollbar-width: none;
+            -ms-overflow-style: none;
+            box-sizing: border-box;
+        }
+
+        #palette.hidden {
+            transform: translateX(calc(100% + 1.25rem));
+        }
+
+        #palette-toggle {
+            position: fixed;
+            top: 1.025rem;
+            right: 0.025rem;
+            background-color: beige;
+            border: 1px solid #ccc;
+            border-radius: 0.25rem 0 0 0.25rem;
+            padding: 0.75rem;
+            cursor: pointer;
+            z-index: 1000;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            transition: transform 0.3s ease-out;
+            min-width: 2.75rem;
+            min-height: 2.75rem;
+            font-size: 1.125rem;
+        }
+
+        #palette-toggle:not(.hidden) {
+            transform: translateX(-11.25rem);
+        }
+
+        #palette input,
+        #palette button {
+            display: block;
+            margin-bottom: 0.75rem;
+            width: 100%;
+        }
+
+        .color-history {
+            display: flex;
+            flex-wrap: wrap;
+            margin-top: 0.625rem;
+        }
+
+        .color-history div {
+            width: 1.25rem;
+            height: 1.25rem;
+            border: 1px solid #000;
+            cursor: pointer;
+            margin-right: 0.3125rem;
+            margin-bottom: 0.3125rem;
+        }
+
+        .zoom-controls {
+            display: flex;
+            gap: 0.3125rem;
+            margin-top: 0.625rem;
+            justify-content: space-between;
+            width: 100%;
+        }
+
+        .zoom-controls button {
+            flex: 1;
+            padding: 0.25rem;
+            height: 2rem;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 1rem;
+            min-height: unset;
+            width: unset;
+        }
+
+        .pan-controls {
+            display: flex;
+            flex-direction: column;
+            gap: 0.3125rem;
+            margin-top: 0.625rem;
+            align-items: center;
+        }
+
+        .pan-middle {
+            display: flex;
+            gap: 0.3125rem;
+            width: 100%;
+        }
+
+        .pan-controls button {
+            padding: 0.25rem;
+            width: 2rem;
+            height: 2rem;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 1rem;
+            min-height: unset;
+        }
+
+        #centerViewBtn {
+            margin-top: 0.625rem;
+        }
+
+        @media (max-width: 48rem) {
+            #palette {
+                width: 12.5rem;
+                top: auto;
+                bottom: 0.625rem;
+            }
+
+            #palette-toggle {
+                top: auto;
+                right: 0.625rem;
+                bottom: 0.625rem;
+            }
+
+            #palette-toggle:not(.hidden) {
+                transform: translateX(-12.5rem);
+            }
+        }
+    </style>
+</head>
+<body>
+    <canvas id="canvas"></canvas>
+    <button id="palette-toggle">☰</button>
+    <div id="palette">
+        <div style="display: flex; gap: 5px; margin-bottom: 10px;">
+            <div style="flex: 1;">
+                <label for="gridWidth">Width:</label>
+                <input type="number" id="gridWidth" value="16" min="1" max="100" style="width: 90%;">
+            </div>
+            <div style="flex: 1;">
+                <label for="gridHeight">Height:</label>
+                <input type="number" id="gridHeight" value="16" min="1" max="100" style="width: 90%;">
+            </div>
+        </div>
+        <label for="colorPicker">Select Color:</label>
+        <input type="color" id="colorPicker" value="#000000">
+        <div id="colorHistory" class="color-history"></div>
+        <div class="zoom-controls">
+            <button id="zoomInBtn">🔍+</button>
+            <button id="zoomOutBtn">🔍-</button>
+        </div>
+        <div class="pan-controls">
+            <button id="panUpBtn">⬆️</button>
+            <div class="pan-middle">
+                <button id="panLeftBtn">⬅️</button>
+                <button id="panRightBtn">➡️</button>
+            </div>
+            <button id="panDownBtn">⬇️</button>
+        </div>
+        <button id="centerViewBtn">🎯 Center View</button>
+        <button id="newCanvasBtn">➕ New Canvas</button>
+        <hr>
+        <button id="exportBtn">Export as PNG</button>
+        <button id="saveProjectBtn">Save Project</button>
+        <button id="loadProjectBtn">Load Project</button>
+        <button id="resetBtn" class="reset-btn">Reset</button>
+    </div>
+    <script src="app.js"></script>
+</body>
+</html>