diff --git a/font_maker/font_maker.js b/font_maker/font_maker.js
new file mode 100644
index 0000000..0a933c6
--- /dev/null
+++ b/font_maker/font_maker.js
@@ -0,0 +1,194 @@
+const canvas = document.querySelector("#canvas");
+const selectedDiv = document.querySelector("#selected");
+const charlistTable = document.querySelector("#charlist");
+const exportButton = document.querySelector("#export");
+const exportAreaCode = document.querySelector("#export-area");
+
+const [width, height] = [400, 400];
+
+canvas.width = width;
+canvas.height = height;
+
+canvas.style.width = width + "px";
+canvas.style.height = height + "px";
+
+const g = canvas.getContext("2d");
+g.imageSmoothingEnabled = false;
+
+const state = new Array(64).fill(false);
+
+function render() {
+ g.fillStyle = "black";
+ g.fillRect(0, 0, width, height);
+ g.fillStyle = "white";
+ for (let y = 0; y < 8; ++y) {
+ for (let x = 0; x < 8; ++x) {
+ if (state[y * 8 + x]) {
+ g.fillRect(
+ x * (width / 8),
+ y * (height / 8),
+ width / 8,
+ height / 8,
+ );
+ }
+ }
+ }
+ renderGrid();
+}
+
+function renderGrid() {
+ g.strokeStyle = "gray";
+ g.lineWidth = 1;
+ g.beginPath();
+ for (let y = 0; y < 8; ++y) {
+ for (let x = 0; x < 8; ++x) {
+ g.moveTo(x * (width / 8), 0);
+ g.lineTo(x * (width / 8), height);
+ g.moveTo(0, y * (height / 8));
+ g.lineTo(width, y * (height / 8));
+ }
+ }
+ g.moveTo(width - 1, 0);
+ g.lineTo(width - 1, height);
+ g.moveTo(0, height - 1);
+ g.lineTo(width, height - 1);
+ g.stroke();
+}
+
+const specialChars = Object.fromEntries(
+ Object.entries({
+ "\0": "0",
+ "\n": "n",
+ "\r": "r",
+ "\t": "t",
+ }).map(([ch, r]) => [ch.charCodeAt(0), "\\" + r]),
+);
+
+const chars = new Array(127).fill(0).map((_, i) => ({
+ i,
+ ch: specialChars[i] ?? (i > 31 ? String.fromCharCode(i) : ""),
+ hex: new Array(8).fill(0),
+}));
+
+let selected = { i: 0, ch: "\0", hex: new Array(8).fill(0) };
+
+function stateToHex(state) {
+ let hex = new Array(8).fill(0);
+ for (let y = 0; y < 8; ++y) {
+ let byte = 0;
+ for (let x = 0; x < 8; ++x) {
+ if (state[y * 8 + x]) {
+ byte |= 1 << 7 - x;
+ }
+ }
+ hex[y] = byte;
+ }
+ return hex;
+}
+
+function stateFromHex(state, hex) {
+ for (let y = 0; y < 8; ++y) {
+ for (let x = 0; x < 8; ++x) {
+ state[y * 8 + x] = (hex[y] >> 7 - x & 1) != 0;
+ }
+ }
+}
+
+function html(strings, ...values) {
+ return strings[0] +
+ strings.slice(1).map((v, i) => values[i].toString() + v).join("");
+}
+
+function renderSelectedChar() {
+ const { i, ch, hex } = selected;
+ selectedDiv.innerHTML = html`
+ ${i}
+ ${ch}
+ 0x${hex.map((v) => v.toString(16).padStart(2, "0")).join(
+ "",
+ )}
+
+
+ `;
+
+ document.querySelector("#selected-save")
+ .onclick = () => {
+ const overwrite = document.querySelector("#overwrite").value;
+ if (overwrite) {
+ const hex = new Array(8).fill(0);
+ for (let i = 0; i < 8; ++i) {
+ hex[i] = parseInt(
+ overwrite.slice(i * 2 + 2, i * 2 + 4),
+ 16,
+ );
+ }
+ chars[selected.i].hex = hex;
+ selected.hex = hex.map((v) => v);
+ stateFromHex(state, hex);
+ renderCharlist();
+ renderSelectedChar();
+ render();
+ } else {
+ chars[selected.i].hex = selected.hex.map((v) => v);
+ renderCharlist();
+ }
+ };
+}
+
+function renderCharlist() {
+ charlistTable.innerHTML = html`
+
${i}${ch}0x${hex.map((v) =>
+ v.toString(16).padStart(2, "0")
+ ).join("")}
+
+
diff --git a/src/io_device.cpp b/src/io_device.cpp
index 37bf01d..fa149ea 100644
--- a/src/io_device.cpp
+++ b/src/io_device.cpp
@@ -20,6 +20,21 @@ constexpr uint64_t char_data(uint8_t ch)
switch (ch) {
// clang-format off
case ' ': return 0x0000000000000000;
+ case '!': return 0x0018181818001818;
+ case '"': return 0x0066660000000000;
+ case '#': return 0x00247e24247e2400;
+ case '$': return 0x00187e6218467e18;
+ case '%': return 0x0022562c18346a44;
+ case '&': return 0x00784c38724a4638;
+ case '\'': return 0x0018180000000000;
+ case '(': return 0x000c18303030180c;
+ case ')': return 0x0030180c0c0c1830;
+ case '*': return 0x0000241818240000;
+ case '+': return 0x000018187e7e1818;
+ case ',': return 0x0000000000181830;
+ case '-': return 0x000000007e7e0000;
+ case '.': return 0x0000000000181800;
+ case '/': return 0x0002060c18306040;
case '0': return 0x003C666E7666663C;
case '1': return 0x001838181818183C;
@@ -32,6 +47,14 @@ constexpr uint64_t char_data(uint8_t ch)
case '8': return 0x003C66663C66663C;
case '9': return 0x003C66663E06663C;
+ case ':': return 0x0018180000181800;
+ case ';': return 0x0000181800181830;
+ case '<': return 0x00060c1830180c06;
+ case '=': return 0x00007e7e007e7e00;
+ case '>': return 0x006030180c183060;
+ case '?': return 0x003e660c18001818;
+ case '@': return 0x003c664a564e603c;
+
case 'A': return 0x00187E66667E6666;
case 'B': return 0x00787E667C667E7C;
case 'C': return 0x003C7E6060607E3C;
@@ -59,7 +82,42 @@ constexpr uint64_t char_data(uint8_t ch)
case 'Y': return 0x0066663C18181818;
case 'Z': return 0x007E7E0C18307E7E;
- case ':': return 0x0018180000181800;
+ case '[': return 0x003c30303030303c;
+ case '\\': return 0x00406030180c0602;
+ case ']': return 0x003c0c0c0c0c0c3c;
+ case '^': return 0x0010386c44000000;
+ case '_': return 0x00000000000000ff;
+ case '`': return 0x0030181800000000;
+ case 'a': return 0x0000003c023e423e;
+ case 'b': return 0x004040407c42427c;
+ case 'c': return 0x000000003c40403c;
+ case 'd': return 0x00020202023e423e;
+ case 'e': return 0x0000003c427c403c;
+ case 'f': return 0x00000c103c101010;
+ case 'g': return 0x003e645840784438;
+ case 'h': return 0x0020202038242424;
+ case 'i': return 0x0000181800181818;
+ case 'j': return 0x000606000606361c;
+ case 'k': return 0x0020202c3830382c;
+ case 'l': return 0x003010101010100c;
+ case 'm': return 0x000000203e2a2a2a;
+ case 'n': return 0x0000000038242424;
+ case 'o': return 0x0000000018242418;
+ case 'p': return 0x0000001c223c2020;
+ case 'q': return 0x00000018241c0404;
+ case 'r': return 0x0000002038242020;
+ case 's': return 0x0000003840380438;
+ case 't': return 0x0000103c1010100c;
+ case 'u': return 0x000000002424241e;
+ case 'v': return 0x0000002424243c18;
+ case 'w': return 0x0000000063222a3e;
+ case 'x': return 0x000000663c183c66;
+ case 'y': return 0x00000024241c0438;
+ case 'z': return 0x0000003e060c183e;
+ case '{': return 0x000c18183018180c;
+ case '|': return 0x1818181818181818;
+ case '}': return 0x003018180c181830;
+ case '~': return 0x0000307a5e0c0000;
default: return 0x55AA55AA55AA55AA;
// clang-format on