diff --git a/css/apple2.css b/css/apple2.css index 866a263..4bf9f06 100644 --- a/css/apple2.css +++ b/css/apple2.css @@ -593,16 +593,15 @@ button:focus { list-style-type: none; } -#printer-modal .feed { - width: 640px; - height: 480px; - overflow: auto; -} - #printer-modal .paper { - min-width: 640px; - min-height: 480px; + width: 70em; + height: 90ch; + overflow: auto; background-color: white; color: black; - font-family: monospace; +} + +#printer-modal .line { + font-family: monospace; + white-space: pre; } diff --git a/js/.gitignore b/js/.gitignore deleted file mode 100644 index e83f384..0000000 --- a/js/.gitignore +++ /dev/null @@ -1 +0,0 @@ -all*.js diff --git a/js/apple2.js b/js/apple2.js new file mode 100644 index 0000000..346869e --- /dev/null +++ b/js/apple2.js @@ -0,0 +1,177 @@ +import Apple2IO from './apple2io'; +import { HiresPage, LoresPage, VideoModes } from './canvas'; +import CPU6502 from './cpu6502'; +import MMU from './mmu'; +import RAM from './ram'; +import { debug } from './util'; + +import SYMBOLS from './symbols'; + +export function Apple2(options) { + var stats = { + renderedFrames: 0 + }; + + var kHz = 1023; + + var paused = false; + + var DEBUG = false; + var TRACE = false; + var MAX_TRACE = 256; + var trace = []; + + var runTimer = null; + var cpu = new CPU6502({ '65C02': options.enhanced }); + + var gr = new LoresPage(1, options.characterRom, options.e, options.screen[0]); + var gr2 = new LoresPage(2, options.characterRom, options.e, options.screen[1]); + var hgr = new HiresPage(1, options.screen[2]); + var hgr2 = new HiresPage(2, options.screen[3]); + + var vm = new VideoModes(gr, hgr, gr2, hgr2, options.e); + vm.multiScreen(options.multiScreen); + + var io = new Apple2IO(cpu, vm); + + if (options.e) { + var mmu = new MMU(cpu, vm, gr, gr2, hgr, hgr2, io, options.rom); + cpu.addPageHandler(mmu); + } else { + var ram1 = new RAM(0x00, 0x03), + ram2 = new RAM(0x0C, 0x1F), + ram3 = new RAM(0x60, 0xBF); + + cpu.addPageHandler(ram1); + cpu.addPageHandler(gr); + cpu.addPageHandler(gr2); + cpu.addPageHandler(ram2); + cpu.addPageHandler(hgr); + cpu.addPageHandler(hgr2); + cpu.addPageHandler(ram3); + cpu.addPageHandler(io); + cpu.addPageHandler(options.rom); + } + + var _requestAnimationFrame = + window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + window.msRequestAnimationFrame; + + function run() { + if (runTimer) { + clearInterval(runTimer); + } + + var interval = 30; + + var now, last = Date.now(); + var runFn = function() { + now = Date.now(); + + var step = (now - last) * kHz, stepMax = kHz * interval; + last = now; + if (step > stepMax) { + step = stepMax; + } + + if (DEBUG) { + cpu.stepCyclesDebug(TRACE ? 1 : step, function() { + var line = cpu.dumpRegisters() + ' ' + + cpu.dumpPC(undefined, SYMBOLS); + if (TRACE) { + debug(line); + } else { + trace.push(line); + if (trace.length > MAX_TRACE) { + trace.shift(); + } + } + }); + } else { + cpu.stepCycles(step); + } + if (io.annunciator(0)) { + if (options.multiScreen) { + vm.blit(); + } + if (io.blit()) { + stats.renderedFrames++; + } + } else { + if (vm.blit()) { + stats.renderedFrames++; + } + } + io.tick(); + options.tick(); + + if (!paused && _requestAnimationFrame) { + _requestAnimationFrame(runFn); + } + }; + if (_requestAnimationFrame) { + _requestAnimationFrame(runFn); + } else { + runTimer = setInterval(runFn, interval); + } + } + + function stop() { + if (runTimer) { + clearInterval(runTimer); + } + runTimer = null; + } + + function saveState() { + var state = { + cpu: cpu.getState(), + }; + + return state; + } + + function restoreState(state) { + cpu.setState(state.cpu); + } + + return { + reset: function () { + cpu.reset(); + }, + + run: function () { + run(); + }, + + stop: function () { + stop(); + }, + + saveState: function () { + saveState(); + }, + + restoreState: function () { + restoreState(); + }, + + getStats: function () { + return stats; + }, + + getCPU: function () { + return cpu; + }, + + getIO: function () { + return io; + }, + + getVideoModes: function () { + return vm; + } + }; +} diff --git a/js/apple2io.js b/js/apple2io.js index a6cb45f..67bbb2b 100644 --- a/js/apple2io.js +++ b/js/apple2io.js @@ -338,6 +338,14 @@ export default function Apple2IO(cpu, callbacks) callbacks.reset(); }, + blit: function apple2io_blit() { + var card = _slot[3]; + if (card && card.blit) { + return card.blit(); + } + return false; + }, + read: function apple2io_read(page, off) { var result = 0; var slot; diff --git a/js/applesoft/compiler.js b/js/applesoft/compiler.js index dbf0d4c..098c578 100644 --- a/js/applesoft/compiler.js +++ b/js/applesoft/compiler.js @@ -201,7 +201,7 @@ export default function ApplesoftCompiler(mem) if (lookAhead === 'O') { result.push(lookAhead.charCodeAt(0)); foundToken = 'TO'; - tokenIdx++; foundToken = 'TO'; + tokenIdx++; } } foundToken = possibleToken; diff --git a/js/applesoft/decompiler.js b/js/applesoft/decompiler.js index 89d5a42..f33e4e2 100644 --- a/js/applesoft/decompiler.js +++ b/js/applesoft/decompiler.js @@ -154,7 +154,7 @@ export default function ApplesoftDump(mem) return str; val = readByte(addr++); - if (val > 0x80) { + if (val >= 0x80) { line += ' '; line += TOKENS[val]; line += ' '; diff --git a/js/cards/disk2.js b/js/cards/disk2.js index b3067e9..6cc5fef 100644 --- a/js/cards/disk2.js +++ b/js/cards/disk2.js @@ -32,9 +32,8 @@ export const DISK_TYPES = [ 'woz' ]; -export default function DiskII(io, slot, callbacks, sectors = 16) +export default function DiskII(io, callbacks, sectors = 16) { - slot = slot || 6; var _drives = [ { // Drive 1 format: 'dsk', @@ -134,7 +133,7 @@ export default function DiskII(io, slot, callbacks, sectors = 16) } function _init() { - debug('Disk ][ in slot', slot); + debug('Disk ]['); } var _clock = 0; diff --git a/js/cards/langcard.js b/js/cards/langcard.js index 4beb95c..bef178f 100644 --- a/js/cards/langcard.js +++ b/js/cards/langcard.js @@ -12,7 +12,7 @@ import RAM from '../ram'; import { debug } from '../util'; -export default function LanguageCard(io, slot, rom) { +export default function LanguageCard(io, rom) { var _rom = rom; var _bank1 = null; var _bank2 = null; @@ -30,7 +30,7 @@ export default function LanguageCard(io, slot, rom) { var _write2 = null; function _init() { - debug('Language card in slot', slot); + debug('Language card'); _bank1 = new RAM(0xd0, 0xdf); _bank2 = new RAM(0xd0, 0xdf); diff --git a/js/cards/parallel.js b/js/cards/parallel.js index bbf2a67..c2d6064 100644 --- a/js/cards/parallel.js +++ b/js/cards/parallel.js @@ -12,10 +12,9 @@ import { debug } from '../util'; import { rom } from '../roms/cards/parallel'; -export default function Parallel(io, slot, cbs) { - slot = slot || 1; +export default function Parallel(io, cbs) { - debug('Parallel card in slot', slot); + debug('Parallel card'); var LOC = { IOREG: 0x80 diff --git a/js/cards/ramfactor.js b/js/cards/ramfactor.js index 1389f66..122cc97 100644 --- a/js/cards/ramfactor.js +++ b/js/cards/ramfactor.js @@ -13,7 +13,7 @@ import { base64_decode, base64_encode } from '../base64'; import { allocMem, debug } from '../util'; import { rom } from '../roms/cards/ramfactor'; -export default function RAMFactor(io, slot, size) { +export default function RAMFactor(io, size) { var mem = []; var _firmware = 0; @@ -37,7 +37,7 @@ export default function RAMFactor(io, slot, size) { }; function _init() { - debug('RAMFactor card in slot', slot); + debug('RAMFactor card'); mem = allocMem(size); for (var off = 0; off < size; off++) { @@ -130,13 +130,7 @@ export default function RAMFactor(io, slot, size) { return _access(off, val); }, read: function ramfactor_read(page, off) { - var result; - if (page == 0xc0 + slot) { - result = rom[slot << 8 | off]; - } else { - result = rom[_firmware << 12 | (page - 0xC0) << 8 | off]; - } - return result; + return rom[_firmware << 12 | (page - 0xC0) << 8 | off]; }, write: function ramfactor_write() {}, reset: function ramfactor_reset() { diff --git a/js/cards/smartport.js b/js/cards/smartport.js index 66f13f1..ffc2d19 100644 --- a/js/cards/smartport.js +++ b/js/cards/smartport.js @@ -12,7 +12,7 @@ import { base64_decode } from '../base64'; import { debug, toHex } from '../util'; -export default function SmartPort(io, slot, cpu) { +export default function SmartPort(io, cpu) { /* $Cn01=$20 diff --git a/js/cards/thunderclock.js b/js/cards/thunderclock.js index ab99be0..fb08bb7 100644 --- a/js/cards/thunderclock.js +++ b/js/cards/thunderclock.js @@ -12,7 +12,7 @@ import { debug, toHex } from '../util'; import { rom } from '../roms/cards/thunderclock'; -export default function Thunderclock(io, slot) +export default function Thunderclock() { var LOC = { CONTROL: 0x80, @@ -33,7 +33,7 @@ export default function Thunderclock(io, slot) }; function _init() { - debug('Thunderclock card in slot', slot); + debug('Thunderclock'); } var _clock = false; diff --git a/js/cards/videoterm.js b/js/cards/videoterm.js index b5503d2..3d2e70e 100644 --- a/js/cards/videoterm.js +++ b/js/cards/videoterm.js @@ -12,10 +12,8 @@ import { allocMemPages, debug } from '../util'; import { ROM, VIDEO_ROM } from '../roms/cards/videoterm'; -export default function Videoterm(io, slot, context) { - slot = slot || 3; - - debug('Videx Videoterm card in slot', slot); +export default function Videoterm(io, context) { + debug('Videx Videoterm'); var LOC = { IOREG: 0x80, diff --git a/js/main2.js b/js/main2.js index bdfcf7a..7e19726 100644 --- a/js/main2.js +++ b/js/main2.js @@ -1,26 +1,14 @@ -import MicroModal from 'micromodal'; - -import Apple2IO from './apple2io'; -import ApplesoftDump from './applesoft/decompiler'; -import { HiresPage, LoresPage, VideoModes } from './canvas'; -import CPU6502 from './cpu6502'; import Prefs from './prefs'; -import RAM from './ram'; -import { debug, gup, hup } from './util'; -import Audio from './ui/audio'; -import DriveLights from './ui/drive_lights'; -import { gamepad, configGamepad, initGamepad, processGamepad } from './ui/gamepad'; -import KeyBoard from './ui/keyboard'; +import { driveLights, initUI, updateUI } from './ui/apple2'; import Printer from './ui/printer'; -import Tape, { TAPE_TYPES } from './ui/tape'; -import DiskII, { DISK_TYPES } from './cards/disk2'; +import DiskII from './cards/disk2'; import LanguageCard from './cards/langcard'; import Parallel from './cards/parallel'; import RAMFactor from './cards/ramfactor'; import Thunderclock from './cards/thunderclock'; -import Videoterm from './cards/videoterm'; +import VideoTerm from './cards/videoterm'; import apple2_charset from './roms/apple2_char'; import apple2j_charset from './roms/apple2j_char'; @@ -32,253 +20,15 @@ import Apple2jROM from './roms/apple2j_char'; import IntBASIC from './roms/intbasic'; import OriginalROM from './roms/original'; -import SYMBOLS from './symbols'; +import { Apple2 } from './apple2'; -var kHz = 1023; - -var focused = false; -var startTime = Date.now(); -var lastCycles = 0; -var renderedFrames = 0, lastFrames = 0; -var paused = false; - -var hashtag; - -var DEBUG = false; -var TRACE = false; -var MAX_TRACE = 256; -var trace = []; - -var disk_categories = {'Local Saves': []}; -var disk_sets = {}; -var disk_cur_name = []; -var disk_cur_cat = []; - -var tape; -var disk2; -var driveLights; -var _currentDrive = 1; - -export function openLoad(drive, event) -{ - _currentDrive = parseInt(drive, 10); - if (event.metaKey) { - openLoadHTTP(drive); - } else { - if (disk_cur_cat[drive]) { - document.querySelector('#category_select').value = disk_cur_cat[drive]; - selectCategory(); - } - MicroModal.show('load-modal'); - } -} - -export function openSave(drive, event) -{ - _currentDrive = parseInt(drive, 10); - - var mimetype = 'application/octet-stream'; - var data = disk2.getBinary(drive); - var a = document.querySelector('#local_save_link'); - - var blob = new Blob([data], { 'type': mimetype }); - a.href = window.URL.createObjectURL(blob); - a.download = driveLights.label(drive) + '.dsk'; - - if (event.metaKey) { - dumpDisk(drive); - } else { - document.querySelector('#save_name').value = driveLights.label(drive); - MicroModal.show('save-modal'); - } -} - -export function handleDragOver(drive, event) { - event.preventDefault(); - event.dataTransfer.dropEffect = 'copy'; -} - -export function handleDragEnd(drive, event) { - var dt = event.dataTransfer; - if (dt.items) { - for (var i = 0; i < dt.items.length; i++) { - dt.items.remove(i); - } - } else { - event.dataTransfer.clearData(); - } -} - -export function handleDrop(drive, event) { - event.preventDefault(); - event.stopPropagation(); - - if (drive < 1) { - if (!disk2.getMetadata(1)) { - drive = 1; - } else if (!disk2.getMetadata(2)) { - drive = 2; - } else { - drive = 1; - } - } - var dt = event.dataTransfer; - if (dt.files.length == 1) { - doLoadLocal(drive, dt.files[0]); - } else if (dt.files.length == 2) { - doLoadLocal(1, dt.files[0]); - doLoadLocal(2, dt.files[1]); - } else { - for (var idx = 0; idx < dt.items.length; idx++) { - if (dt.items[idx].type === 'text/uri-list') { - dt.items[idx].getAsString(function(url) { - var parts = document.location.hash.split('|'); - parts[drive - 1] = url; - document.location.hash = parts.join('|'); - }); - } - } - } -} - -var loading = false; - -function loadAjax(drive, url) { - loading = true; - MicroModal.show('loading-modal'); - - fetch(url).then(function(response) { - return response.json(); - }).then(function(data) { - if (data.type == 'binary') { - loadBinary(drive, data); - } else if (DISK_TYPES.indexOf(data.type) > -1) { - loadDisk(drive, data); - } - initGamepad(data.gamepad); - MicroModal.close('loading-modal'); - loading = false; - }).catch(function(error) { - window.alert(error || status); - MicroModal.close('loading-modal'); - loading = false; - }); -} - -export function doLoad() { - MicroModal.close('load-modal'); - var urls = document.querySelector('#disk_select').value, url; - if (urls && urls.length) { - if (typeof(urls) == 'string') { - url = urls; - } else { - url = urls[0]; - } - } - - var files = document.querySelector('#local_file').files; - if (files.length == 1) { - doLoadLocal(_currentDrive, files[0]); - } else if (url) { - var filename; - if (url.substr(0,6) == 'local:') { - filename = url.substr(6); - if (filename == '__manage') { - openManage(); - } else { - loadLocalStorage(_currentDrive, filename); - } - } else { - var r1 = /json\/disks\/(.*).json$/.exec(url); - if (r1) { - filename = r1[1]; - } else { - filename = url; - } - var parts = document.location.hash.split('|'); - parts[_currentDrive - 1] = filename; - document.location.hash = parts.join('|'); - } - } -} - -export function doSave() { - var name = document.querySelector('#save_name').value; - saveLocalStorage(_currentDrive, name); - MicroModal.close('save-modal'); -} - -export function doDelete(name) { - if (window.confirm('Delete ' + name + '?')) { - deleteLocalStorage(name); - } -} - -function doLoadLocal(drive, file) { - var parts = file.name.split('.'); - var ext = parts[parts.length - 1].toLowerCase(); - if (DISK_TYPES.indexOf(ext) > -1) { - doLoadLocalDisk(drive, file); - } else if (TAPE_TYPES.indexOf(ext) > -1) { - tape.doLoadLocalTape(file); - } else { - window.alert('Unknown file type: ' + ext); - } -} - -function doLoadLocalDisk(drive, file) { - var fileReader = new FileReader(); - fileReader.onload = function() { - var parts = file.name.split('.'); - var ext = parts.pop().toLowerCase(); - var name = parts.join('.'); - if (disk2.setBinary(drive, name, ext, this.result)) { - driveLights.label(drive, name); - initGamepad(); - } - }; - fileReader.readAsArrayBuffer(file); -} - -function doLoadHTTP(drive, _url) { - var url = _url || document.querySelector('#http_url').value; - if (url) { - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.responseType = 'arraybuffer'; - - req.onload = function() { - var urlParts = url.split('/'); - var file = urlParts.pop(); - var fileParts = file.split('.'); - var ext = fileParts.pop().toLowerCase(); - var name = decodeURIComponent(fileParts.join('.')); - if (disk2.setBinary(drive, name, ext, req.response)) { - driveLights.label(drive, name); - if (!_url) { - MicroModal.close('http-modal'); - } - initGamepad(); - } - }; - req.send(null); - } -} - -function openLoadHTTP(drive) { - _currentDrive = parseInt(drive, 10); - MicroModal.show('http-modal'); -} - -function openManage() { - MicroModal.show('manage-modal'); -} +export * from './ui/apple2'; var prefs = new Prefs(); var romVersion = prefs.readPref('computer_type2'); -var multiScreen = false; var rom; -var char_rom = apple2_charset; +var characterRom = apple2_charset; + switch (romVersion) { case 'apple2': rom = new IntBASIC(); @@ -288,735 +38,73 @@ case 'original': break; case 'apple2jplus': rom = new Apple2jROM(); - char_rom = apple2j_charset; + characterRom = apple2j_charset; break; case 'apple2pig': rom = new Apple2ROM(); - char_rom = pigfont_charset; + characterRom = pigfont_charset; break; case 'apple2lc': rom = new Apple2ROM(); - char_rom = apple2lc_charset; + characterRom = apple2lc_charset; break; default: rom = new Apple2ROM(); } -var runTimer = null; -var cpu = new CPU6502(); - -var context1, context2, context3, context4; +var options = { + screen: [], + multiScreen: false, + rom: rom, + characterRom: characterRom, + e: false, + enhanced: false, + cards: [], + tick: updateUI +}; var canvas1 = document.getElementById('screen'); var canvas2 = document.getElementById('screen2'); var canvas3 = document.getElementById('screen3'); var canvas4 = document.getElementById('screen4'); -context1 = canvas1.getContext('2d'); +options.screen[0] = canvas1.getContext('2d'); if (canvas4) { - multiScreen = true; - context2 = canvas2.getContext('2d'); - context3 = canvas3.getContext('2d'); - context4 = canvas4.getContext('2d'); + options.multiScreen = true; + options.screen[1] = canvas2.getContext('2d'); + options.screen[2] = canvas3.getContext('2d'); + options.screen[3] = canvas4.getContext('2d'); } else if (canvas2) { - multiScreen = true; - context2 = context1; - context3 = canvas2.getContext('2d'); - context4 = context3; + options.multiScreen = true; + options.screen[1] = options.screen[0]; + options.screen[2] = canvas2.getContext('2d'); + options.screen[3] = options.screen[2]; } else { - context2 = context1; - context3 = context1; - context4 = context1; + options.screen[1] = options.screen[0]; + options.screen[2] = options.screen[0]; + options.screen[3] = options.screen[0]; } -var gr = new LoresPage(1, char_rom, false, context1); -var gr2 = new LoresPage(2, char_rom, false, context2); -var hgr = new HiresPage(1, context3); -var hgr2 = new HiresPage(2, context4); +var apple2 = new Apple2(options); +var cpu = apple2.getCPU(); +var io = apple2.getIO(); -var ram1 = new RAM(0x00, 0x03), - ram2 = new RAM(0x0C, 0x1F), - ram3 = new RAM(0x60, 0xBF); - - -var vm = new VideoModes(gr, hgr, gr2, hgr2, false); -vm.multiScreen(multiScreen); -var dumper = new ApplesoftDump(cpu); - -driveLights = new DriveLights(); -export var io = new Apple2IO(cpu, vm); -var keyboard = new KeyBoard(cpu, io); -var audio = new Audio(io); -tape = new Tape(io); var printer = new Printer('#printer-modal .paper'); -var lc = new LanguageCard(io, 0, rom); -var parallel = new Parallel(io, 1, printer); -var slinky = new RAMFactor(io, 2, 1024 * 1024); -var videoterm = new Videoterm(io, 3, context1); -disk2 = new DiskII(io, 6, driveLights); -var clock = new Thunderclock(io, 7); +var lc = new LanguageCard(io, rom); +var parallel = new Parallel(io, printer); +var videoTerm = new VideoTerm(io, options.screen[0]); +var slinky = new RAMFactor(io, 1024 * 1024); +var disk2 = new DiskII(io, driveLights); +var clock = new Thunderclock(io); -cpu.addPageHandler(ram1); -cpu.addPageHandler(gr); -cpu.addPageHandler(gr2); -cpu.addPageHandler(ram2); -cpu.addPageHandler(hgr); -cpu.addPageHandler(hgr2); -cpu.addPageHandler(ram3); -cpu.addPageHandler(io); -cpu.addPageHandler(lc); +initUI(apple2, disk2, false); io.setSlot(0, lc); io.setSlot(1, parallel); io.setSlot(2, slinky); -io.setSlot(3, videoterm); +io.setSlot(4, clock); +io.setSlot(3, videoTerm); io.setSlot(6, disk2); -io.setSlot(7, clock); -var showFPS = false; - -function updateKHz() { - var now = Date.now(); - var ms = now - startTime; - var cycles = cpu.cycles(); - var delta; - - if (showFPS) { - delta = renderedFrames - lastFrames; - var fps = parseInt(delta/(ms/1000), 10); - document.querySelector('#khz').innerText = fps + 'fps'; - } else { - delta = cycles - lastCycles; - var khz = parseInt(delta/ms); - document.querySelector('#khz').innerText = khz + 'KHz'; - } - - startTime = now; - lastCycles = cycles; - lastFrames = renderedFrames; -} - -export function toggleShowFPS() { - showFPS = !showFPS; -} - -export function updateSound() { - var on = document.querySelector('#enable_sound').checked; - var label = document.querySelector('#toggle-sound i'); - audio.enable(on); - if (on) { - label.classList.remove('fa-volume-off'); - label.classList.add('fa-volume-up'); - } else { - label.classList.remove('fa-volume-up'); - label.classList.add('fa-volume-off'); - } -} - -function dumpDisk(drive) { - var wind = window.open('', '_blank'); - wind.document.title = driveLights.label(drive); - wind.document.write('
');
-    wind.document.write(disk2.getJSON(drive, true));
-    wind.document.write('
'); - wind.document.close(); -} - -export function dumpProgram() { - var wind = window.open('', '_blank'); - wind.document.title = 'Program Listing'; - wind.document.write('
');
-    wind.document.write(dumper.toString());
-    wind.document.write('
'); - wind.document.close(); -} - -export function step() -{ - if (runTimer) { - clearInterval(runTimer); - } - runTimer = null; - - cpu.step(function() { - debug(cpu.dumpRegisters()); - debug(cpu.dumpPC()); - }); -} - -var accelerated = false; - -export function updateCPU() -{ - accelerated = document.querySelector('#accelerator_toggle').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(); - - var step = (now - last) * kHz, stepMax = kHz * ival; - last = now; - if (step > stepMax) { - step = stepMax; - } - - if (document.location.hash != hashtag) { - hashtag = document.location.hash; - var hash = hup(); - if (hash) { - processHash(hash); - } - } - if (!loading) { - if (DEBUG) { - cpu.stepCyclesDebug(TRACE ? 1 : step, function() { - var line = cpu.dumpRegisters() + ' ' + - cpu.dumpPC(undefined, SYMBOLS); - if (TRACE) { - debug(line); - } else { - trace.push(line); - if (trace.length > MAX_TRACE) { - trace.shift(); - } - } - }); - } else { - cpu.stepCycles(step); - } - if (io.annunciator(0)) { - if (multiScreen) { - vm.blit(); - } - if (videoterm.blit()) { - renderedFrames++; - } - } else { - if (vm.blit()) { - renderedFrames++; - } - } - io.tick(); - } - - processGamepad(io); - - if (!paused && _requestAnimationFrame) { - _requestAnimationFrame(runFn); - } - }; - if (_requestAnimationFrame) { - _requestAnimationFrame(runFn); - } else { - runTimer = setInterval(runFn, ival); - } -} - -function stop() { - if (runTimer) { - clearInterval(runTimer); - } - runTimer = null; -} - -export function reset() -{ - cpu.reset(); -} - -var state = null; - -function storeStateLocal() { - window.localStorage['apple2.state'] = JSON.stringify(state); -} - -function restoreStateLocal() { - var data = window.localStorage['apple2.state']; - if (data) { - state = JSON.parse(data); - } -} - -function saveState() { - if (state && !window.confirm('Overwrite Saved State?')) { - return; - } - - state = { - cpu: cpu.getState(), - ram1: ram1.getState(), - ram2: ram2.getState(), - ram3: ram3.getState(), - io: io.getState(), - lc: lc.getState(), - vm: vm.getState(), - disk2: disk2.getState(), - driveLights: driveLights.getState() - }; - if (slinky) { - state.slinky = slinky.getState(); - } - - if (window.localStorage) { - storeStateLocal(); - } -} - -function restoreState() { - if (window.localStorage) { - restoreStateLocal(); - } - if (!state) { - return; - } - cpu.setState(state.cpu); - ram1.setState(state.ram1); - ram2.setState(state.ram2); - ram3.setState(state.ram3); - io.setState(state.io); - lc.setState(state.lc); - vm.setState(state.vm); - disk2.setState(state.disk2); - driveLights.setState(state.driveLights); - if (slinky && state.slinky) { - slinky.setState(state.slinky); - } -} - -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); -} - -export function selectCategory() { - document.querySelector('#disk_select').innerHTML = ''; - var cat = disk_categories[document.querySelector('#category_select').value]; - 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 = document.createElement('option'); - option.value = file.filename; - option.innerText = name; - document.querySelector('#disk_select').append(option); - if (disk_cur_name[_currentDrive] == name) { - option.selected = true; - } - } - } -} - -export function selectDisk() { - document.querySelector('#local_file').value = ''; -} - -export function clickDisk() { - doLoad(); -} - -function loadDisk(drive, disk) { - var name = disk.name; - var category = disk.category; - - if (disk.disk) { - name += ' - ' + disk.disk; - } - - disk_cur_cat[drive] = category; - disk_cur_name[drive] = name; - - driveLights.label(drive, name); - disk2.setDisk(drive, disk); - initGamepad(disk.gamepad); -} - -/* - * 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'] = []; - document.querySelector('#manage-modal-content').innerHTML = ''; - - names.forEach(function(name) { - cat.push({ - 'category': 'Local Saves', - 'name': name, - 'filename': 'local:' + name - }); - document.querySelector('#manage-modal-content').innerHTML = - '' + - name + - ' Delete
'; - }); - 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); - } -} - -function processHash(hash) { - var files = hash.split('|'); - for (var idx = 0; idx < files.length; idx++) { - var file = files[idx]; - if (file.indexOf('://') > 0) { - var parts = file.split('.'); - var ext = parts[parts.length - 1].toLowerCase(); - if (ext == 'json') { - loadAjax(idx + 1, file); - } else { - doLoadHTTP(idx + 1, file); - } - } else { - loadAjax(idx + 1, 'json/disks/' + file + '.json'); - } - } -} - -/* - * Keyboard/Gamepad routines - */ - -function _keydown(evt) { - if (!focused && (!evt.metaKey || evt.ctrlKey)) { - evt.preventDefault(); - - var key = keyboard.mapKeyEvent(evt); - if (key != 0xff) { - io.keyDown(key); - } - } - if (evt.keyCode === 112) { // F1 - Reset - cpu.reset(); - evt.preventDefault(); // prevent launching help - } else if (evt.keyCode === 113) { // F2 - Full Screen - var elem = document.getElementById('screen'); - if (evt.shiftKey) { // Full window, but not full screen - document.querySelector('#display').classList.toggle('zoomwindow'); - document.querySelector('#display > div').classList.toggle('overscan'); - document.querySelector('#display > div').classList.toggle('flexbox-centering'); - document.querySelector('#screen').classList.toggle('maxhw'); - document.querySelector('#header').classList.toggle('hidden'); - document.querySelectorAll('.inset').forEach(function(el) { el.classList.toggle('hidden'); }); - } else 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 === 114) { // F3 - io.keyDown(0x1b); - } else if (evt.keyCode === 117) { // F6 Quick Save - saveState(); - } else if (evt.keyCode === 120) { // F9 Quick Restore - restoreState(); - } else if (evt.keyCode == 16) { // Shift - keyboard.shiftKey(true); - } else if (evt.keyCode == 17) { // Control - keyboard.controlKey(true); - } -} - -function _keyup(evt) { - if (!focused) - io.keyUp(); - - if (evt.keyCode == 16) { // Shift - keyboard.shiftKey(false); - } else if (evt.keyCode == 17) { // Control - keyboard.controlKey(false); - } -} - -export function updateScreen() { - var green = document.querySelector('#green_screen').checked; - var scanlines = document.querySelector('#show_scanlines').checked; - - vm.green(green); - vm.scanlines(scanlines); -} - -var disableMouseJoystick = false; -var flipX = false; -var flipY = false; -var swapXY = false; - -export function updateJoystick() { - disableMouseJoystick = document.querySelector('#disable_mouse').checked; - flipX = document.querySelector('#flip_x').checked; - flipY = document.querySelector('#flip_y').checked; - swapXY = document.querySelector('#swap_x_y').checked; - configGamepad(flipX, flipY); - - if (disableMouseJoystick) { - io.paddle(0, 0.5); - io.paddle(1, 0.5); - return; - } -} - -function _mousemove(evt) { - if (gamepad || disableMouseJoystick) { - return; - } - - var s = document.querySelector('#screen'); - var offset = s.getBoundingClientRect(); - var x = (evt.pageX - offset.left) / s.clientWidth, - y = (evt.pageY - offset.top) / s.clientHeight, - z = x; - - if (swapXY) { - x = y; - y = z; - } - - io.paddle(0, flipX ? 1 - x : x); - io.paddle(1, flipY ? 1 - y : y); -} - -export function pauseRun() { - var label = document.querySelector('#pause-run i'); - if (paused) { - run(); - label.classList.remove('fa-play'); - label.classList.add('fa-pause'); - } else { - stop(); - label.classList.remove('fa-pause'); - label.classList.add('fa-play'); - } - paused = !paused; -} - -export function toggleSound() { - var enableSound = document.querySelector('#enable_sound'); - enableSound.checked = !enableSound.checked; - updateSound(); -} - -export function openOptions() { - MicroModal.show('options-modal'); -} - -export function openPrinterModal() { - MicroModal.show('printer-modal'); -} - -MicroModal.init(); - -document.addEventListener('DOMContentLoaded', function() { - hashtag = document.location.hash; - - /* - * Input Handling - */ - - window.addEventListener('keydown', _keydown); - window.addEventListener('keyup', _keyup); - window.addEventListener('mousedown', function() { audio.autoStart(); }); - - document.querySelectorAll('canvas').forEach(function(canvas) { - canvas.addEventListener('mousedown', function(evt) { - if (!gamepad) { - io.buttonDown(evt.which == 1 ? 0 : 1); - } - evt.preventDefault(); - }); - canvas.addEventListener('mouseup', function(evt) { - if (!gamepad) { - io.buttonUp(evt.which == 1 ? 0 : 1); - } - }); - }); - - document.body.addEventListener('mousemove', _mousemove); - - document.querySelectorAll('input,textarea').forEach(function(input) { - input.addEventListener('focus', function() { - focused = true; - }); - input.addEventListener('blur', function() { - focused = false; - }); - }); - - keyboard.create('#keyboard'); - - if (prefs.havePrefs()) { - document.querySelectorAll('#options-modal input[type=checkbox]').forEach(function(el) { - var val = prefs.readPref(el.id); - if (val) { - el.checked = JSON.parse(val); - } - el.addEventListener('change', function() { - prefs.writePref(el.id, JSON.stringify(el.checked)); - }); - }); - document.querySelectorAll('#options-modal select').forEach(function(el) { - var val = prefs.readPref(el.id); - if (val) { - el.value = val; - } - el.addEventListener('change', function() { - prefs.writePref(el.id, el.value); - }); - }); - } - - cpu.reset(); - setInterval(updateKHz, 1000); - updateSound(); - updateScreen(); - updateCPU(); - - if (window.localStorage !== undefined) { - document.querySelectorAll('.disksave').forEach(function (el) { el.style.display = 'inline-block';}); - } - - var oldcat = ''; - var option; - for (var idx = 0; idx < window.disk_index.length; idx++) { - var file = window.disk_index[idx]; - var cat = file.category; - var name = file.name, disk = file.disk; - if (file.e) { - continue; - } - if (cat != oldcat) { - option = document.createElement('option'); - option.value = cat; - option.innerText = cat; - document.querySelector('#category_select').append(option); - - 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 = document.createElement('option'); - option.innerText = 'Local Saves'; - document.querySelector('#category_select').append(option); - - updateLocalStorage(); - initGamepad(); - - // Check for disks in hashtag - - var hash = gup('disk') || hup(); - if (hash) { - processHash(hash); - } - - if (navigator.standalone) { - document.body.classList.add('standalone'); - } - - var reptKey = document.querySelector('.key-REPT'); - reptKey.addEventListener('click', function() { - document.querySelector('#keyboard').style.display = 'none'; - document.querySelector('#textarea').style.display = 'block'; - document.querySelector('#textarea').focus(); - }); - document.querySelector('#text_input').addEventListener('keydown', function() { - focused = document.querySelector('#buffering').checked; - }); - document.querySelector('#text_input').addEventListener('keyup', function() { - focused = document.querySelector('#buffering').checked; - }); - - run(); -}); +cpu.addPageHandler(lc); diff --git a/js/main2e.js b/js/main2e.js index dbd679a..465e3e7 100644 --- a/js/main2e.js +++ b/js/main2e.js @@ -1,22 +1,9 @@ -import MicroModal from 'micromodal'; - -import Apple2IO from './apple2io'; -import ApplesoftDump from './applesoft/decompiler'; -import ApplesoftCompiler from './applesoft/compiler'; -import { HiresPage, LoresPage, VideoModes } from './canvas'; -import CPU6502 from './cpu6502'; -import MMU from './mmu'; import Prefs from './prefs'; -import { debug, gup, hup } from './util'; -import Audio from './ui/audio'; -import DriveLights from './ui/drive_lights'; -import { gamepad, configGamepad, initGamepad, processGamepad } from './ui/gamepad'; -import KeyBoard from './ui/keyboard'; +import { driveLights, initUI, updateUI } from './ui/apple2'; import Printer from './ui/printer'; -import Tape, { TAPE_TYPES } from './ui/tape'; -import DiskII, { DISK_TYPES } from './cards/disk2'; +import DiskII from './cards/disk2'; import Parallel from './cards/parallel'; import RAMFactor from './cards/ramfactor'; import Thunderclock from './cards/thunderclock'; @@ -28,966 +15,79 @@ import rmfont_charset from './roms/rmfont_char'; import Apple2eROM from './roms/apple2e'; import Apple2eEnhancedROM from './roms/apple2enh'; -import SYMBOLS from './symbols'; +import { Apple2 } from './apple2'; -var kHz = 1023; - -var focused = false; -var startTime = Date.now(); -var lastCycles = 0; -var renderedFrames = 0, lastFrames = 0; -var paused = false; - -var hashtag; - -var DEBUG = true; -var TRACE = false; -var MAX_TRACE = 256; -var trace = []; - -var disk_categories = {'Local Saves': []}; -var disk_sets = {}; -var disk_cur_name = []; -var disk_cur_cat = []; - -var tape; -var disk2; -var driveLights; -var _currentDrive = 1; - -export function setTrace(debug, trace) { - DEBUG = debug; - TRACE = trace; -} - -export function getTrace() { - return trace; -} - -export function openLoad(drive, event) -{ - _currentDrive = parseInt(drive, 10); - if (event.metaKey) { - openLoadHTTP(drive); - } else { - if (disk_cur_cat[drive]) { - document.querySelector('#category_select').value = disk_cur_cat[drive]; - selectCategory(); - } - MicroModal.show('load-modal'); - } -} - -export function openSave(drive, event) -{ - _currentDrive = parseInt(drive, 10); - - var mimetype = 'application/octet-stream'; - var data = disk2.getBinary(drive); - var a = document.querySelector('#local_save_link'); - - var blob = new Blob([data], { 'type': mimetype }); - a.href = window.URL.createObjectURL(blob); - a.download = driveLights.label(drive) + '.dsk'; - - if (event.metaKey) { - dumpDisk(drive); - } else { - document.querySelector('#save_name').value = driveLights.label(drive); - MicroModal.show('save-modal'); - } -} - -export function handleDragOver(drive, event) { - event.preventDefault(); - event.dataTransfer.dropEffect = 'copy'; -} - -export function handleDragEnd(drive, event) { - var dt = event.dataTransfer; - if (dt.items) { - for (var i = 0; i < dt.items.length; i++) { - dt.items.remove(i); - } - } else { - event.dataTransfer.clearData(); - } -} - -export function handleDrop(drive, event) { - event.preventDefault(); - event.stopPropagation(); - - if (drive < 1) { - if (!disk2.getMetadata(1)) { - drive = 1; - } else if (!disk2.getMetadata(2)) { - drive = 2; - } else { - drive = 1; - } - } - var dt = event.dataTransfer; - if (dt.files.length == 1) { - doLoadLocal(drive, dt.files[0]); - } else if (dt.files.length == 2) { - doLoadLocal(1, dt.files[0]); - doLoadLocal(2, dt.files[1]); - } else { - for (var idx = 0; idx < dt.items.length; idx++) { - if (dt.items[idx].type === 'text/uri-list') { - dt.items[idx].getAsString(function(url) { - var parts = document.location.hash.split('|'); - parts[drive - 1] = url; - document.location.hash = parts.join('|'); - }); - } - } - } -} - -var loading = false; - -function loadAjax(drive, url) { - loading = true; - MicroModal.show('loading-modal'); - - fetch(url).then(function(response) { - return response.json(); - }).then(function(data) { - if (data.type == 'binary') { - loadBinary(drive, data); - } else if (DISK_TYPES.indexOf(data.type) > -1) { - loadDisk(drive, data); - } - initGamepad(data.gamepad); - MicroModal.close('loading-modal'); - loading = false; - }).catch(function(error) { - window.alert(error || status); - MicroModal.close('loading-modal'); - loading = false; - }); -} - -export function doLoad() { - MicroModal.close('load-modal'); - var urls = document.querySelector('#disk_select').value, url; - if (urls && urls.length) { - if (typeof(urls) == 'string') { - url = urls; - } else { - url = urls[0]; - } - } - - var files = document.querySelector('#local_file').files; - if (files.length == 1) { - doLoadLocal(_currentDrive, files[0]); - } else if (url) { - var filename; - MicroModal.close('load-modal'); - if (url.substr(0,6) == 'local:') { - filename = url.substr(6); - if (filename == '__manage') { - openManage(); - } else { - loadLocalStorage(_currentDrive, filename); - } - } else { - var r1 = /json\/disks\/(.*).json$/.exec(url); - if (r1) { - filename = r1[1]; - } else { - filename = url; - } - var parts = document.location.hash.split('|'); - parts[_currentDrive - 1] = filename; - document.location.hash = parts.join('|'); - } - } -} - -export function doSave() { - var name = document.querySelector('#save_name').value; - saveLocalStorage(_currentDrive, name); - MicroModal.close('save-modal'); -} - -export function doDelete(name) { - if (window.confirm('Delete ' + name + '?')) { - deleteLocalStorage(name); - } -} - -function doLoadLocal(drive, file) { - var parts = file.name.split('.'); - var ext = parts[parts.length - 1].toLowerCase(); - if (DISK_TYPES.indexOf(ext) > -1) { - doLoadLocalDisk(drive, file); - } else if (TAPE_TYPES.indexOf(ext) > -1) { - tape.doLoadLocalTape(file); - } else { - window.alert('Unknown file type: ' + ext); - } -} - -function doLoadLocalDisk(drive, file) { - var fileReader = new FileReader(); - fileReader.onload = function() { - var parts = file.name.split('.'); - var ext = parts.pop().toLowerCase(); - var name = parts.join('.'); - if (disk2.setBinary(drive, name, ext, this.result)) { - driveLights.label(drive, name); - initGamepad(); - } - }; - fileReader.readAsArrayBuffer(file); -} - -function doLoadHTTP(drive, _url) { - var url = _url || document.querySelector('#http_url').value; - if (url) { - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.responseType = 'arraybuffer'; - - req.onload = function() { - if (req.status !== 200) { - return window.alert('Unable to load "' + url + '"'); - } - var urlParts = url.split('/'); - var file = urlParts.pop(); - var fileParts = file.split('.'); - var ext = fileParts.pop().toLowerCase(); - var name = decodeURIComponent(fileParts.join('.')); - if (disk2.setBinary(drive, name, ext, req.response)) { - driveLights.label(drive, name); - if (!_url) { - MicroModal.close('http-modal'); - } - initGamepad(); - } - }; - req.send(null); - } -} - -function openLoadHTTP(drive) { - _currentDrive = parseInt(drive, 10); - MicroModal.show('http-modal'); -} - -function openManage() { - MicroModal.show('manage-modal'); -} +export * from './ui/apple2'; var prefs = new Prefs(); var romVersion = prefs.readPref('computer_type2e'); var enhanced = false; -var multiScreen = false; var rom; -var char_rom = apple2e_charset; +var characterRom = apple2e_charset; + switch (romVersion) { case 'apple2e': rom = new Apple2eROM(); break; case 'apple2rm': rom = new Apple2eEnhancedROM(); - char_rom = rmfont_charset; + characterRom = rmfont_charset; enhanced = true; break; default: rom = new Apple2eEnhancedROM(); - char_rom =apple2enh_charset; + characterRom = apple2enh_charset; enhanced = true; } -var runTimer = null; -export var cpu = new CPU6502({'65C02': enhanced}); - -var context1, context2, context3, context4; +var options = { + screen: [], + multiScreen: false, + rom: rom, + characterRom: characterRom, + e: true, + enhanced: enhanced, + cards: [], + tick: updateUI +}; var canvas1 = document.getElementById('screen'); var canvas2 = document.getElementById('screen2'); var canvas3 = document.getElementById('screen3'); var canvas4 = document.getElementById('screen4'); -context1 = canvas1.getContext('2d'); +options.screen[0] = canvas1.getContext('2d'); if (canvas4) { - multiScreen = true; - context2 = canvas2.getContext('2d'); - context3 = canvas3.getContext('2d'); - context4 = canvas4.getContext('2d'); + options.multiScreen = true; + options.screen[1] = canvas2.getContext('2d'); + options.screen[2] = canvas3.getContext('2d'); + options.screen[3] = canvas4.getContext('2d'); } else if (canvas2) { - multiScreen = true; - context2 = context1; - context3 = canvas2.getContext('2d'); - context4 = context3; + options.multiScreen = true; + options.screen[1] = options.screen[0]; + options.screen[2] = canvas2.getContext('2d'); + options.screen[3] = options.screen[2]; } else { - context2 = context1; - context3 = context1; - context4 = context1; + options.screen[1] = options.screen[0]; + options.screen[2] = options.screen[0]; + options.screen[3] = options.screen[0]; } -var gr = new LoresPage(1, char_rom, true, context1); -var gr2 = new LoresPage(2, char_rom, true, context2); -var hgr = new HiresPage(1, context3); -var hgr2 = new HiresPage(2, context4); +var apple2 = new Apple2(options); +var io = apple2.getIO(); -var vm = new VideoModes(gr, hgr, gr2, hgr2, true); -vm.enhanced(enhanced); -vm.multiScreen(multiScreen); -var dumper = new ApplesoftDump(cpu); -var compiler = new ApplesoftCompiler(cpu); - -driveLights = new DriveLights(); -var io = new Apple2IO(cpu, vm); -var keyboard = new KeyBoard(cpu, io, true); -var audio = new Audio(io); -tape = new Tape(io); var printer = new Printer('#printer-modal .paper'); -var mmu = new MMU(cpu, vm, gr, gr2, hgr, hgr2, io, rom); +var parallel = new Parallel(io, printer); +var slinky = new RAMFactor(io, 1024 * 1024); +var disk2 = new DiskII(io, driveLights); +var clock = new Thunderclock(io); -cpu.addPageHandler(mmu); - -var parallel = new Parallel(io, 1, printer); -var slinky = new RAMFactor(io, 2, 1024 * 1024); -disk2 = new DiskII(io, 6, driveLights); -var clock = new Thunderclock(io, 7); +initUI(apple2, disk2, enhanced); io.setSlot(1, parallel); io.setSlot(2, slinky); +io.setSlot(4, clock); io.setSlot(6, disk2); -io.setSlot(7, clock); -var showFPS = false; -function updateKHz() { - var now = Date.now(); - var ms = now - startTime; - var cycles = cpu.cycles(); - var delta; - - if (showFPS) { - delta = renderedFrames - lastFrames; - var fps = parseInt(delta/(ms/1000), 10); - document.querySelector('#khz').innerText = fps + 'fps'; - } else { - delta = cycles - lastCycles; - var khz = parseInt(delta/ms); - document.querySelector('#khz').innerText = khz + 'KHz'; - } - - startTime = now; - lastCycles = cycles; - lastFrames = renderedFrames; -} - -export function toggleShowFPS() { - showFPS = !showFPS; -} - -export function updateSound() { - var on = document.querySelector('#enable_sound').checked; - var label = document.querySelector('#toggle-sound i'); - audio.enable(on); - if (on) { - label.classList.remove('fa-volume-off'); - label.classList.add('fa-volume-up'); - } else { - label.classList.remove('fa-volume-up'); - label.classList.add('fa-volume-off'); - } -} - -function dumpDisk(drive) { - var wind = window.open('', '_blank'); - wind.document.title = driveLights.label(drive); - wind.document.write('
');
-    wind.document.write(disk2.getJSON(drive, true));
-    wind.document.write('
'); - wind.document.close(); -} - -export function dumpProgram() { - debug(dumper.toString()); -} - -export function compileProgram(program) { - compiler.compile(program); -} - -export function step() -{ - if (runTimer) { - clearInterval(runTimer); - } - runTimer = null; - - cpu.step(function() { - debug(cpu.dumpRegisters()); - debug(cpu.dumpPC()); - }); -} - -var accelerated = false; - -export function updateCPU() -{ - accelerated = document.querySelector('#accelerator_toggle').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(); - - var step = (now - last) * kHz, stepMax = kHz * ival; - last = now; - if (step > stepMax) { - step = stepMax; - } - - if (document.location.hash != hashtag) { - hashtag = document.location.hash; - var hash = hup(); - if (hash) { - processHash(hash); - } - } - if (!loading) { - mmu.resetVB(); - if (DEBUG) { - cpu.stepCyclesDebug(TRACE ? 1 : step, function() { - var line = cpu.dumpRegisters() + ' ' + - cpu.dumpPC(undefined, SYMBOLS); - if (TRACE) { - debug(line); - } else { - trace.push(line); - if (trace.length > MAX_TRACE) { - trace.shift(); - } - } - }); - } else { - cpu.stepCycles(step); - } - if (vm.blit()) { - renderedFrames++; - } - io.tick(); - } - - processGamepad(io); - - if (!paused && _requestAnimationFrame) { - _requestAnimationFrame(runFn); - } - }; - if (_requestAnimationFrame) { - _requestAnimationFrame(runFn); - } else { - runTimer = setInterval(runFn, ival); - } -} - -function stop() { - if (runTimer) { - clearInterval(runTimer); - } - runTimer = null; -} - -export function reset() -{ - cpu.reset(); -} - -var state = null; - -function storeStateLocal() { - window.localStorage['apple2.state'] = JSON.stringify(state); -} - -function restoreStateLocal() { - var data = window.localStorage['apple2.state']; - if (data) { - state = JSON.parse(data); - } -} - -function saveState() { - if (state && !window.confirm('Overwrite Saved State?')) { - return; - } - - state = { - cpu: cpu.getState(), - io: io.getState(), - mmu: mmu.getState(), - vm: vm.getState(), - disk2: disk2.getState(), - driveLights: driveLights.getState() - }; - if (slinky) { - state.slinky = slinky.getState(); - } - - if (window.localStorage) { - storeStateLocal(); - } -} - -function restoreState() { - if (window.localStorage) { - restoreStateLocal(); - } - if (!state) { - return; - } - cpu.setState(state.cpu); - io.setState(state.io); - mmu.setState(state.mmu); - vm.setState(state.vm); - disk2.setState(state.disk2); - driveLights.setState(state.driveLights); - if (slinky && state.slinky) { - slinky.setState(state.slinky); - } -} - -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); -} - -export function selectCategory() { - document.querySelector('#disk_select').innerHTML = ''; - var cat = disk_categories[document.querySelector('#category_select').value]; - 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 = document.createElement('option'); - option.value = file.filename; - option.innerText = name; - document.querySelector('#disk_select').append(option); - if (disk_cur_name[_currentDrive] == name) { - option.selected = true; - } - } - } -} - -export function selectDisk() { - document.querySelector('#local_file').value = ''; -} - -export function clickDisk() { - doLoad(); -} - -function loadDisk(drive, disk) { - var name = disk.name; - var category = disk.category; - - if (disk.disk) { - name += ' - ' + disk.disk; - } - - disk_cur_cat[drive] = category; - disk_cur_name[drive] = name; - - driveLights.label(drive, name); - disk2.setDisk(drive, disk); - initGamepad(disk.gamepad); -} - -/* - * 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'] = []; - document.querySelector('#manage-modal-content').innerHTML = ''; - - names.forEach(function(name) { - cat.push({ - 'category': 'Local Saves', - 'name': name, - 'filename': 'local:' + name - }); - document.querySelector('#manage-modal-content').innerHTML = - '' + - name + - ' Delete
'; - }); - 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); - } -} - -function processHash(hash) { - var files = hash.split('|'); - for (var idx = 0; idx < files.length; idx++) { - var file = files[idx]; - if (file.indexOf('://') > 0) { - var parts = file.split('.'); - var ext = parts[parts.length - 1].toLowerCase(); - if (ext == 'json') { - loadAjax(idx + 1, file); - } else { - doLoadHTTP(idx + 1, file); - } - } else { - loadAjax(idx + 1, 'json/disks/' + file + '.json'); - } - } -} - -/* - * Keyboard/Gamepad routines - */ - -function _keydown(evt) { - if (!focused && (!evt.metaKey || evt.ctrlKey)) { - evt.preventDefault(); - - var key = keyboard.mapKeyEvent(evt); - if (key != 0xff) { - io.keyDown(key); - } - } - if (evt.keyCode === 112) { // F1 - Reset - cpu.reset(); - evt.preventDefault(); // prevent launching help - } else if (evt.keyCode === 113) { // F2 - Full Screen - var elem = document.getElementById('screen'); - if (evt.shiftKey) { // Full window, but not full screen - document.querySelector('#display').classList.toggle('zoomwindow'); - document.querySelector('#display > div').classList.toggle('overscan'); - document.querySelector('#display > div').classList.toggle('flexbox-centering'); - document.querySelector('#screen').classList.toggle('maxhw'); - document.querySelector('#header').classList.toggle('hidden'); - document.querySelectorAll('.inset').forEach(function(el) { el.classList.toggle('hidden'); }); - document.querySelector('#reset').classList.toggle('hidden'); - } else 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 === 114) { // F3 - io.keyDown(0x1b); - } else if (evt.keyCode === 117) { // F6 Quick Save - saveState(); - } else if (evt.keyCode === 120) { // F9 Quick Restore - restoreState(); - } else if (evt.keyCode == 16) { // Shift - keyboard.shiftKey(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 - if (evt.location == 1) { - keyboard.commandKey(true); - } else { - keyboard.optionKey(true); - } - } -} - -function _keyup(evt) { - if (!focused) - io.keyUp(); - - if (evt.keyCode == 16) { // Shift - keyboard.shiftKey(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 - if (evt.location == 1) { - keyboard.commandKey(false); - } else { - keyboard.optionKey(false); - } - } -} - -export function updateScreen() { - var green = document.querySelector('#green_screen').checked; - var scanlines = document.querySelector('#show_scanlines').checked; - - vm.green(green); - vm.scanlines(scanlines); -} - -var disableMouseJoystick = false; -var flipX = false; -var flipY = false; -var swapXY = false; - -export function updateJoystick() { - disableMouseJoystick = document.querySelector('#disable_mouse').checked; - flipX = document.querySelector('#flip_x').checked; - flipY = document.querySelector('#flip_y').checked; - swapXY = document.querySelector('#swap_x_y').checked; - configGamepad(flipX, flipY); - - if (disableMouseJoystick) { - io.paddle(0, 0.5); - io.paddle(1, 0.5); - return; - } -} - -function _mousemove(evt) { - if (gamepad || disableMouseJoystick) { - return; - } - - var s = document.querySelector('#screen'); - var offset = s.getBoundingClientRect(); - var x = (evt.pageX - offset.left) / s.clientWidth, - y = (evt.pageY - offset.top) / s.clientHeight, - z = x; - - if (swapXY) { - x = y; - y = z; - } - - io.paddle(0, flipX ? 1 - x : x); - io.paddle(1, flipY ? 1 - y : y); -} - -export function pauseRun() { - var label = document.querySelector('#pause-run i'); - if (paused) { - run(); - label.classList.remove('fa-play'); - label.classList.add('fa-pause'); - } else { - stop(); - label.classList.remove('fa-pause'); - label.classList.add('fa-play'); - } - paused = !paused; -} - -export function toggleSound() { - var enableSound = document.querySelector('#enable_sound'); - enableSound.checked = !enableSound.checked; - updateSound(); -} - -export function openOptions() { - MicroModal.show('options-modal'); -} - -export function openPrinterModal() { - MicroModal.show('printer-modal'); -} - -MicroModal.init(); - -document.addEventListener('DOMContentLoaded', function() { - hashtag = document.location.hash; - - /* - * Input Handling - */ - - window.addEventListener('keydown', _keydown); - window.addEventListener('keyup', _keyup); - window.addEventListener('mousedown', function() { audio.autoStart(); }); - - document.querySelectorAll('canvas').forEach(function(canvas) { - canvas.addEventListener('mousedown', function(evt) { - if (!gamepad) { - io.buttonDown(evt.which == 1 ? 0 : 1); - } - evt.preventDefault(); - }); - canvas.addEventListener('mouseup', function(evt) { - if (!gamepad) { - io.buttonUp(evt.which == 1 ? 0 : 1); - } - }); - }); - - document.body.addEventListener('mousemove', _mousemove); - - document.querySelectorAll('input,textarea').forEach(function(input) { - input.addEventListener('input', function() { focused = true; }); - input.addEventListener('blur', function() { focused = false; }); - }); - - keyboard.create('#keyboard'); - - if (prefs.havePrefs()) { - document.querySelectorAll('#options-modal input[type=checkbox]').forEach(function(el) { - var val = prefs.readPref(el.id); - if (val) { - el.checked = JSON.parse(val); - } - el.addEventListener('change', function() { - prefs.writePref(el.id, JSON.stringify(el.checked)); - }); - }); - document.querySelectorAll('#options-modal select').forEach(function(el) { - var val = prefs.readPref(el.id); - if (val) { - el.value = val; - } - el.addEventListener('change', function() { - prefs.writePref(el.id, el.value); - }); - }); - } - - cpu.reset(); - setInterval(updateKHz, 1000); - updateSound(); - updateScreen(); - updateCPU(); - - if (window.localStorage !== undefined) { - document.querySelectorAll('.disksave').forEach(function (el) { el.style.display = 'inline-block';}); - } - - var oldcat = ''; - var option; - for (var idx = 0; idx < window.disk_index.length; idx++) { - var file = window.disk_index[idx]; - var cat = file.category; - var name = file.name, disk = file.disk; - if (cat != oldcat) { - option = document.createElement('option'); - option.value = cat; - option.innerText = cat; - document.querySelector('#category_select').append(option); - - 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 = document.createElement('option'); - option.innerText = 'Local Saves'; - document.querySelector('#category_select').append(option); - - updateLocalStorage(); - initGamepad(); - - // Check for disks in hashtag - - var hash = gup('disk') || hup(); - if (hash) { - processHash(hash); - } - - if (navigator.standalone) { - document.body.classList.add('standalone'); - } - - run(); -}); diff --git a/js/ui/.gitignore b/js/ui/.gitignore deleted file mode 100644 index b27bc7c..0000000 --- a/js/ui/.gitignore +++ /dev/null @@ -1 +0,0 @@ -ui-all*.js diff --git a/js/ui/apple2.js b/js/ui/apple2.js new file mode 100644 index 0000000..5dfec69 --- /dev/null +++ b/js/ui/apple2.js @@ -0,0 +1,763 @@ +import MicroModal from 'micromodal'; + +import Audio from './audio'; +import DriveLights from './drive_lights'; +import { DISK_TYPES } from '../cards/disk2'; +import { gamepad, configGamepad, initGamepad } from './gamepad'; +import KeyBoard from './keyboard'; +import Tape, { TAPE_TYPES } from './tape'; + +import ApplesoftDump from '../applesoft/decompiler'; +import ApplesoftCompiler from '../applesoft/compiler'; + +import { debug, gup, hup } from '../util'; +import Prefs from '../prefs'; + +var kHz = 1023; + +var focused = false; +var startTime = Date.now(); +var lastCycles = 0; +var lastFrames = 0; + +var hashtag; + +var disk_categories = {'Local Saves': []}; +var disk_sets = {}; +var disk_cur_name = []; +var disk_cur_cat = []; + +var _apple2; +var cpu; +var stats; +var vm; +var tape; +var _disk2; +var audio; +var keyboard; +var io; +var _currentDrive = 1; + +export const driveLights = new DriveLights(); + +export function dumpAppleSoftProgram() { + var dumper = new ApplesoftDump(cpu); + debug(dumper.toString()); +} + +export function compileAppleSoftProgram(program) { + var compiler = new ApplesoftCompiler(cpu); + compiler.compile(program); +} + +export function openLoad(drive, event) { + _currentDrive = parseInt(drive, 10); + if (event.metaKey) { + openLoadHTTP(drive); + } else { + if (disk_cur_cat[drive]) { + document.querySelector('#category_select').value = disk_cur_cat[drive]; + selectCategory(); + } + MicroModal.show('load-modal'); + } +} + +export function openSave(drive, event) { + _currentDrive = parseInt(drive, 10); + + var mimeType = 'application/octet-stream'; + var data = _disk2.getBinary(drive); + var a = document.querySelector('#local_save_link'); + + var blob = new Blob([data], { 'type': mimeType }); + a.href = window.URL.createObjectURL(blob); + a.download = driveLights.label(drive) + '.dsk'; + + if (event.metaKey) { + dumpDisk(drive); + } else { + document.querySelector('#save_name').value = driveLights.label(drive); + MicroModal.show('save-modal'); + } +} + +export function handleDragOver(drive, event) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; +} + +export function handleDragEnd(drive, event) { + var dt = event.dataTransfer; + if (dt.items) { + for (var i = 0; i < dt.items.length; i++) { + dt.items.remove(i); + } + } else { + event.dataTransfer.clearData(); + } +} + +export function handleDrop(drive, event) { + event.preventDefault(); + event.stopPropagation(); + + if (drive < 1) { + if (!_disk2.getMetadata(1)) { + drive = 1; + } else if (!_disk2.getMetadata(2)) { + drive = 2; + } else { + drive = 1; + } + } + var dt = event.dataTransfer; + if (dt.files.length == 1) { + + doLoadLocal(drive, dt.files[0]); + } else if (dt.files.length == 2) { + doLoadLocal(1, dt.files[0]); + doLoadLocal(2, dt.files[1]); + } else { + for (var idx = 0; idx < dt.items.length; idx++) { + if (dt.items[idx].type === 'text/uri-list') { + dt.items[idx].getAsString(function(url) { + var parts = document.location.hash.split('|'); + parts[drive - 1] = url; + document.location.hash = parts.join('|'); + }); + } + } + } +} + +export function loadAjax(drive, url) { + MicroModal.show('loading-modal'); + + fetch(url).then(function(response) { + if (response.ok) { + return response.json(); + } else { + throw new Error('Error loading: ' + response.statusText); + } + }).then(function(data) { + if (data.type == 'binary') { + loadBinary(drive, data); + } else if (DISK_TYPES.indexOf(data.type) > -1) { + loadDisk(drive, data); + } + initGamepad(data.gamepad); + MicroModal.close('loading-modal'); + }).catch(function(error) { + window.alert(error.message); + MicroModal.close('loading-modal'); + }); +} + +export function doLoad() { + MicroModal.close('load-modal'); + var urls = document.querySelector('#disk_select').value, url; + if (urls && urls.length) { + if (typeof(urls) == 'string') { + url = urls; + } else { + url = urls[0]; + } + } + + var files = document.querySelector('#local_file').files; + if (files.length == 1) { + doLoadLocal(_currentDrive, files[0]); + } else if (url) { + var filename; + MicroModal.close('load-modal'); + if (url.substr(0,6) == 'local:') { + filename = url.substr(6); + if (filename == '__manage') { + openManage(); + } else { + loadLocalStorage(_currentDrive, filename); + } + } else { + var r1 = /json\/disks\/(.*).json$/.exec(url); + if (r1) { + filename = r1[1]; + } else { + filename = url; + } + var parts = document.location.hash.split('|'); + parts[_currentDrive - 1] = filename; + document.location.hash = parts.join('|'); + } + } +} + +export function doSave() { + var name = document.querySelector('#save_name').value; + saveLocalStorage(_currentDrive, name); + MicroModal.close('save-modal'); +} + +export function doDelete(name) { + if (window.confirm('Delete ' + name + '?')) { + deleteLocalStorage(name); + } +} + +function doLoadLocal(drive, file) { + var parts = file.name.split('.'); + var ext = parts[parts.length - 1].toLowerCase(); + if (DISK_TYPES.indexOf(ext) > -1) { + doLoadLocalDisk(drive, file); + } else if (TAPE_TYPES.indexOf(ext) > -1) { + tape.doLoadLocalTape(file); + } else { + window.alert('Unknown file type: ' + ext); + } +} + +function doLoadLocalDisk(drive, file) { + MicroModal.show('loading-modal'); + var fileReader = new FileReader(); + fileReader.onload = function() { + var parts = file.name.split('.'); + var ext = parts.pop().toLowerCase(); + var name = parts.join('.'); + if (_disk2.setBinary(drive, name, ext, this.result)) { + driveLights.label(drive, name); + MicroModal.close('loading-modal'); + initGamepad(); + } + }; + fileReader.readAsArrayBuffer(file); +} + +export function doLoadHTTP(drive, _url) { + var url = _url || document.querySelector('#http_url').value; + if (url) { + fetch(url).then(function(response) { + if (response.ok) { + return response.arrayBuffer(); + } else { + throw new Error('Error loading: ' + response.statusText); + } + }).then(function(data) { + var urlParts = url.split('/'); + var file = urlParts.pop(); + var fileParts = file.split('.'); + var ext = fileParts.pop().toLowerCase(); + var name = decodeURIComponent(fileParts.join('.')); + if (_disk2.setBinary(drive, name, ext, data)) { + driveLights.label(drive, name); + initGamepad(); + } + if (!_url) { MicroModal.close('http-modal'); } + }).catch(function(error) { + window.alert(error.message); + if (!_url) { MicroModal.close('http-modal'); } + }); + } +} + +function openLoadHTTP(drive) { + _currentDrive = parseInt(drive, 10); + MicroModal.show('http-modal'); +} + +function openManage() { + MicroModal.show('manage-modal'); +} + +var prefs = new Prefs(); +var showFPS = false; + +export function updateKHz() { + var now = Date.now(); + var ms = now - startTime; + var cycles = cpu.cycles(); + var delta; + + if (showFPS) { + delta = stats.renderedFrames - lastFrames; + var fps = parseInt(delta/(ms/1000), 10); + document.querySelector('#khz').innerText = fps + 'fps'; + } else { + delta = cycles - lastCycles; + var khz = parseInt(delta/ms); + document.querySelector('#khz').innerText = khz + 'KHz'; + } + + startTime = now; + lastCycles = cycles; + lastFrames = stats.renderedFrames; +} + +export function toggleShowFPS() { + showFPS = !showFPS; +} + +export function updateSound() { + var on = document.querySelector('#enable_sound').checked; + var label = document.querySelector('#toggle-sound i'); + audio.enable(on); + if (on) { + label.classList.remove('fa-volume-off'); + label.classList.add('fa-volume-up'); + } else { + label.classList.remove('fa-volume-up'); + label.classList.add('fa-volume-off'); + } +} + +function dumpDisk(drive) { + var wind = window.open('', '_blank'); + wind.document.title = driveLights.label(drive); + wind.document.write('
');
+    wind.document.write(_disk2.getJSON(drive, true));
+    wind.document.write('
'); + wind.document.close(); +} + +export function reset() { + _apple2.reset(); +} + +function loadBinary(bin) { + for (var idx = 0; idx < bin.length; idx++) { + var pos = bin.start + idx; + cpu.write(pos >> 8, pos & 0xff, bin.data[idx]); + } + cpu.reset(); + cpu.setPC(bin.start); +} + +export function selectCategory() { + document.querySelector('#disk_select').innerHTML = ''; + var cat = disk_categories[document.querySelector('#category_select').value]; + 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 = document.createElement('option'); + option.value = file.filename; + option.innerText = name; + document.querySelector('#disk_select').append(option); + if (disk_cur_name[_currentDrive] == name) { + option.selected = true; + } + } + } +} + +export function selectDisk() { + document.querySelector('#local_file').value = ''; +} + +export function clickDisk() { + doLoad(); +} + +function loadDisk(drive, disk) { + var name = disk.name; + var category = disk.category; + + if (disk.disk) { + name += ' - ' + disk.disk; + } + + disk_cur_cat[drive] = category; + disk_cur_name[drive] = name; + + driveLights.label(drive, name); + _disk2.setDisk(drive, disk); + initGamepad(disk.gamepad); +} + +/* + * 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'] = []; + document.querySelector('#manage-modal-content').innerHTML = ''; + + names.forEach(function(name) { + cat.push({ + 'category': 'Local Saves', + 'name': name, + 'filename': 'local:' + name + }); + document.querySelector('#manage-modal-content').innerHTML = + '' + + name + + ' Delete
'; + }); + 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); + } +} + +if (window.localStorage !== undefined) { + document.querySelectorAll('.disksave').forEach(function (el) { el.style.display = 'inline-block';}); +} + +var oldcat = ''; +var option; +for (var idx = 0; idx < window.disk_index.length; idx++) { + var file = window.disk_index[idx]; + var cat = file.category; + var name = file.name, disk = file.disk; + if (file.e) { + continue; + } + if (cat != oldcat) { + option = document.createElement('option'); + option.value = cat; + option.innerText = cat; + document.querySelector('#category_select').append(option); + + 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 = document.createElement('option'); +option.innerText = 'Local Saves'; +document.querySelector('#category_select').append(option); + +updateLocalStorage(); + +function processHash(hash) { + var files = hash.split('|'); + for (var idx = 0; idx < files.length; idx++) { + var file = files[idx]; + if (file.indexOf('://') > 0) { + var parts = file.split('.'); + var ext = parts[parts.length - 1].toLowerCase(); + if (ext == 'json') { + loadAjax(idx + 1, file); + } else { + doLoadHTTP(idx + 1, file); + } + } else { + loadAjax(idx + 1, 'json/disks/' + file + '.json'); + } + } +} + +/* + * Keyboard/Gamepad routines + */ + +function _keydown(evt) { + if (!focused && (!evt.metaKey || evt.ctrlKey)) { + evt.preventDefault(); + + var key = keyboard.mapKeyEvent(evt); + if (key != 0xff) { + io.keyDown(key); + } + } + if (evt.keyCode === 112) { // F1 - Reset + cpu.reset(); + evt.preventDefault(); // prevent launching help + } else if (evt.keyCode === 113) { // F2 - Full Screen + var elem = document.getElementById('screen'); + if (evt.shiftKey) { // Full window, but not full screen + document.querySelector('#display').classList.toggle('zoomwindow'); + document.querySelector('#display > div').classList.toggle('overscan'); + document.querySelector('#display > div').classList.toggle('flexbox-centering'); + document.querySelector('#screen').classList.toggle('maxhw'); + document.querySelector('#header').classList.toggle('hidden'); + document.querySelectorAll('.inset').forEach(function(el) { el.classList.toggle('hidden'); }); + document.querySelector('#reset').classList.toggle('hidden'); + } else 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 === 114) { // F3 + io.keyDown(0x1b); + } else if (evt.keyCode === 117) { // F6 Quick Save + _apple2.saveState(); + } else if (evt.keyCode === 120) { // F9 Quick Restore + _apple2.restoreState(); + } else if (evt.keyCode == 16) { // Shift + keyboard.shiftKey(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 + if (evt.location == 1) { + keyboard.commandKey(true); + } else { + keyboard.optionKey(true); + } + } +} + +function _keyup(evt) { + if (!focused) + io.keyUp(); + + if (evt.keyCode == 16) { // Shift + keyboard.shiftKey(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 + if (evt.location == 1) { + keyboard.commandKey(false); + } else { + keyboard.optionKey(false); + } + } +} + +export function updateScreen() { + var green = document.querySelector('#green_screen').checked; + var scanlines = document.querySelector('#show_scanlines').checked; + + vm.green(green); + vm.scanlines(scanlines); +} + +export function updateCPU() { + var accelerated = document.querySelector('#accelerator_toggle').checked; + kHz = accelerated ? 4092 : 1023; + io.updateHz(kHz * 1000); +} + +export function updateUI() { + if (document.location.hash != hashtag) { + hashtag = document.location.hash; + var hash = hup(); + if (hash) { + processHash(hash); + } + } +} + +var disableMouseJoystick = false; +var flipX = false; +var flipY = false; +var swapXY = false; + +export function updateJoystick() { + disableMouseJoystick = document.querySelector('#disable_mouse').checked; + flipX = document.querySelector('#flip_x').checked; + flipY = document.querySelector('#flip_y').checked; + swapXY = document.querySelector('#swap_x_y').checked; + configGamepad(flipX, flipY); + + if (disableMouseJoystick) { + io.paddle(0, 0.5); + io.paddle(1, 0.5); + return; + } +} + +function _mousemove(evt) { + if (gamepad || disableMouseJoystick) { + return; + } + + var s = document.querySelector('#screen'); + var offset = s.getBoundingClientRect(); + var x = (evt.pageX - offset.left) / s.clientWidth, + y = (evt.pageY - offset.top) / s.clientHeight, + 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; + +export function pauseRun() { + var label = document.querySelector('#pause-run i'); + if (paused) { + cpu.run(); + label.classList.remove('fa-play'); + label.classList.add('fa-pause'); + } else { + cpu.stop(); + label.classList.remove('fa-pause'); + label.classList.add('fa-play'); + } + paused = !paused; +} + +export function toggleSound() { + var enableSound = document.querySelector('#enable_sound'); + enableSound.checked = !enableSound.checked; + updateSound(); +} + +export function openOptions() { + MicroModal.show('options-modal'); +} + +export function openPrinterModal() { + MicroModal.show('printer-modal'); +} + +export function initUI(apple2, disk2, e) { + _apple2 = apple2; + cpu = _apple2.getCPU(); + io = _apple2.getIO(); + stats = apple2.getStats(); + vm = apple2.getVideoModes(); + tape = new Tape(io); + _disk2 = disk2; + + keyboard = new KeyBoard(cpu, io, e); + keyboard.create('#keyboard'); + audio = new Audio(io); + + MicroModal.init(); + + /* + * Input Handling + */ + + window.addEventListener('keydown', _keydown); + window.addEventListener('keyup', _keyup); + window.addEventListener('mousedown', function() { audio.autoStart(); }); + + document.querySelectorAll('canvas').forEach(function(canvas) { + canvas.addEventListener('mousedown', function(evt) { + if (!gamepad) { + io.buttonDown(evt.which == 1 ? 0 : 1); + } + evt.preventDefault(); + }); + canvas.addEventListener('mouseup', function(evt) { + if (!gamepad) { + io.buttonUp(evt.which == 1 ? 0 : 1); + } + }); + }); + + document.body.addEventListener('mousemove', _mousemove); + + document.querySelectorAll('input,textarea').forEach(function(input) { + input.addEventListener('input', function() { focused = true; }); + input.addEventListener('blur', function() { focused = false; }); + }); + + if (prefs.havePrefs()) { + document.querySelectorAll('#options-modal input[type=checkbox]').forEach(function(el) { + var val = prefs.readPref(el.id); + if (val) { + el.checked = JSON.parse(val); + } + el.addEventListener('change', function() { + prefs.writePref(el.id, JSON.stringify(el.checked)); + }); + }); + document.querySelectorAll('#options-modal select').forEach(function(el) { + var val = prefs.readPref(el.id); + if (val) { + el.value = val; + } + el.addEventListener('change', function() { + prefs.writePref(el.id, el.value); + }); + }); + } + + cpu.reset(); + setInterval(updateKHz, 1000); + updateSound(); + updateScreen(); + updateCPU(); + initGamepad(); + + // Check for disks in hashtag + + var hash = gup('disk') || hup(); + if (hash) { + processHash(hash); + } + + if (navigator.standalone) { + document.body.classList.add('standalone'); + } + + _apple2.run(); +} diff --git a/js/ui/printer.js b/js/ui/printer.js index 908d81b..2dc2023 100644 --- a/js/ui/printer.js +++ b/js/ui/printer.js @@ -30,18 +30,16 @@ export default function Printer(el) { var visible = val >= 0x20; var c = String.fromCharCode(ascii); - if (c == '\r') { + if (c === '\r') { newLine(); - } else if (c == '\n') { + } else if (c === '\n') { // eat for now - } else if (c == '\t') { + } else if (c === '\t') { _lineBuffer += ' '; - } else if (c == 0x04) { + } else if (ascii === 0x04) { _lineBuffer = _lineBuffer.slice(0, -1); - } else { - if (visible) { - _lineBuffer += c; - } + } else if (visible) { + _lineBuffer += c; } _line.innerText = _lineBuffer; },