mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
990 lines
27 KiB
HTML
990 lines
27 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 ][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">
|
|
<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 ][ 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 2 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 -->
|
|
<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/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/audio.js"></script>
|
|
<script type="text/javascript" src="js/ui/keyboard2.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 kHz = 1023;
|
|
|
|
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 DISK_TYPES = ['dsk','do','po','raw','nib','2mg'];
|
|
var TAPE_TYPES = ['wav','aiff','aif','mp3'];
|
|
|
|
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 parts = file.name.split(".");
|
|
var ext = parts[parts.length - 1].toLowerCase();
|
|
if ($.inArray(ext, DISK_TYPES) >= 0) {
|
|
doLoadLocalDisk(file);
|
|
} else if ($.inArray(ext, TAPE_TYPES) >= 0) {
|
|
doLoadLocalTape(file);
|
|
} else {
|
|
alert('Unknown file type: ' + ext);
|
|
$("#load").dialog("close");
|
|
}
|
|
}
|
|
}
|
|
|
|
function doLoadLocalDisk(file) {
|
|
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)) {
|
|
$("#disklabel" + _saveDrive).text(name);
|
|
$("#load").dialog("close");
|
|
initGamepad();
|
|
}
|
|
}
|
|
fileReader.readAsArrayBuffer(file);
|
|
}
|
|
|
|
function doLoadLocalTape(file) {
|
|
// Audio Buffer Source
|
|
if (typeof webkitAudioContext != "undefined") {
|
|
var context = new webkitAudioContext();
|
|
} else {
|
|
alert("Not supported by your browser");
|
|
$("#load").dialog("close");
|
|
return;
|
|
}
|
|
|
|
var fileReader = new FileReader();
|
|
fileReader.onload = function(ev) {
|
|
context.decodeAudioData(ev.target.result, function(buffer) {
|
|
var buf = [];
|
|
var data = buffer.getChannelData(0), datum = data[0];
|
|
var old = (datum > 0.0), current;
|
|
var last = 0, delta, ival;
|
|
debug('Sample Count: ' + data.length);
|
|
debug('Sample rate: ' + buffer.sampleRate);
|
|
for (var idx = 1; idx < data.length; idx++) {
|
|
datum = data[idx];
|
|
if ((datum > 0.1) || (datum < -0.1)) {
|
|
current = (datum > 0.0);
|
|
if (current != old) {
|
|
delta = idx - last;
|
|
if (delta > 2000000) {
|
|
delta = 2000000;
|
|
}
|
|
ival = delta / buffer.sampleRate * 1000;
|
|
if (ival >= 0.550 && ival < 0.750) {
|
|
ival = 0.650; // Header
|
|
} else if (ival >= 0.175 && ival < 0.225) {
|
|
ival = 0.200; // sync 1
|
|
} else if (ival >= 0.225 && ival < 0.275) {
|
|
ival = 0.250; // 0 / sync 2
|
|
} else if (ival >= 0.450 && ival < 0.550) {
|
|
ival = 0.500; // 1
|
|
} else {
|
|
// debug(idx + ' ' + buf.length + ' ' + ival);
|
|
}
|
|
buf.push(parseInt(ival * kHz));
|
|
old = current;
|
|
last = idx;
|
|
}
|
|
}
|
|
}
|
|
io.setTape(buf);
|
|
$("#load").dialog("close");
|
|
});
|
|
};
|
|
fileReader.readAsArrayBuffer(file);
|
|
}
|
|
|
|
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();
|
|
|
|
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 clock = new Thunderclock(mmu, io, 7);
|
|
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);
|
|
cpu.addPageHandler(clock);
|
|
|
|
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").html( fps + "fps");
|
|
} else {
|
|
delta = cycles - lastCycles;
|
|
khz = parseInt(delta/ms);
|
|
$("#khz").html( khz + "KHz");
|
|
}
|
|
|
|
startTime = now;
|
|
lastCycles = cycles;
|
|
lastFrames = frames;
|
|
}
|
|
|
|
/* Audio Handling */
|
|
initAudio(io);
|
|
|
|
function updateSound()
|
|
{
|
|
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 accelerated = false;
|
|
|
|
function updateSpeed()
|
|
{
|
|
accelerated = $("#accelerator_toggle").prop("checked");
|
|
kHz = accelerated ? 4092 : 1023;
|
|
io.updateHz(kHz * 1000);
|
|
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;
|
|
var now, last = Date.now();
|
|
var runFn = function() {
|
|
now = Date.now();
|
|
frames++;
|
|
|
|
var step = (now - last) * kHz, stepMax = kHz * ival;
|
|
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) {
|
|
running = true;
|
|
cpu.stepCycles(step);
|
|
running = false;
|
|
vm.blit();
|
|
io.sampleTick();
|
|
}
|
|
|
|
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, DISK_TYPES) >= 0) {
|
|
loadDisk(data);
|
|
}
|
|
initGamepad(data.gamepad);
|
|
$("#loading").dialog("close");
|
|
loading = false;
|
|
}
|
|
|
|
/*
|
|
* LocalStorage Disk Storage
|
|
*/
|
|
|
|
function updateLocalStorage() {
|
|
var diskIndex = JSON.parse(window.localStorage.diskIndex || "{}");
|
|
var names = [], name, cat;
|
|
|
|
for (name in diskIndex) {
|
|
if (diskIndex.hasOwnProperty(name)) {
|
|
names.push(name);
|
|
}
|
|
}
|
|
|
|
cat = disk_categories['Local Saves'] = [];
|
|
$("#manage").empty();
|
|
|
|
names.forEach(function(name) {
|
|
cat.push({'category': 'Local Saves',
|
|
'name': name,
|
|
'filename': 'local:' + name});
|
|
$("#manage").append("<span class='local_save'>" +
|
|
name +
|
|
" <a href='#' onclick='doDelete(\"" +
|
|
name +
|
|
"\")'>Delete</a><br /></span>");
|
|
});
|
|
cat.push({'category': 'Local Saves',
|
|
'name': 'Manage Saves...',
|
|
'filename': 'local:__manage'});
|
|
}
|
|
|
|
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);
|
|
updateLocalStorage();
|
|
}
|
|
|
|
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);
|
|
} 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) {
|
|
io.keyDown(key);
|
|
_key = key;
|
|
}
|
|
}
|
|
}
|
|
|
|
function _keyup(evt) {
|
|
_key = 0xff;
|
|
|
|
if (evt.keyCode == 16) { // Shift
|
|
keyboard.shiftKey(false);
|
|
io.buttonUp(2);
|
|
} else if (evt.keyCode == 17) { // Control
|
|
keyboard.controlKey(false);
|
|
} else {
|
|
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);
|
|
}
|
|
})
|
|
.bind("contextmenu", function(e) { e.preventDefault(); });
|
|
|
|
$('body').mousemove(_mousemove);
|
|
|
|
$("input,textarea").focus(function() { focused = true; });
|
|
$("input,textarea").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();
|
|
updateSpeed();
|
|
|
|
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();
|
|
initGamepad();
|
|
|
|
// 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>
|
|
<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 class="overscan">
|
|
<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"> </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"> </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">
|
|
<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" id="accelerator_toggle" onclick="updateSpeed()"/>
|
|
<label for="accelerator_toggle">
|
|
Accelerated CPU
|
|
</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>
|