"use strict"; import { hex, clamp } from "./util"; // external modules declare var jt, Javatari, Z80_fast, CPU6809; // Emulator classes export var PLATFORMS = {}; export var frameUpdateFunction : (Canvas) => void = null; var _random_state = 1; export function noise() { let x = _random_state; x ^= x << 13; x ^= x >> 17; x ^= x << 5; return (_random_state = x) & 0xff; } export function getNoiseSeed() { return _random_state; } export function setNoiseSeed(x : number) { _random_state = x; } type KeyboardCallback = (which:number, charCode:number, flags:number) => void; function __createCanvas(mainElement:HTMLElement, width:number, height:number) : HTMLCanvasElement { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.classList.add("emuvideo"); canvas.tabIndex = -1; // Make it focusable mainElement.appendChild(canvas); return canvas; } function _setKeyboardEvents(canvas:HTMLElement, callback:KeyboardCallback) { canvas.onkeydown = function(e) { callback(e.which, 0, 1|_metakeyflags(e)); }; canvas.onkeyup = function(e) { callback(e.which, 0, 0|_metakeyflags(e)); }; canvas.onkeypress = function(e) { callback(e.which, e.charCode, 1|_metakeyflags(e)); }; }; type VideoCanvasOptions = {rotate?:number, overscan?:boolean}; export class RasterVideo { mainElement : HTMLElement; width : number; height : number; options : VideoCanvasOptions; constructor(mainElement:HTMLElement, width:number, height:number, options?:VideoCanvasOptions) { this.mainElement = mainElement; this.width = width; this.height = height; this.options = options; } canvas : HTMLCanvasElement; ctx; imageData; arraybuf; buf8; datau32; vcanvas; paddle_x = 255; paddle_y = 255; setRotate(rotate:number) { var canvas = this.canvas; if (rotate) { // TODO: aspect ratio? canvas.style.transform = "rotate("+rotate+"deg)"; if (canvas.width < canvas.height) canvas.style.paddingLeft = canvas.style.paddingRight = "10%"; } else { canvas.style.transform = null; canvas.style.paddingLeft = canvas.style.paddingRight = null; } } create() { var canvas; this.canvas = canvas = __createCanvas(this.mainElement, this.width, this.height); this.vcanvas = $(canvas); if (this.options && this.options.rotate) { this.setRotate(this.options.rotate); } if (this.options && this.options.overscan) { this.vcanvas.css('padding','0px'); } this.ctx = canvas.getContext('2d'); this.imageData = this.ctx.createImageData(this.width, this.height); this.datau32 = new Uint32Array(this.imageData.data.buffer); } setKeyboardEvents(callback) { _setKeyboardEvents(this.canvas, callback); } getFrameData() { return this.datau32; } getContext() { return this.ctx; } updateFrame(sx:number, sy:number, dx:number, dy:number, w?:number, h?:number) { if (w && h) this.ctx.putImageData(this.imageData, sx, sy, dx, dy, w, h); else this.ctx.putImageData(this.imageData, 0, 0); if (frameUpdateFunction) frameUpdateFunction(this.canvas); } setupMouseEvents(el? : HTMLElement) { if (!el) el = this.canvas; $(el).mousemove( (e) => { // TODO: get coords right var x = e.pageX - this.vcanvas.offset().left; var y = e.pageY - this.vcanvas.offset().top; var new_x = Math.floor(x * 255 / this.vcanvas.width() - 20); var new_y = Math.floor(y * 255 / this.vcanvas.height() - 20); this.paddle_x = clamp(0, 255, new_x); this.paddle_y = clamp(0, 255, new_y); }); }; } export class VectorVideo extends RasterVideo { persistenceAlpha = 0.5; jitter = 1.0; gamma = 0.8; sx : number; sy : number; create() { super.create(); this.sx = this.width/1024.0; this.sy = this.height/1024.0; } clear() { var ctx = this.ctx; ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = this.persistenceAlpha; ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, this.width, this.height); ctx.globalAlpha = 1.0; ctx.globalCompositeOperation = 'lighter'; if (frameUpdateFunction) frameUpdateFunction(this.canvas); } COLORS = [ '#111111', '#1111ff', '#11ff11', '#11ffff', '#ff1111', '#ff11ff', '#ffff11', '#ffffff' ]; drawLine(x1:number, y1:number, x2:number, y2:number, intensity:number, color:number) { var ctx = this.ctx; var sx = this.sx; var sy = this.sy; //console.log(x1,y1,x2,y2,intensity,color); if (intensity > 0) { // TODO: landscape vs portrait var alpha = Math.pow(intensity / 255.0, this.gamma); ctx.globalAlpha = alpha; ctx.beginPath(); // TODO: bright dots var jx = this.jitter * (Math.random() - 0.5); var jy = this.jitter * (Math.random() - 0.5); x1 += jx; x2 += jx; y1 += jy; y2 += jy; ctx.moveTo(x1*sx, this.height-y1*sy); if (x1 == x2 && y1 == y2) ctx.lineTo(x2*sx+1, this.height-y2*sy); else ctx.lineTo(x2*sx, this.height-y2*sy); ctx.strokeStyle = this.COLORS[color & 7]; ctx.stroke(); } } } export class RAM { mem : Uint8Array; constructor(size:number) { this.mem = new Uint8Array(new ArrayBuffer(size)); } } export var AnimationTimer = function(frequencyHz:number, callback:() => void) { var intervalMsec = 1000.0 / frequencyHz; var running : boolean = false; var pulsing : boolean = false; var lastts = 0; var useReqAnimFrame = false; //TODO window.requestAnimationFrame ? (frequencyHz>40) : false; var nframes, startts; // for FPS calc this.frameRate = frequencyHz; function scheduleFrame(msec:number) { if (useReqAnimFrame) window.requestAnimationFrame(nextFrame); else setTimeout(nextFrame, msec); } function nextFrame(ts:number) { if (!ts) ts = Date.now(); if (ts - lastts < intervalMsec*10) { lastts += intervalMsec; } else { lastts = ts + intervalMsec; // frames skipped, catch up } if (!useReqAnimFrame || lastts - ts > intervalMsec/2) { if (running) { callback(); } if (nframes == 0) startts = ts; if (nframes++ == 300) { console.log("Avg framerate: " + nframes*1000/(ts-startts) + " fps"); } } if (running) { scheduleFrame(lastts - ts); } else { pulsing = false; } } this.isRunning = function() { return running; } this.start = function() { if (!running) { running = true; lastts = 0; nframes = 0; if (!pulsing) { scheduleFrame(0); pulsing = true; } } } this.stop = function() { running = false; } } // TODO: move to util? export function dumpRAM(ram:Uint8Array|number[], ramofs:number, ramlen:number) : string { var s = ""; // TODO: show scrollable RAM for other platforms for (var ofs=0; ofs len) { throw Error("Data too long, " + data.length + " > " + len); } var r = new RAM(len); r.mem.set(data); return r.mem; } type AddressReadWriteFn = ((a:number) => number) | ((a:number,v:number) => void); type AddressDecoderEntry = [number, number, number, AddressReadWriteFn]; type AddressDecoderOptions = {gmask?:number}; // TODO: better performance, check values export function AddressDecoder(table : AddressDecoderEntry[], options?:AddressDecoderOptions) { var self = this; function makeFunction(lo, hi) { var s = ""; if (options && options.gmask) { s += "a&=" + options.gmask + ";"; } for (var i=0; i number { return new (AddressDecoder as any)(table, options); }