"use strict"; import { hex, clamp } from "./util"; // external modules declare var jt, Javatari, Z80_fast, CPU6809; declare var Mousetrap; // Emulator classes export var PLATFORMS = {}; 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:KeyFlags) => 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; } export enum KeyFlags { KeyDown = 1, Shift = 2, Ctrl = 4, Alt = 8, Meta = 16, KeyUp = 64, KeyPress = 128, } function _setKeyboardEvents(canvas:HTMLElement, callback:KeyboardCallback) { canvas.onkeydown = function(e) { callback(e.which, 0, KeyFlags.KeyDown|_metakeyflags(e)); }; canvas.onkeyup = function(e) { callback(e.which, 0, KeyFlags.KeyUp|_metakeyflags(e)); }; canvas.onkeypress = function(e) { callback(e.which, e.charCode, KeyFlags.KeyPress|_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 : JQuery; 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); } setupMouseEvents(el? : HTMLCanvasElement) { if (!el) el = this.canvas; $(el).mousemove( (e) => { var pos = getMousePos(el, e); var new_x = Math.floor(pos.x * 255 / this.canvas.width); var new_y = Math.floor(pos.y * 255 / this.canvas.height); 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'; } 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 class EmuHalt extends Error { } export class AnimationTimer { callback; running : boolean = false; pulsing : boolean = false; lastts = 0; useReqAnimFrame = false; //TODO window.requestAnimationFrame ? (frequencyHz>40) : false; nframes; startts; // for FPS calc frameRate; intervalMsec; constructor(frequencyHz:number, callback:() => void) { this.frameRate = frequencyHz; this.intervalMsec = 1000.0 / frequencyHz; this.callback = callback; } scheduleFrame(msec:number) { var fn = () => { try { this.nextFrame(); } catch (e) { this.running = false; this.pulsing = false; throw e; } } if (this.useReqAnimFrame) window.requestAnimationFrame(fn); else setTimeout(fn, msec); } nextFrame(ts?:number) { if (!ts) ts = Date.now(); if (ts - this.lastts < this.intervalMsec*10) { this.lastts += this.intervalMsec; } else { this.lastts = ts + this.intervalMsec; // frames skipped, catch up } if (!this.useReqAnimFrame || this.lastts - ts > this.intervalMsec/2) { if (this.running) { this.callback(); } if (this.nframes == 0) this.startts = ts; if (this.nframes++ == 300) { console.log("Avg framerate: " + this.nframes*1000/(ts-this.startts) + " fps"); } } if (this.running) { this.scheduleFrame(this.lastts - ts); } else { this.pulsing = false; } } isRunning() { return this.running; } start() { if (!this.running) { this.running = true; this.lastts = 0; this.nframes = 0; if (!this.pulsing) { this.scheduleFrame(0); this.pulsing = true; } } } stop() { this.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); } /// TOOLBAR export class Toolbar { span : JQuery; grp : JQuery; mousetrap; boundkeys = []; constructor(parentDiv:HTMLElement, focusDiv:HTMLElement) { this.mousetrap = focusDiv ? new Mousetrap(focusDiv) : Mousetrap; this.span = $(document.createElement("span")).addClass("btn_toolbar"); parentDiv.appendChild(this.span[0]); this.newGroup(); } destroy() { if (this.span) { this.span.remove(); this.span = null; } if (this.mousetrap) { for (var key of this.boundkeys) { this.mousetrap.unbind(key); } this.mousetrap = null; } } newGroup() { return this.grp = $(document.createElement("span")).addClass("btn_group").appendTo(this.span); } add(key:string, alttext:string, icon:string, fn:(e,combo) => void) { var btn = null; if (icon) { btn = $(document.createElement("button")).addClass("btn"); if (icon.startsWith('glyphicon')) { icon = ''; } btn.html(icon); btn.prop("title", key ? (alttext+" ("+key+")") : alttext); btn.click(fn); this.grp.append(btn); } if (key) { this.mousetrap.bind(key, fn); this.boundkeys.push(key); } return btn; } } // https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas export function getMousePos(canvas : HTMLCanvasElement, evt) : {x:number,y:number} { var rect = canvas.getBoundingClientRect(), // abs. size of element scaleX = canvas.width / rect.width, // relationship bitmap vs. element for X scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y return { x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have y: (evt.clientY - rect.top) * scaleY // been adjusted to be relative to element } }