-
Notifications
You must be signed in to change notification settings - Fork 0
Emulator apple2js
The Apple ][+ bundle was a contrast to the emscripten emulators around it: pure JavaScript, no WASM, no Emscripten — but its own kind of integration work because the upstream emulator ships a full app with chrome we needed to hide.
Will Scullin's apple2js — MIT-licensed, the mature browser Apple ][ option, period-correct ][+ with 48 KB / Applesoft / Disk II. Pure JavaScript. The "browser" in the name isn't a wrapper around a C emulator; the 6502 is implemented in JS. Built with webpack 5.
git clone --depth 1 https://github.com/whscullin/apple2js.git
cd apple2js && git submodule update --init
npm install && npm run build # → dist/*.bundle.js (~1.3 MB total)We mirror into emulators/apple2/:
-
dist/*.bundle.js(no.map) — and crucially, all dynamic chunks (the numbered<id>.bundle.jsfiles, ~16 of them). webpack loads them lazily; missing chunks fail at runtime. -
dist/*.pnganddist/*.woff*— fonts and glyphs for chrome. Technically unused since we hide chrome, but cheap. -
css/apple2.css— bundle references it for positioning. -
img/*.png— same reason. -
json/disks/index.js— required. The bundle loads this at boot; needs a minimaldisk_index = [...]array listing each bundled disk.
Upstream apple2js.html is a full app: header, exit-fullscreen button, modal dialogs, status panels. None of it suits GenX-DOS's bezel-first aesthetic.
The mistake would be deleting those DOM elements. The bundle queries them at boot — document.getElementById('exit-fullscreen') etc. — and throws if anything's missing. Keep the DOM intact; hide it with CSS:
<style>
#header, #exit-fullscreen, .inset, .modal { display: none !important; }
</style>Then resize #display, .overscan, and #screen to fit the viewport at 4:3 (Apple ][+ NTSC native is 280×192, the apple2js canvas is 592×416). Result: just the canvas, full-bleed.
GenX-DOS's convention is ?game=<key>; apple2js's convention is ?disk=<key>. Rather than fork the URL handling, we rewrite the param before the main bundle reads it:
const params = new URLSearchParams(location.search);
const key = params.get('game');
if (key) {
params.delete('game');
params.set('disk', key);
history.replaceState(null, '', '?' + params.toString());
}apple2js's processHash then loads json/disks/<key>.json like normal.
No-game-param launches drop straight into the BASIC prompt — period-correct ][+ behaviour, no menu needed.
apple2js doesn't take .dsk files directly. It needs disks in its own JSON format (base64-encoded sectors). Upstream ships the conversion tool:
./bin/dsk2json -c <Category> -n "<Name>" <input.dsk> > json/disks/<key>.jsonA standard DOS 3.3 disk is 35 tracks × 16 sectors × 256 bytes = 143,360 bytes raw, which expands to ~200 KB JSON. Each game key has to appear in:
-
json/disks/<key>.json(the disk itself) -
json/disks/index.js(the local-catalog declaration) - The menu's
<key>.batlink
Any one of those missing and the disk silently fails to load.
The [4am Apple ] preservation collection is the cleanest source: each item ships a 00playable.dsk (143,360 bytes, ready to mount, copy-protection removed). For titles 4am didn't crack — Karateka, etc. — TOSEC dumps work but may carry protection that breaks on first sector read.
Finding what 4am preserved:
curl "https://archive.org/advancedsearch.php?q=collection:apple_ii_library_4am+AND+<title>&fl[]=identifier&fl[]=title&rows=10&output=json"Code names cap at 8 chars (this is a DOS terminal). Long titles get trimmed: CHOPLIFTR → CHOPLIFT, LODERUNR → LODERUN. The file key, the displayed code, and the link path's ?game= value all have to match.
emulators/apple2/
├── play.html ← chrome hider + ?game→?disk rewrite
├── apple2js.html ← upstream entry, mostly unused
├── *.bundle.js ← main2 + numbered chunks
├── css/, img/, *.png, *.woff*
└── json/disks/
├── index.js ← local catalog
└── *.json ← per-game disks
- Emulator-apple1js — same author, different machine
- Emulators — index