apple2js/apple2js.html

922 lines
25 KiB
HTML
Raw Normal View History

<!DOCTYPE html><!-- -*- mode: HTML; indent-tabs-mode: nil -*- -->
<!--
Copyright 2010-2013 Will Scullin <scullin@scullinsteel.com>
Permission to use, copy, modify, distribute, and sell this software and its
documentation for any purpose is hereby granted without fee, provided that
the above copyright notice appear in all copies and that both that
copyright notice and this permission notice appear in supporting
documentation. No representations are made about the suitability of this
software for any purpose. It is provided "as is" without express or
implied warranty.
-->
<html>
<head>
<title>Apple ][js - An Apple 2 Emulator in JavaScript</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
2013-11-18 01:07:14 +00:00
<meta name="apple-mobile-web-app-title" content="Apple ][js">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta charset="utf-8" />
<meta name="description" content="Apple ][js is an Apple ][ (or Apple II or Apple2) emulator written using only JavaScript and HTML5. It has color display, sound and disk support. It works best in the Chrome and Safari browsers." />
<meta name="keywords" content="apple2,apple,ii,javascript,emulator,html5" />
<link rel="apple-touch-icon" href="img/webapp-iphone.png" />
<link rel="apple-touch-icon" size="72x72" href="img/webapp-ipad.png" />
<link rel="shortcut icon" href="logoicon.png" />
<link rel="stylesheet" type="text/css" href="css/apple2.css" />
<link rel="stylesheet" type="text/css"
href="http://code.jquery.com/ui/1.10.3/themes/mint-choc/jquery-ui.css" />
<meta property="og:title" content="Apple ][js" />
<meta property="og:type" content="website" />
<meta property="og:description" content="Apple ][js is an Apple ][ (or Apple II or Apple2) emulator written using only JavaScript and HTML5." />
<meta property="og:url" content="http://www.scullinsteel.com/apple2/" />
<meta property="og:image" content="http://www.scullinsteel.com/apple2/img/image.png" />
<meta property="fb:admins" content="700585391" />
<!-- jQuery -->
<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.js">
</script>
<script type="text/javascript" src="http://code.jquery.com/ui/1.10.3/jquery-ui.js">
</script>
<!-- Emulator Scripts -->
2013-11-16 20:38:18 +00:00
<script type="text/javascript" src="js/util.js"></script>
<script type="text/javascript" src="js/prefs.js"></script>
<script type="text/javascript" src="js/ram.js"></script>
<script type="text/javascript" src="js/langcard.js"></script>
<script type="text/javascript" src="js/fpbasic.js"></script>
<script type="text/javascript" src="js/apple2char.js"></script>
<script type="text/javascript" src="js/canvas2.js"></script>
<script type="text/javascript" src="js/apple2io.js"></script>
<script type="text/javascript" src="js/parallel.js"></script>
<script type="text/javascript" src="js/disk2.js"></script>
<script type="text/javascript" src="js/ramfactor.js"></script>
<script type="text/javascript" src="js/cpu6502.js"></script>
<script type="text/javascript" src="js/base64.js"></script>
2014-03-06 04:04:54 +00:00
<script type="text/javascript" src="js/ui/audio.js"></script>
2013-11-16 20:38:18 +00:00
<script type="text/javascript" src="js/ui/keyboard2.js"></script>
<script type="text/javascript" src="js/ui/gamepad.js"></script>
2013-11-16 20:38:18 +00:00
<script type="text/javascript" src="js/ui/printer.js"></script>
<!-- Disk Index -->
<script type="text/javascript" src="json/disks/index.js"></script>
<script type="text/javascript">
var focused = false;
var startTime = Date.now();
var lastCycles = 0;
var frames = 0, lastFrames = 0;
var paused = false;
var sound = true;
var hashtag;
var disk_categories = {'Local Saves': []};
var disk_sets = {}
var disk_cur_name = [];
var disk_cur_cat = [];
function DriveLights()
{
return {
driveLight: function(drive, on) {
$("#disk" + drive).css("background-image",
on ? "url(css/red-on-16.png)" :
"url(css/red-off-16.png)");
},
dirty: function(drive, dirty) {
},
label: function(drive, label) {
$("#disklabel" + drive).text(label);
}
}
}
var _saveDrive = 1;
var _loadDrive = 1;
function openLoad(drive, event)
{
_loadDrive = drive;
if (disk_cur_cat[drive]) {
$("#category_select").val(disk_cur_cat[drive]).change();
}
$("#load").dialog("open");
}
function openSave(drive, event)
{
_saveDrive = drive;
$("#save_name").val($("#disklabel" + drive).text());
$("#save").dialog("open");
}
var loading = false;
function loadAjax(url) {
loading = true;
$("#loading").dialog("open");
$.ajax({ url: url,
cache: false,
dataType: "jsonp",
jsonp: false,
global: false
});
}
function doLoad() {
var urls = $("#disk_select").val(), url;
if (urls && urls.length) {
if (typeof(urls) == "string") {
url = urls;
} else {
url = urls[0];
}
}
var files = $("#local_file").prop("files");
if (files.length == 1) {
doLoadLocal();
} else if (url) {
var filename;
$("#load").dialog("close");
if (url.substr(0,6) == "local:") {
filename = url.substr(6);
if (filename == "__manage") {
openManage();
} else {
loadLocalStorage(_loadDrive, filename);
}
} else {
var r1 = /json\/disks\/(.*).json$/.exec(url);
if (r1 && _loadDrive == 1) {
filename = r1[1];
document.location.hash = filename;
}
loadAjax(url);
}
}
}
function doSave() {
var name = $("#save_name").val();
saveLocalStorage(_saveDrive, name);
$("#save").dialog("close");
}
function doDelete(name) {
if (confirm("Delete " + name + "?")) {
deleteLocalStorage(name);
}
}
function doLoadLocal() {
var files = $("#local_file").prop("files")
if (files.length == 1) {
var file = files[0];
var fileReader = new FileReader();
fileReader.onload = function(event) {
var parts = file.name.split(".");
var name = parts[0], ext = parts[parts.length - 1].toLowerCase();
if (disk2.setBinary(_saveDrive, name, ext, this.result)) {
$("#load").dialog("close");
}
}
fileReader.readAsArrayBuffer(file);
}
}
function openLoadLocal(drive) {
_saveDrive = drive;
$("#local").dialog("open");
}
function openManage() {
$("#manage").dialog("open");
}
2014-01-05 17:37:33 +00:00
function loadHTTP(url) {
2013-12-25 18:21:01 +00:00
loading = true;
$("#loading").dialog("open");
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.responseType = "arraybuffer";
req.onload = function(event) {
var parts = url.split(/[\/\.]/);
var name = decodeURIComponent(parts[parts.length - 2]);
var ext = parts[parts.length - 1].toLowerCase();
if (disk2.setBinary(_saveDrive, name, ext, req.response)) {
$("#disklabel" + _saveDrive).text(name);
$("#loading").dialog("close");
loading = false;
}
}
req.send(null);
}
var prefs = new Prefs();
var runTimer = null;
var cpu = new CPU6502();
var ram1 = new RAM(0x00, 0x03),
ram2 = new RAM(0x0C, 0x1F),
ram3 = new RAM(0x60, 0xBF);
var hgr = new HiresPage(1);
var hgr2 = new HiresPage(2);
var gr = new LoresPage(1);
var gr2 = new LoresPage(2);
var rom = new Apple2ROM();
var vm = new VideoModes(gr, hgr, gr2, hgr2);
var mmu = {
auxRom: function(slot, rom) {
cpu.addPageHandler(rom);
}
};
var drivelights = new DriveLights();
var io = new Apple2IO(cpu, vm);
var keyboard = new KeyBoard(io);
var parallel = new Parallel(io, new Printer(), 1);
var disk2 = new DiskII(io, drivelights, 6);
var slinky = new RAMFactor(mmu, io, 2, 1024 * 1024);
var lc = new LanguageCard(io, rom);
cpu.addPageHandler(ram1);
cpu.addPageHandler(gr);
cpu.addPageHandler(gr2);
cpu.addPageHandler(ram2);
cpu.addPageHandler(hgr);
cpu.addPageHandler(hgr2);
cpu.addPageHandler(ram3);
cpu.addPageHandler(lc);
cpu.addPageHandler(io);
cpu.addPageHandler(parallel);
cpu.addPageHandler(slinky);
cpu.addPageHandler(disk2);
var showFPS = false;
function updateKHz() {
var now = Date.now();
var ms = now - startTime;
var cycles = cpu.cycles();
var delta;
if (showFPS) {
delta = frames - lastFrames;
2013-11-18 01:07:14 +00:00
var fps = parseInt(delta/(ms/1000), 10);
$("#khz").text( fps + "fps");
} else {
delta = cycles - lastCycles;
khz = parseInt(delta/ms);
$("#khz").text( khz + "KHz");
}
startTime = now;
lastCycles = cycles;
lastFrames = frames;
}
2014-03-06 04:04:54 +00:00
/* Audio Handling */
initAudio();
function updateSound()
{
2014-03-06 04:04:54 +00:00
enableSound($("#enable_sound").attr("checked"));
}
function dumpDisk(drive) {
var wind = window.open("", "_blank");
wind.document.title = $("#disklabel" + drive).text();
wind.document.write("<pre>");
wind.document.write(disk2.getJSON(drive, true));
wind.document.write("</pre>");
wind.document.close();
}
function step()
{
if (runTimer) {
clearInterval(runTimer);
}
runTimer = null;
cpu.step(function() {
debug(cpu.dumpRegisters());
debug(cpu.dumpPC());
});
}
var running = false;
var throttling = true;
function toggleSpeed()
{
throttling = $("#speed_toggle").prop("checked");
if (runTimer) {
run();
}
}
var _requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
function run(pc) {
if (runTimer) {
clearInterval(runTimer);
}
if (pc) {
cpu.setPC(pc);
}
var ival = 30, step = 1023 * ival, stepMax = step;
if (!throttling) {
ival = 1;
}
var now, last = Date.now(), lastSample = last;
var runFn = function() {
now = Date.now();
frames++;
if (_requestAnimationFrame) {
step = (now - last) * 1023;
last = now;
if (step > stepMax) {
step = stepMax;
}
}
if (document.location.hash != hashtag) {
hashtag = document.location.hash;
filename = hup()
if (filename) {
2013-12-25 18:21:01 +00:00
if (filename.indexOf("://") > 0) {
loadHTTP(disk);
} else {
loadAjax("json/disks/" + filename + ".json");
}
}
}
if (!loading) {
running = true;
cpu.stepCycles(step);
running = false;
vm.blit();
if (now - lastSample >= 200) {
2014-03-06 04:04:54 +00:00
if (!audioAPI) {
playSample();
}
lastSample = now;
}
}
processGamepad(io);
if (!paused && _requestAnimationFrame) {
_requestAnimationFrame(runFn);
}
};
if (_requestAnimationFrame) {
_requestAnimationFrame(runFn);
} else {
runTimer = setInterval(runFn, ival);
}
}
function stop() {
if (runTimer) {
clearInterval(runTimer);
}
runTimer = null;
}
function reset()
{
cpu.reset();
}
function loadBinary(bin) {
stop();
for (var idx = 0; idx < bin.length; idx++) {
var pos = bin.start + idx;
cpu.write(pos >> 8, pos & 0xff, bin.data[idx]);
}
run(bin.start);
}
function selectCategory(event) {
$("#disk_select").empty();
var cat = disk_categories[$("#category_select").val()];
if (cat) {
for (var idx = 0; idx < cat.length; idx++) {
var file = cat[idx], name = file.name;
if (file.disk) {
name += " - " + file.disk;
}
var option = $("<option />").val(file.filename).text(name)
.appendTo("#disk_select");
if (disk_cur_name[_loadDrive] == name) {
option.attr("selected", "selected");
}
}
}
}
function selectDisk(event) {
// Maybe display some info.
}
function clickDisk(event) {
doLoad();
}
function loadDisk(data) {
var name = data.name;
var category = data.category;
if (data.disk) {
name += " - " + data.disk;
}
disk_cur_cat[_loadDrive] = category;
disk_cur_name[_loadDrive] = name;
$("#disklabel" + _loadDrive).text(name);
disk2.setDisk(_loadDrive, data);
initGamepad(data.gamepad);
}
function loadJSON(data) {
if (data.type == "binary") {
loadBinary(data);
} else if ($.inArray(data.type, ["dsk","po","raw","nib"]) >= 0) {
loadDisk(data);
}
$("#loading").dialog("close");
loading = false;
}
/*
* LocalStorage Disk Storage
*/
function updateLocalStorage() {
var diskIndex = JSON.parse(window.localStorage.diskIndex || "{}");
var names = [], name, cat, idx;
for (name in diskIndex) {
if (diskIndex.hasOwnProperty(name)) {
names.push(name);
}
}
cat = disk_categories['Local Saves'] = [];
for (name in diskIndex) {
cat.push({'category': 'Local Saves',
'name': name,
'filename': 'local:' + name});
}
cat.push({'category': 'Local Saves',
'name': 'Manage Saves...',
'filename': 'local:__manage'});
$("#manage").empty();
for (idx in names) {
$("#manage").before("<span class='local_save'>" + names[idx] + " <a href='#' onclick='doDelete(\"" + names[idx] + "\")'>Delete</a><br /></span>");
}
}
function saveLocalStorage(drive, name) {
var diskIndex = JSON.parse(window.localStorage.diskIndex || "{}");
var json = disk2.getJSON(drive);
diskIndex[name] = json;
window.localStorage.diskIndex = JSON.stringify(diskIndex);
window.alert("Saved");
drivelights.label(drive, name);
drivelights.dirty(drive, false);
updateLocalStorage();
}
function deleteLocalStorage(name) {
var diskIndex = JSON.parse(window.localStorage.diskIndex || "{}");
if (diskIndex[name]) {
delete diskIndex[name];
window.alert("Deleted");
}
window.localStorage.diskIndex = JSON.stringify(diskIndex);
}
function loadLocalStorage(drive, name) {
var diskIndex = JSON.parse(window.localStorage.diskIndex || "{}");
if (diskIndex[name]) {
disk2.setJSON(drive, diskIndex[name]);
drivelights.label(drive, name);
drivelights.dirty(drive, false);
}
}
/*
* Keyboard/Gamepad routines
*/
var _key;
function _keydown(evt) {
if (evt.keyCode === 112) { // F1 - Reset
cpu.reset();
} else if (evt.keyCode === 113) { // F2 - Full Screen
var elem = document.getElementById("screen");
if (document.webkitCancelFullScreen) {
if (document.webkitIsFullScreen) {
document.webkitCancelFullScreen();
} else {
if (Element.ALLOW_KEYBOARD_INPUT) {
elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
} else {
elem.webkitRequestFullScreen();
}
}
} else if (document.mozCancelFullScreen) {
if (document.mozIsFullScreen) {
document.mozCancelFullScreen();
} else {
elem.mozRequestFullScreen();
}
}
2013-11-16 20:56:35 +00:00
} else if (evt.keyCode == 16) { // Shift
keyboard.shiftKey(true);
2013-12-19 22:55:04 +00:00
io.buttonDown(2, true);
2013-11-16 20:56:35 +00:00
} else if (evt.keyCode == 17) { // Control
keyboard.controlKey(true);
} else if (!focused && (!evt.metaKey || evt.ctrlKey)) {
evt.preventDefault();
var key = keyboard.mapKeyEvent(evt);
if (key != 0xff) {
if (_key != 0xff) io.keyUp();
io.keyDown(key);
_key = key;
}
}
}
function _keyup(evt) {
2013-11-16 20:56:35 +00:00
if (evt.keyCode == 16) { // Shift
keyboard.shiftKey(false);
2013-12-19 22:55:04 +00:00
io.buttonDown(2, false);
2013-11-16 20:56:35 +00:00
} else if (evt.keyCode == 17) { // Control
keyboard.controlKey(false);
} else {
2014-01-05 15:42:03 +00:00
_key = 0xff;
2013-11-16 20:56:35 +00:00
if (!focused) {
io.keyUp();
}
}
}
function updateScreen() {
var green = $("#green_screen").prop("checked");
scanlines = $("#show_scanlines").prop("checked");
vm.green(green);
}
var flipX = false;
var flipY = false;
var swapXY = false;
function updateJoystick() {
flipX = $("#flip_x").prop("checked");
flipY = $("#flip_y").prop("checked");
swapXY = $("#swap_x_y").prop("checked");
}
function _mousemove(evt) {
if (gamepad) {
return;
}
var s = $("#screen");
var offset = s.offset();
var x = (evt.pageX - offset.left) / s.width(),
y = (evt.pageY - offset.top) / s.height(),
z = x;
if (swapXY) {
x = y;
y = z;
}
io.paddle(0, flipX ? 1 - x : x);
io.paddle(1, flipY ? 1 - y : y);
}
var paused = false;
function pauseRun(b) {
if (paused) {
run();
b.value = "Pause";
} else {
stop();
b.value = "Run";
}
paused = !paused;
}
$(function() {
hashtag = document.location.hash;
$("button,input[type=button],a.button").button().focus(function() {
// Crazy hack required by Chrome
var self = this;
window.setTimeout(function() {
self.blur();
}, 1)
});
var canvas = document.getElementById("screen");
var context = canvas.getContext('2d');
vm.setContext(context);
/*
* Input Handling
*/
$(window).keydown(_keydown);
$(window).keyup(_keyup);
$("canvas").mousedown(function(evt) {
if (!gamepad) {
io.buttonDown(evt.which == 1 ? 0 : 1);
}
evt.preventDefault();
})
.mouseup(function(evt) {
if (!gamepad) {
io.buttonUp(evt.which == 1 ? 0 : 1);
}
})
.mousemove(_mousemove)
.bind("contextmenu", function(e) { e.preventDefault(); });
2013-12-19 22:55:04 +00:00
$("input,textarea").focus(function() { focused = true; })
.blur(function() { focused = false; });
keyboard.create($("#keyboard"));
if (prefs.havePrefs()) {
$("input[type=checkbox]").each(function() {
var val = prefs.readPref(this.id);
if (val != null)
this.checked = JSON.parse(val);
});
$("input[type=checkbox]").change(function() {
prefs.writePref(this.id, JSON.stringify(this.checked));
});
}
reset();
run();
setInterval(updateKHz, 1000);
updateSound();
updateScreen();
var cancel = function() { $(this).dialog("close"); };
$("#loading").dialog({ autoOpen: false, modal: true });
$("#options").dialog({ autoOpen: false,
modal: true,
width: 320,
height: 400,
buttons: {"Close": cancel }});
$("#load").dialog({ autoOpen: false,
modal: true,
width: 540,
buttons: {"Cancel": cancel, "Load": doLoad }});
$("#save").dialog({ autoOpen: false,
modal: true,
width: 320,
buttons: {"Cancel": cancel, "Save": doSave }});
$("#manage").dialog({ autoOpen: false,
modal: true,
width: 320,
buttons: {"Close": cancel }});
if (window.localStorage !== undefined) {
$("button.disksave").show();
}
var oldcat = "";
for (var idx = 0; idx < disk_index.length; idx++) {
var file = disk_index[idx];
var cat = file.category;
var name = file.name, disk = file.disk;
if (cat != oldcat) {
$("<option />").val(cat).text(cat).appendTo("#category_select");
disk_categories[cat] = [];
oldcat = cat;
}
disk_categories[cat].push(file);
if (disk) {
if (!disk_sets[name]) {
disk_sets[name] = []
}
disk_sets[name].push(file);
}
}
$("<option/>").text("Local Saves").appendTo("#category_select");
updateLocalStorage();
// Check for disks in hashtag
2013-12-25 18:21:01 +00:00
var filename = gup("disk") || hup();
if (filename) {
if (filename.indexOf("://") > 0) {
loadHTTP(filename);
} else {
2013-12-25 18:21:01 +00:00
loadAjax("json/disks/" + filename + ".json");
}
}
if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
$("select").removeAttr("multiple").css("height", "auto");
}
});
</script>
</head>
<body>
<div style="margin: auto; width: 604px">
<a href="http://www.w3.org/html/logo/"><img src="http://www.w3.org/html/logo/badge/html5-badge-h-solo.png" width="63" height="64" alt="HTML5 Powered" title="HTML5 Powered" style="float: right"></a>
<a href="about.html" target="_blank">
<img src="img/badge.png" id="badge" />
</a>
<h1 id="subtitle">An Apple ][ Emulator in JavaScript</h1>
<table id="display">
<tr>
<td style="vertical-align: top">
<div role="textbox" class="overscan"
onKeyDown="_keydown(event);"
onKeyUp="_keyup(event);">
<canvas id="screen" width="560" height="384"></canvas>
</div>
</td>
</tr>
</table>
<div class="inset">
<div style="float: left; width: 50%">
<div class="disk" id="disk1">&nbsp;</div>
<span id="disklabel1" class="disklabel">Disk 1</span>
<button id="diskload1" class="diskload" value="Load"
onclick="openLoad(1, event);">
Load
</button>
<button id="disksave1" class="disksave" value="Save"
onclick="openSave(1, event);">
Save
</button>
</div>
<div style="float: left; width: 50%">
<div class="disk" id="disk2">&nbsp;</div>
<span id="disklabel2" class="disklabel">Disk 2</span>
<button id="diskload2" class="diskload" value="Load"
onclick="openLoad(2, event);">
Load
</button>
<button id="disksave2" class="disksave" value="Save"
onclick="openSave(2, event);">
Save
</button>
</div>
<div style="clear: both"></div>
</div>
<div class="inset">
<div id="khz" onclick="showFPS = !showFPS">0KHz</div>
<input type="button" value="Pause" onclick="pauseRun(this)" />
<div style="float: right">
<input type="button" onclick="window.open('about.html','_html')"
name="About" value="About">
<input type="button" onclick="$('#options').dialog('open')"
name="Options" value="Options">
</div>
</div>
<div class="inset">
2014-01-05 15:42:03 +00:00
<div id="keyboard"></div>
</div>
</div>
<div id="loading" title="Loading">
Loading...
</div>
<div id="options" title="Options">
<h3>CPU</h3>
<ul>
<li>
<input type="checkbox" checked="checked" id="speed_toggle"
onclick="toggleSpeed()"/>
<label for="speed_toggle">
Regulate Speed
</label>
</li>
</ul>
<h3>Joystick</h3>
<ul>
<li>
<input type="checkbox" id="flip_x"
onclick="updateJoystick()" />
<label for="flip_x">
Flip X-Axis
</label>
</li>
<li>
<input type="checkbox" id="flip_y"
onclick="updateJoystick()" />
<label for="flip_y">
Flip Y-Axis
</label>
</li>
<li>
<input type="checkbox" id="swap_x_y"
onclick="updateJoystick()" />
<label for="swap_x_y">
Swap X-Y Axis
</label>
</li>
</ul>
<h3>Monitor</h3>
<ul>
<li>
<input type="checkbox" id="green_screen"
onclick="updateScreen()" />
<label for="green_screen">
Green Screen
</label>
</li>
<li>
<input type="checkbox" id="show_scanlines"
onclick="updateScreen()" />
<label for="show_scanlines">
Show Scanlines
</label>
</li>
</ul>
<h3>Sound</h3>
<ul>
<li>
<input type="checkbox" id="enable_sound"
onclick="updateSound()" checked="checked" />
<label for="enable_sound">
Enable (Experimental)
</label>
</li>
</ul>
</div>
<div id="save" title="Save Disk">
<form action="#" onsubmit="return false;">
Save Name: <input type="text" name="name" id="save_name"
style="width: 200px" />
</form>
</div>
<div id="manage" title="Manage Disks">
</div>
<div id="load" title="Load Disk">
<table>
<tr>
<td>
<select id="category_select" multiple="multiple"
class="ui-widget ui-state-default"
onchange="selectCategory(event)" >
</select>
</td>
<td>
<select id="disk_select" multiple="multiple"
class="ui-widget ui-state-default"
onchange="selectDisk(event)"
ondblclick="clickDisk(event)">
</select>
</td>
</tr>
</table>
<form action="#">
<input type="file" id="local_file" />
</form>
</div>
</body>
</html>