Skip to content

Instantly share code, notes, and snippets.

@ashtonmeuser
Last active March 5, 2025 22:43
Show Gist options
  • Save ashtonmeuser/04710d3befc446e849108a58755163ea to your computer and use it in GitHub Desktop.
Save ashtonmeuser/04710d3befc446e849108a58755163ea to your computer and use it in GitHub Desktop.
DOOM in a bookmarklet
// bookmarklet-title: DOOM
// bookmarklet-about: Take a break. Kill some demons. The entirety of the 1993 classic DOOM shoehorned into a bookmarklet. Some browsers are snobby about executing nearly 10 MB of raw JS from the favorites bar 🙄 in which case you can opt to fetch the DOOM binary rather than embed it.
import Modal from '/ashtonmeuser/0613e3aeff5a4692d8c148d7fcd02f34/raw/d6dd01eaab665a14bded5487a8c03b6bb7197388/Modal.ts';
import binary from 'https://cdn.jsdelivr.net/gh/ashtonmeuser/godot-wasm-doom/doom.wasm';
const embed = false; // bookmarklet-var(boolean): embed
const uuid: string = ''; // bookmarklet-var(uuid): uuid
type Size = { x: number, y: number };
type KeyMap = { [key: string]: number };
type DoomExports = {
main: () => void, // We'll ignore the real signature: (i32, i32) => i32
add_browser_event: (status: number, code: number) => void,
doom_loop_step: () => void,
};
async function doomWasm(imports: WebAssembly.Imports): Promise<WebAssembly.WebAssemblyInstantiatedSource> {
if (embed) return await WebAssembly.instantiate(binary, imports);
return await WebAssembly.instantiateStreaming(fetch('https://cdn.jsdelivr.net/gh/ashtonmeuser/godot-wasm-doom/doom.wasm'), imports);
}
function makeKeyMap(): KeyMap {
const map = {
Enter: 13,
Backspace: 127,
Space: 32,
ArrowLeft: 0xac,
ArrowRight: 0xae,
ArrowUp: 0xad,
ArrowDown: 0xaf,
ControlLeft: 0x80+0x1d,
AltLeft: 0x80+0x38,
Escape: 27,
Tab: 9,
ShiftLeft: 16,
};
for (let i = 0; i < 26; i++) map[`Key${String.fromCharCode(65 + i)}`] = 65 + 32 + i; // Alphabetic
for (let i = 0; i <= 9; i++) map[`Digit${i}`] = 48 + i; // Numeric
for (let i = 1; i <= 12; i++) map[`F${i}`] = 186 + i; // Function
return map;
}
(async () => {
const size: Size = { x: 320 * 2, y: 200 * 2 };
const canvas = document.createRange().createContextualFragment(`<canvas width="${size.x}" height="${size.y}" style="display:block;max-width:100%"></canvas>`).firstChild as HTMLCanvasElement;
const modal = Modal.factory(uuid, { content: canvas, padding: '0', style: `dialog{aspect-ratio:${size.x}/${size.y}}` });
if (!modal) return; // Failed to create modal
const memory = new WebAssembly.Memory({ initial : 108 });
const update = (data: Uint8ClampedArray) => canvas.getContext('2d')?.putImageData(new ImageData(data, size.x, size.y), 0, 0);
const makeLogger = (logger: (message: string) => void) => (offset: number, length: number) => logger(new TextDecoder('utf8').decode(new Uint8Array(memory.buffer, offset, length)));
const keyMap = makeKeyMap();
const imports = {
js: {
js_console_log: makeLogger(console.info),
js_stdout: makeLogger(console.info),
js_stderr: makeLogger(console.warn),
js_milliseconds_since_start: (): number => performance.now(),
js_draw_screen: (offset: number) => {
var data = new Uint8ClampedArray(memory.buffer, offset, size.x * size.y * 4);
update(data);
},
},
env: { memory },
};
const { instance } = await doomWasm(imports);
const exports = instance.exports as DoomExports;
const listener = (event: KeyboardEvent) => {
exports.add_browser_event(event.type === 'keydown' ? 0 : 1, keyMap[event.code]);
event.preventDefault();
};
window.addEventListener('keydown', listener);
window.addEventListener('keyup', listener);
let running = true;
modal.onClose(() => {
running = false;
window.removeEventListener('keydown', listener);
window.removeEventListener('keyup', listener);
});
exports.main();
function step() {
if (!running) return;
exports.doom_loop_step();
window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment