apple2js/apple2jse.html
Will Scullin 9226b47f24 Canvas update
Render both dimensions x2 for better visual results.
2014-02-05 10:53:08 -08:00

1035 lines
28 KiB
HTML

<!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 //jse - An Apple //e Emulator in JavaScript</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="Apple //jse">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta charset="utf-8" />
<meta name="description" content="Apple //jse is an Apple //e emulator written using only JavaScript and HTML5. It has color display, sound and disk support." />
<meta name="keywords" content="apple2e,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 //jse is an Apple //e 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 -->
<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/mmu.js"></script>
<script type="text/javascript" src="js/apple2e.js"></script>
<script type="text/javascript" src="js/apple2e-enhanced.js"></script>
<script type="text/javascript" src="js/apple2echar.js"></script>
<script type="text/javascript" src="js/canvas2e.js"></script>
<script type="text/javascript" src="js/slot3.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/thunderclock.js"></script>
<script type="text/javascript" src="js/cpu6502.js"></script>
<script type="text/javascript" src="js/base64.js"></script>
<script type="text/javascript" src="js/ui/keyboard2e.js"></script>
<script type="text/javascript" src="js/ui/gamepad.js"></script>
<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 enhanced = true;
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");
}
function loadHTTP(url) {
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({"65C02": enhanced});
var hgr = new HiresPage(1);
var hgr2 = new HiresPage(2);
var gr = new LoresPage(1);
var gr2 = new LoresPage(2);
var rom;
if (enhanced) {
rom = new Apple2eEnhancedROM();
} else {
rom = new Apple2eROM();
}
var vm = new VideoModes(gr, hgr, gr2, hgr2);
var drivelights = new DriveLights();
var io = new Apple2IO(cpu, vm);
var keyboard = new KeyBoard(io);
var mmu = new MMU(cpu, gr, gr2, hgr, hgr2, io, rom);
var parallel = new Parallel(io, new Printer(), 1);
var disk2 = new DiskII(io, drivelights, 6);
var slot3 = new Slot3(mmu, rom);
var slinky = new RAMFactor(mmu, io, 2, 1024 * 1024);
var clock = new Thunderclock(mmu, io, 7);
mmu.addSlot(1, parallel);
mmu.addSlot(2, slinky);
mmu.addSlot(3, slot3);
mmu.addSlot(6, disk2);
mmu.addSlot(7, clock);
cpu.addPageHandler(mmu);
var showFPS = false;
function updateKHz() {
var now = Date.now();
var ms = now - startTime;
var cycles = cpu.cycles();
var delta;
if (showFPS) {
delta = frames - lastFrames;
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;
}
/*
* Audio Handling
*/
// 8000 = 0x1f40 = 64, 31
// 16000 = 0x3e80 = 128, 62
var wavHeader =
['R','I','F','F',68 ,3 ,0 ,0 ,'W','A','V','E','f','m','t',' ',
16 ,0 ,0 ,0 ,1 ,0 ,1 ,0 ,128,62 ,0 ,0 ,128,62 ,0 ,0 ,
1 ,0 ,8 ,0 ,'d','a','t','a',32 ,3 ,0 ,0 ];
function percentEncode(ary) {
var buf = "";
for (var idx = 0; idx < ary.length; idx++) {
buf += "%" + toHex(ary[idx])
}
return buf;
}
wavHeader = $.map(wavHeader, function(n, i) {
return typeof(n) == "string" ? n.charCodeAt(0) : n;
});
wavHeaderStr = percentEncode(wavHeader);
var audioAPI = false;
var audioMoz = false;
var audioContext;
var audioNode;
var audio, audio2;
if (typeof webkitAudioContext != "undefined") {
debug("Using Web Audio API");
audioAPI = true;
audioContext = new webkitAudioContext();
audioNode = audioContext.createJavaScriptNode(4096, 1, 1);
io.floatAudio(audioContext.sampleRate);
audioNode.onaudioprocess = function(event) {
var data = event.outputBuffer.getChannelData(0);
var sample = io.getSample();
for (var idx = 0; idx < data.length; idx++) {
if (idx < sample.length) {
data[idx] = sample[idx];
} else {
data[idx] = 0;
}
}
}
} else {
audio = document.createElement("audio");
if (audio.mozSetup) {
debug("Using Mozilla Audio API");
audio.mozSetup(1, 16000);
io.floatAudio(16000);
audioMoz = true;
} else {
audio2 = document.createElement("audio");
}
}
function playSample() {
// [audio,audio2] = [audio2,audio];
var sample = io.getSample();
if (audioMoz) {
audio.mozWriteAudio(sample);
return;
}
var tmp = audio;
audio = audio2;
audio2 = tmp;
if (sample && sample.length) {
var len = sample.length,
buf = sample.join(""),
o1 = percentEncode([(len + 36) & 0xff, (len + 36) >> 8]),
o2 = percentEncode([len & 0xff, len >> 8]),
header = wavHeaderStr.replace("%44%03",o1).replace("%20%03", o2);
audio.src = "data:audio/x-wav," + header + buf;
// debug(audio.src);
audio.play();
}
}
function updateSound()
{
var sound = $("#enable_sound").attr("checked");
if (audioAPI) {
if (sound) {
audioNode.connect(audioContext.destination);
} else {
audioNode.disconnect();
}
} else {
if (sound) {
if (audio) audio.volume = 0.5;
if (audio2) audio2.volume = 0.5;
} else {
io.getSample(false);
}
}
}
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) {
if (filename.indexOf("://") > 0) {
loadHTTP(disk);
} else {
loadAjax("json/disks/" + filename + ".json");
}
}
}
if (!loading) {
mmu.resetVB();
running = true;
cpu.stepCycles(step);
running = false;
vm.blit();
if (now - lastSample >= 200) {
if (!audioAPI && sound) {
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();
}
}
} else if (evt.keyCode == 16) { // Shift
keyboard.shiftKey(true);
io.buttonDown(2, true);
} else if (evt.keyCode == 17) { // Control
keyboard.controlKey(true);
} else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command
keyboard.commandKey(true);
} else if (evt.keyCode == 18) { // Alt
keyboard.optionKey(true);
} else if (!focused) {
evt.preventDefault();
var key = keyboard.mapKeyEvent(evt);
if (key != 0xff) {
if (_key != 0xff) io.keyUp();
io.keyDown(key);
_key = key;
}
}
}
function _keyup(evt) {
if (evt.keyCode == 16) { // Shift
keyboard.shiftKey(false);
io.buttonDown(2, false);
} else if (evt.keyCode == 17) { // Control
keyboard.controlKey(false);
} else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command
keyboard.commandKey(false);
} else if (evt.keyCode == 18) { // Alt
keyboard.optionKey(false);
} else {
_key = 0xff;
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(); });
$("input,textarea").focus(function() { focused = true; })
.blur(function() { focused = false; });
$("body > div").hover(function() { focused = false; },
function() { focused = true; });
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
var filename = gup("disk") || hup();
if (filename) {
if (filename.indexOf("://") > 0) {
loadHTTP(filename);
} else {
loadAjax("json/disks/" + filename + ".json");
}
}
if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
$("select").removeAttr("multiple").css("height", "auto");
}
});
</script>
</head>
<body class="apple2e">
<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 //e 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)" />
<input type="button" value="Reset" onclick="reset()" />
<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">
<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>