diff --git a/js/apple1.js b/js/apple1.js deleted file mode 100644 index 649ffc1..0000000 --- a/js/apple1.js +++ /dev/null @@ -1,498 +0,0 @@ -import MicroModal from 'micromodal'; - -import Apple1IO from './apple1io'; -import CPU6502 from './cpu6502'; -import Prefs from './prefs'; -import RAM from './ram'; -import { TextPage } from './canvas1'; -import { debug, hup } from './util'; - -import Basic from './roms/basic'; -import Bios from './roms/bios'; -import Krusader from './roms/krusader'; - -import ACI from './cards/aci'; - -import { mapKeyEvent, KeyBoard } from './ui/keyboard'; - -var DEBUG=false; -var TRACE=true; -var skidmarks = []; - -var focused = false; -var startTime = Date.now(); -var lastCycles = 0; -var renderedFrames = 0, lastFrames = 0; -var paused = false; - -var hashtag; -var prefs = new Prefs(); -var runTimer = null; -var cpu = new CPU6502(); - -var krusader = window.location.hash == '#krusader'; - -var raml, ramh, rom, aci, io, text, keyboard; - -// 32K base memory. Should be 0x0f for 4K, 0x1f for 8K, 0x3f for 16K -raml = new RAM(0x00, 0x7f); -text = new TextPage(); -text.init(); - -aci = new ACI(cpu, { progress: function(val) { - document.querySelector('#tape').style.width = val * 100 + 'px'; -}}); -io = new Apple1IO(text); - -if (krusader) { - ramh = null; - rom = new Krusader(); -} else { - // ramh = new RAM(0xe0, 0xef); // 4K ACI memory. - ramh = new Basic(); - rom = new Bios(); -} -keyboard = new KeyBoard('#keyboard', cpu, io, text); - -cpu.addPageHandler(raml); -if (ramh) { - cpu.addPageHandler(ramh); -} -cpu.addPageHandler(rom); - -cpu.addPageHandler(aci); -cpu.addPageHandler(io); - -var showFPS = false; - -//aci.setData([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]) -//aci.setData([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]) -//aci.setData([0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef]) - -//aci.setData(tapes['BASIC']); -aci.setData(window.tapes['Microchess'].tracks); - -// Audio Buffer Source -var context; -if (typeof window.webkitAudioContext !== 'undefined') { - context = new window.webkitAudioContext(); -} else if (typeof window.AudioContext !== 'undefined') { - context = new window.AudioContext(); -} - -export function doLoadLocal(files) { - context.resume(); - files = files || document.querySelector('#local_file').files; - if (files.length == 1) { - var file = files[0]; - var fileReader = new FileReader(); - fileReader.onload = function(ev) { - context.decodeAudioData( - ev.target.result, - function(buffer) { - var buf = []; - var data = buffer.getChannelData(0); - var old = (data[0] > 0.25); - var last = 0; - for (var idx = 1; idx < data.length; idx++) { - var current = (data[idx] > 0.25); - if (current != old) { - var delta = idx - last; - buf.push(parseInt(delta / buffer.sampleRate * 1023000)); - old = current; - last = idx; - } - } - aci.buffer = buf; - MicroModal.close('local-modal'); - }, - function() { - window.alert('Unable to read tape file: ' + file.name); - } - ); - }; - fileReader.readAsArrayBuffer(file); - } -} - -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)); - document.querySelector('#khz').innerHTML = fps + 'fps'; - } else { - delta = cycles - lastCycles; - var khz = parseInt(delta/ms); - document.querySelector('#khz').innerHTML = khz + 'KHz'; - } - - startTime = now; - lastCycles = cycles; - lastFrames = renderedFrames; -} - -var loading = false; -var throttling = true; -var turbotape = false; - -export function toggleFPS() { - showFPS = !showFPS; -} - -export function toggleSpeed() -{ - throttling = document.querySelector('#speed_toggle').checked; - if (runTimer) { - run(); - } -} - -export function setKeyBuffer(text) { - io.paste(text); -} - -export function setTurboTape(val) { - turbotape = val; -} - -var _requestAnimationFrame = - window.requestAnimationFrame || - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || - window.msRequestAnimationFrame; - -function run(pc) { - if (runTimer) { - clearInterval(runTimer); - } - - if (pc) { - cpu.setPC(pc); - } - - var ival = 30, step = 1023 * ival, stepMax = step; - - if (!throttling) { - ival = 1; - } - - var now, last = Date.now(); - var runFn = function() { - now = Date.now(); - renderedFrames++; - if (_requestAnimationFrame) { - step = (now - last) * 1023; - last = now; - if (step > stepMax) { - step = stepMax; - } - } - if (document.location.hash != hashtag) { - hashtag = document.location.hash; - } - if (!loading) { - if (DEBUG) { - cpu.stepCyclesDebug(TRACE ? 1 : step, function() { - var line = cpu.dumpRegisters() + ' ' + cpu.dumpPC(); - if (TRACE) { - debug(line); - } else { - skidmarks.push(); - if (skidmarks.length > 256) { - skidmarks.shift(); - } - } - }); - } else { - cpu.stepCycles(step); - } - text.blit(); - } - 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(); -} - -export 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); -} - -var _key; -function _keydown(evt) { - if (evt.keyCode === 112) { - cpu.reset(); - } else if (evt.keyCode === 113) { - if (document.webkitIsFullScreen) { - document.webkitCancelFullScreen(); - } else { - var elem = document.getElementById('display'); - elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); - } - } else if (evt.key === 'Shift') { - keyboard.shiftKey(true); - } else if (evt.key == 'Control') { - keyboard.controlKey(true); - } else if (!focused && (!evt.metaKey || evt.ctrlKey)) { - evt.preventDefault(); - - var key = mapKeyEvent(evt); - if (key != 0xff) { - if (_key != 0xff) io.keyUp(); - io.keyDown(key); - _key = key; - } - } -} - -function _keyup(evt) { - _key = 0xff; - - if (evt.key === 'Shift') { - keyboard.shiftKey(false); - } else if (evt.key === 'Control') { - keyboard.controlKey(false); - } else { - if (!focused) { - io.keyUp(); - } - } -} - -var _updateScreenTimer = null; - -export function updateScreen() { - var green = document.querySelector('#green_screen').checked; - var scanlines = document.querySelector('#show_scanlines').checked; - - text.green(green); - text.scanlines(scanlines); - - if (!_updateScreenTimer) - _updateScreenTimer = - setInterval(function() { - text.refresh(); - clearInterval(_updateScreenTimer); - _updateScreenTimer = null; - }, 100); -} - -paused = false; -export function pauseRun(b) { - if (paused) { - run(); - b.value = 'Pause'; - } else { - stop(); - b.value = 'Run'; - } - paused = !paused; -} - -export function openOptions() { - MicroModal.show('options-modal'); -} - -export function openLoadText(event) { - if (event && event.altKey) { - MicroModal.show('local-modal'); - } else { - MicroModal.show('input-modal'); - } -} - -export function doLoadText() { - var text = document.querySelector('#text_input').value; - if (!text.indexOf('//Binary')) { - var lines = text.split('\n'); - lines.forEach(function(line) { - var parts = line.split(': '); - if (parts.length == 2) { - var addr; - if (parts[0].length > 0) { - addr = parseInt(parts[0], 16); - } - var data = parts[1].split(' '); - for (var idx = 0; idx < data.length; idx++) { - cpu.write(addr >> 8, addr & 0xff, parseInt(data[idx], 16)); - addr++; - } - } - }); - } else { - io.paste(text); - } - MicroModal.close('input-modal'); -} - -export function handleDragOver(event) { - event.preventDefault(); - event.dataTransfer.dropEffect = 'copy'; -} - -export function handleDrop(event) { - event.preventDefault(); - event.stopPropagation(); - - var dt = event.dataTransfer; - if (dt.files.length > 0) { - doLoadLocal(dt.files); - } -} - -export function handleDragEnd(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(); - } -} - -MicroModal.init(); - -document.addEventListener('DOMContentLoaded', function() { - hashtag = document.location.hash; - - /* - * Input Handling - */ - - var canvas = document.getElementById('text'); - var context = canvas.getContext('2d'); - - text.setContext(context); - - window.addEventListener('keydown', _keydown); - window.addEventListener('keyup', _keyup); - - window.addEventListener('paste', (event) => { - var paste = (event.clipboardData || window.clipboardData).getData('text'); - setKeyBuffer(paste); - event.preventDefault(); - }); - - window.addEventListener('copy', (event) => { - event.clipboardData.setData('text/plain', text.getText()); - event.preventDefault(); - }); - - document.querySelector('.overscan').addEventListener('paste', function(event) { - io.paste(event.originalEvent.clipboardData().getData('text/plain')); - event.preventDefault(); - }); - - document.querySelectorAll('input,textarea').forEach(function(el) { - el.addEventListener('focus', function() { focused = true; }); - }); - document.querySelectorAll('input,textarea').forEach(function(el) { - el.addEventListener('blur', function() { focused = false; }); - }); - keyboard.create(); - - if (prefs.havePrefs()) { - document.querySelectorAll('input[type=checkbox]').forEach(function(el) { - var val = prefs.readPref(el.id); - if (val != null) - el.checked = JSON.parse(val); - }); - document.querySelectorAll('input[type=checkbox]').forEach(function(el) { - el.addEventListener('change', function() { - prefs.writePref(el.id, JSON.stringify(el.checked)); - }); - }); - } - - turbotape = document.querySelector('#turbo_tape').checked; - - Object.keys(window.tapes).sort().forEach(function(key) { - var option = document.createElement('option'); - option.value = key; - option.text = key; - document.querySelector('#tape_select').append(option); - }); - - function doTapeSelect() { - var tapeId = document.querySelector('#tape_select').value; - var tape = window.tapes[tapeId]; - if (!tape) { - document.querySelector('#text_input').value = ''; - return; - } - debug('Loading', tapeId); - - window.location.hash = tapeId; - reset(); - if (turbotape) { - var trackIdx = 0, script = ''; - var parts = tape.script.split('\n'); - // Ignore part 0 (C100R) - // Split part 1 into ranges - var ranges = parts[1].split(' '); - var idx; - for (idx = 0; idx < ranges.length; idx++) { - var range = ranges[idx].split('.'); - var start = parseInt(range[0], 16); - var end = parseInt(range[1], 16); - var track = tape.tracks[trackIdx]; - var kdx = 0; - for (var jdx = start; jdx <= end; jdx++) { - cpu.write(jdx >> 8, jdx & 0xff, track[kdx++]); - } - trackIdx++; - } - // Execute parts 2-n - for (idx = 2; idx < parts.length; idx++) { - if (parts[idx]) { - script += parts[idx] + '\n'; - } - } - document.querySelector('#text_input').value = script; - } else { - aci.setData(tape.tracks); - document.querySelector('#text_input').value = tape.script; - } - doLoadText(); - } - document.querySelector('#tape_select').addEventListener('change', doTapeSelect); - - run(); - setInterval(updateKHz, 1000); - updateScreen(); - - var tape = hup(); - if (tape) { - openLoadText(); - document.querySelector('#tape_select').value = tape; - doTapeSelect(); - } -}); diff --git a/js/apple1.ts b/js/apple1.ts new file mode 100644 index 0000000..437a270 --- /dev/null +++ b/js/apple1.ts @@ -0,0 +1,520 @@ +import MicroModal from "micromodal"; + +import Apple1IO from "./apple1io"; +import CPU6502 from "./cpu6502"; +import Prefs from "./prefs"; +import RAM from "./ram"; +import { TextPage } from "./canvas1"; +import { debug, hup } from "./util"; + +import Basic from "./roms/basic"; +import Bios from "./roms/bios"; +import Krusader from "./roms/krusader"; + +import ACI from "./cards/aci"; + +import { mapKeyEvent, KeyBoard } from "./ui/keyboard"; +import { address, byte } from "./types"; + +let DEBUG = false; +let TRACE = true; +const skidmarks: string[] = []; + +let focused = false; +let startTime = Date.now(); +let lastCycles = 0; +let renderedFrames = 0, + lastFrames = 0; +let paused = false; + +let hashtag: string | undefined; +const prefs = new Prefs(); +let runTimer: ReturnType | null = null; +const cpu = new CPU6502(); + +var krusader = window.location.hash == "#krusader"; + +var raml, ramh, rom, aci: ACI, io: Apple1IO, text: TextPage, keyboard: KeyBoard; + +// 32K base memory. Should be 0x0f for 4K, 0x1f for 8K, 0x3f for 16K +raml = new RAM(0x00, 0x7f); +text = new TextPage(); +text.init(); + +aci = new ACI(cpu, { + progress: function (val) { + document.querySelector("#tape")!.style.width = + val * 100 + "px"; + }, +}); +io = new Apple1IO(text); + +if (krusader) { + ramh = null; + rom = new Krusader(); +} else { + // ramh = new RAM(0xe0, 0xef); // 4K ACI memory. + ramh = new Basic(); + rom = new Bios(); +} +keyboard = new KeyBoard("#keyboard", cpu, io, text); + +cpu.addPageHandler(raml); +if (ramh) { + cpu.addPageHandler(ramh); +} +cpu.addPageHandler(rom); + +cpu.addPageHandler(aci); +cpu.addPageHandler(io); + +var showFPS = false; + +interface Tape { + script: string; + tracks: number[][]; +} + +declare global { + interface Window { + tapes: Record; + } +} + +//aci.setData([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]) +//aci.setData([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]) +//aci.setData([0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef]) + +//aci.setData(tapes['BASIC']); +aci.setData(window.tapes["Microchess"].tracks); + +// Audio Buffer Source +declare global { + interface Window { + webkitAudioContext: AudioContext; + } +} + +const AudioContext = window.AudioContext || window.webkitAudioContext; +const context = new AudioContext(); + +export function doLoadLocal(files: FileList) { + context.resume(); + files = + files || document.querySelector("#local_file")!.files; + if (files.length == 1) { + var file = files[0]; + var fileReader = new FileReader(); + fileReader.onload = function (ev) { + context.decodeAudioData( + ev.target!.result as ArrayBuffer, + function (buffer) { + var buf = []; + var data = buffer.getChannelData(0); + var old = data[0] > 0.25; + var last = 0; + for (var idx = 1; idx < data.length; idx++) { + var current = data[idx] > 0.25; + if (current != old) { + var delta = idx - last; + buf.push(Math.floor((delta / buffer.sampleRate) * 1023000)); + old = current; + last = idx; + } + } + aci.buffer = buf; + MicroModal.close("local-modal"); + }, + function () { + window.alert("Unable to read tape file: " + file.name); + } + ); + }; + fileReader.readAsArrayBuffer(file); + } +} + +function updateKHz() { + let now = Date.now(); + let ms = now - startTime; + let cycles = cpu.getCycles(); + let delta: number; + + if (showFPS) { + delta = renderedFrames - lastFrames; + var fps = Math.floor(delta / (ms / 1000)); + document.querySelector("#khz")!.innerHTML = fps + "fps"; + } else { + delta = cycles - lastCycles; + var khz = Math.floor(delta / ms); + document.querySelector("#khz")!.innerHTML = khz + "KHz"; + } + + startTime = now; + lastCycles = cycles; + lastFrames = renderedFrames; +} + +var loading = false; +var throttling = true; +var turbotape = false; + +export function toggleFPS() { + showFPS = !showFPS; +} + +export function toggleSpeed() { + throttling = + document.querySelector("#speed_toggle")!.checked; + if (runTimer) { + run(); + } +} + +export function setKeyBuffer(text: string) { + io.paste(text); +} + +export function setTurboTape(val: boolean) { + turbotape = val; +} + +function run(pc?: address) { + if (runTimer) { + clearInterval(runTimer); + } + + if (pc) { + cpu.setPC(pc); + } + + var ival = 30, + step = 1023 * ival, + stepMax = step; + + if (!throttling) { + ival = 1; + } + + var now, + last = Date.now(); + var runFn = function () { + now = Date.now(); + renderedFrames++; + step = (now - last) * 1023; + last = now; + if (step > stepMax) { + step = stepMax; + } + if (document.location.hash != hashtag) { + hashtag = document.location.hash; + } + if (!loading) { + if (DEBUG) { + cpu.stepCyclesDebug(TRACE ? 1 : step, function () { + var line = JSON.stringify(cpu.getState()); + if (TRACE) { + debug(line); + } else { + skidmarks.push(line); + if (skidmarks.length > 256) { + skidmarks.shift(); + } + } + }); + } else { + cpu.stepCycles(step); + } + text.blit(); + } + if (!paused) { + requestAnimationFrame(runFn); + } + }; + requestAnimationFrame(runFn); +} + +function stop() { + if (runTimer) { + clearInterval(runTimer); + } + runTimer = null; +} + +function reset() { + cpu.reset(); +} + +declare global { + interface Document { + webkitCancelFullScreen: () => void; + webkitIsFullScreen: boolean; + } + interface Element { + webkitRequestFullScreen: (options?: unknown) => void; + } +} + +var _key: byte; +function _keydown(evt: KeyboardEvent) { + if (evt.keyCode === 112) { + cpu.reset(); + } else if (evt.keyCode === 113) { + if (document.webkitIsFullScreen) { + document.webkitCancelFullScreen(); + } else { + var elem = document.getElementById("display"); + elem!.webkitRequestFullScreen(); + } + } else if (evt.key === "Shift") { + keyboard.shiftKey(true); + } else if (evt.key == "Control") { + keyboard.controlKey(true); + } else if (!focused && (!evt.metaKey || evt.ctrlKey)) { + evt.preventDefault(); + + var key = mapKeyEvent(evt); + if (key != 0xff) { + if (_key != 0xff) io.keyUp(); + io.keyDown(key); + _key = key; + } + } +} + +function _keyup(evt: KeyboardEvent) { + _key = 0xff; + + if (evt.key === "Shift") { + keyboard.shiftKey(false); + } else if (evt.key === "Control") { + keyboard.controlKey(false); + } else { + if (!focused) { + io.keyUp(); + } + } +} + +let _updateScreenTimer: ReturnType | null = null; + +export function updateScreen() { + const green = + document.querySelector("#green_screen")!.checked; + const scanlines = + document.querySelector("#show_scanlines")!.checked; + + text.green(green); + text.scanlines(scanlines); + + if (!_updateScreenTimer) + _updateScreenTimer = setInterval(function () { + text.refresh(); + if (_updateScreenTimer) { + clearInterval(_updateScreenTimer); + } + _updateScreenTimer = null; + }, 100); +} + +paused = false; +export function pauseRun(b: HTMLButtonElement) { + if (paused) { + run(); + b.value = "Pause"; + } else { + stop(); + b.value = "Run"; + } + paused = !paused; +} + +export function openOptions() { + MicroModal.show("options-modal"); +} + +export function openLoadText(event?: MouseEvent) { + if (event && event.altKey) { + MicroModal.show("local-modal"); + } else { + MicroModal.show("input-modal"); + } +} + +export function doLoadText() { + var text = document.querySelector("#text_input")!.value; + if (!text.indexOf("//Binary")) { + var lines = text.split("\n"); + lines.forEach(function (line) { + var parts = line.split(": "); + if (parts.length == 2) { + let addr: address = 0; + if (parts[0].length > 0) { + addr = parseInt(parts[0], 16); + } + var data = parts[1].split(" "); + for (var idx = 0; idx < data.length; idx++) { + cpu.write(addr >> 8, addr & 0xff, parseInt(data[idx], 16)); + addr++; + } + } + }); + } else { + io.paste(text); + } + MicroModal.close("input-modal"); +} + +export function handleDragOver(event: DragEvent) { + event.preventDefault(); + event.dataTransfer!.dropEffect = "copy"; +} + +export function handleDrop(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + + var dt = event.dataTransfer; + if (dt?.files && dt.files.length > 0) { + doLoadLocal(dt.files); + } +} + +export function handleDragEnd(event: DragEvent) { + var dt = event.dataTransfer; + if (dt?.items) { + for (var i = 0; i < dt.items.length; i++) { + dt.items.remove(i); + } + } else { + event.dataTransfer?.clearData(); + } +} + +MicroModal.init(); + +document.addEventListener("DOMContentLoaded", function () { + hashtag = document.location.hash; + + /* + * Input Handling + */ + + const canvas = document.querySelector("#text")!; + const context = canvas.getContext("2d")!; + + text.setContext(context); + + window.addEventListener("keydown", _keydown); + window.addEventListener("keyup", _keyup); + + window.addEventListener("paste", (event) => { + var paste = event.clipboardData!.getData("text/plain"); + setKeyBuffer(paste); + event.preventDefault(); + }); + + window.addEventListener("copy", (event) => { + event.clipboardData?.setData("text/plain", text.getText()); + event.preventDefault(); + }); + + document.querySelectorAll("input,textarea").forEach(function (el) { + el.addEventListener("focus", function () { + focused = true; + }); + }); + document.querySelectorAll("input,textarea").forEach(function (el) { + el.addEventListener("blur", function () { + focused = false; + }); + }); + keyboard.create(); + + if (prefs.havePrefs()) { + document + .querySelectorAll("input[type=checkbox]") + .forEach(function (el) { + var val = prefs.readPref(el.id); + if (val != null) el.checked = JSON.parse(val); + }); + document + .querySelectorAll("input[type=checkbox]") + .forEach(function (el) { + el.addEventListener("change", function () { + prefs.writePref(el.id, JSON.stringify(el.checked)); + }); + }); + } + + turbotape = document.querySelector("#turbo_tape")!.checked; + + Object.keys(window.tapes) + .sort() + .forEach(function (key) { + var option = document.createElement("option"); + option.value = key; + option.text = key; + document.querySelector("#tape_select")!.append(option); + }); + + function doTapeSelect() { + var tapeId = + document.querySelector("#tape_select")!.value; + var tape = window.tapes[tapeId]; + if (!tape) { + document.querySelector("#text_input")!.value = ""; + return; + } + debug("Loading", tapeId); + + window.location.hash = tapeId; + reset(); + if (turbotape) { + var trackIdx = 0, + script = ""; + var parts = tape.script.split("\n"); + // Ignore part 0 (C100R) + // Split part 1 into ranges + var ranges = parts[1].split(" "); + var idx; + for (idx = 0; idx < ranges.length; idx++) { + var range = ranges[idx].split("."); + var start = parseInt(range[0], 16); + var end = parseInt(range[1], 16); + var track = tape.tracks[trackIdx]; + var kdx = 0; + for (var jdx = start; jdx <= end; jdx++) { + cpu.write(jdx >> 8, jdx & 0xff, track[kdx++]); + } + trackIdx++; + } + // Execute parts 2-n + for (idx = 2; idx < parts.length; idx++) { + if (parts[idx]) { + script += parts[idx] + "\n"; + } + } + document.querySelector("#text_input")!.value = script; + } else { + aci.setData(tape.tracks); + document.querySelector("#text_input")!.value = + tape.script; + } + doLoadText(); + } + document + .querySelector("#tape_select")! + .addEventListener("change", doTapeSelect); + + run(); + setInterval(updateKHz, 1000); + updateScreen(); + + var tape = hup(); + if (tape) { + openLoadText(); + document.querySelector("#tape_select")!.value = tape; + doTapeSelect(); + } +});