mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
parent
1923d7a71c
commit
be35aa9315
5
.github/workflows/nodejs.yml
vendored
5
.github/workflows/nodejs.yml
vendored
@ -12,11 +12,14 @@ jobs:
|
||||
node-version: [12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- uses: webfactory/ssh-agent@v0.5.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
- name: npm install, build, and test
|
||||
run: |
|
||||
npm ci
|
||||
|
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
<div id="display">
|
||||
<div class="overscan">
|
||||
<canvas id="screen" width="560" height="384"></canvas>
|
||||
<canvas id="screen" width="592" height="416"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inset">
|
||||
@ -195,10 +195,10 @@
|
||||
<h3>Monitor</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="checkbox" id="green_screen"
|
||||
<input type="checkbox" id="mono_screen"
|
||||
onclick="Apple2.updateScreen()" />
|
||||
<label for="green_screen">
|
||||
Green Screen
|
||||
<label for="mono_screen">
|
||||
Mono Screen
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@ -208,6 +208,13 @@
|
||||
Show Scanlines
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="gl_canvas"
|
||||
onclick="Apple2.updateScreen()" />
|
||||
<label for="gl_canvas">
|
||||
GL Renderer (Restart required)
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Sound</h3>
|
||||
<ul>
|
||||
|
@ -50,7 +50,7 @@
|
||||
</div>
|
||||
<div id="display">
|
||||
<div class="overscan">
|
||||
<canvas id="screen" width="560" height="384"></canvas>
|
||||
<canvas id="screen" width="592" height="416"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inset">
|
||||
@ -200,10 +200,10 @@
|
||||
<h3>Monitor</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="checkbox" id="green_screen"
|
||||
<input type="checkbox" id="mono_screen"
|
||||
onclick="Apple2.updateScreen()" />
|
||||
<label for="green_screen">
|
||||
Green Screen
|
||||
<label for="mono_screen">
|
||||
Mono Screen
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@ -213,6 +213,13 @@
|
||||
Show Scanlines
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="gl_canvas"
|
||||
onclick="Apple2.updateScreen()" />
|
||||
<label for="gl_canvas">
|
||||
GL Renderer (Restart required)
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Sound</h3>
|
||||
<ul>
|
||||
|
@ -55,9 +55,8 @@ body {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
background-color: black;
|
||||
padding: 16px;
|
||||
width: 560px;
|
||||
height: 384px;
|
||||
width: 592px;
|
||||
height: 416px;
|
||||
border: 6px inset #f0edd0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@ -189,7 +188,7 @@ canvas {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.green {
|
||||
.mono {
|
||||
filter: url('#green');
|
||||
}
|
||||
|
||||
@ -209,6 +208,8 @@ canvas {
|
||||
-moz-image-rendering: -moz-crisp-edges;
|
||||
-webkit-image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: optimizeSpeed;
|
||||
width: 592px;
|
||||
height: 416px;
|
||||
}
|
||||
|
||||
#screen:-webkit-full-screen {
|
||||
|
50
js/apple2.ts
50
js/apple2.ts
@ -1,21 +1,36 @@
|
||||
import Apple2IO from './apple2io';
|
||||
import { HiresPage, LoresPage, VideoModes } from './canvas';
|
||||
// import * as gl from './gl';
|
||||
import {
|
||||
HiresPage,
|
||||
LoresPage,
|
||||
VideoModes,
|
||||
} from './videomodes';
|
||||
import {
|
||||
HiresPage2D,
|
||||
LoresPage2D,
|
||||
VideoModes2D,
|
||||
} from './canvas';
|
||||
import {
|
||||
HiresPageGL,
|
||||
LoresPageGL,
|
||||
VideoModesGL,
|
||||
} from './gl';
|
||||
import CPU6502, { PageHandler, CpuState } from './cpu6502';
|
||||
import MMU from './mmu';
|
||||
import RAM from './ram';
|
||||
import { debug } from './util';
|
||||
|
||||
import SYMBOLS from './symbols';
|
||||
import { Restorable } from './types';
|
||||
import { Restorable, memory } from './types';
|
||||
import { processGamepad } from './ui/gamepad';
|
||||
|
||||
interface Options {
|
||||
characterRom: any,
|
||||
characterRom: memory,
|
||||
enhanced: boolean,
|
||||
e: boolean,
|
||||
multiScreen: boolean,
|
||||
gl: boolean,
|
||||
rom: PageHandler,
|
||||
screen: any[],
|
||||
canvas: HTMLCanvasElement,
|
||||
tick: () => void,
|
||||
}
|
||||
|
||||
@ -44,7 +59,6 @@ export class Apple2 implements Restorable<State> {
|
||||
private io: Apple2IO;
|
||||
private mmu: MMU;
|
||||
|
||||
private multiScreen: boolean;
|
||||
private tick: () => void;
|
||||
|
||||
private stats = {
|
||||
@ -53,16 +67,18 @@ export class Apple2 implements Restorable<State> {
|
||||
};
|
||||
|
||||
constructor(options: Options) {
|
||||
const LoresPage = options.gl ? LoresPageGL : LoresPage2D;
|
||||
const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
|
||||
const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
|
||||
|
||||
this.cpu = new CPU6502({ '65C02': options.enhanced });
|
||||
this.gr = new LoresPage(1, options.characterRom, options.e, options.screen[0]);
|
||||
this.gr2 = new LoresPage(2, options.characterRom, options.e, options.screen[1]);
|
||||
this.hgr = new HiresPage(1, options.screen[2]);
|
||||
this.hgr2 = new HiresPage(2, options.screen[3]);
|
||||
this.vm = new VideoModes(this.gr, this.hgr, this.gr2, this.hgr2, options.e);
|
||||
this.vm.multiScreen(options.multiScreen);
|
||||
this.gr = new LoresPage(1, options.characterRom, options.e);
|
||||
this.gr2 = new LoresPage(2, options.characterRom, options.e);
|
||||
this.hgr = new HiresPage(1);
|
||||
this.hgr2 = new HiresPage(2);
|
||||
this.vm = new VideoModes(this.gr, this.hgr, this.gr2, this.hgr2, options.canvas, options.e);
|
||||
this.vm.enhanced(options.enhanced);
|
||||
this.io = new Apple2IO(this.cpu, this.vm);
|
||||
this.multiScreen = options.multiScreen;
|
||||
this.tick = options.tick;
|
||||
|
||||
if (options.e) {
|
||||
@ -129,11 +145,9 @@ export class Apple2 implements Restorable<State> {
|
||||
this.mmu.resetVB();
|
||||
}
|
||||
if (this.io.annunciator(0)) {
|
||||
if (this.multiScreen) {
|
||||
this.vm.blit();
|
||||
}
|
||||
if (this.io.blit()) {
|
||||
this.stats.renderedFrames++;
|
||||
const imageData = this.io.blit();
|
||||
if (imageData) {
|
||||
this.vm.blit(imageData);
|
||||
}
|
||||
} else {
|
||||
if (this.vm.blit()) {
|
||||
|
@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import CPU6502, { PageHandler } from './cpu6502';
|
||||
import { byte } from './types';
|
||||
import { Card, Memory, TapeData, byte } from './types';
|
||||
import { debug } from './util';
|
||||
|
||||
type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
@ -66,8 +66,8 @@ const LOC = {
|
||||
};
|
||||
|
||||
export default class Apple2IO implements PageHandler {
|
||||
private _slot: any[] = []; // TODO(flan): Needs typing.
|
||||
private _auxRom: any = null; // TODO(flan): Needs typing.
|
||||
private _slot: Card[] = [];
|
||||
private _auxRom: Memory | null = null;
|
||||
|
||||
private _khz = 1023;
|
||||
private _rate = 44000;
|
||||
@ -94,9 +94,9 @@ export default class Apple2IO implements PageHandler {
|
||||
private _trigger = 0;
|
||||
private _annunciators: Annunciators = [false, false, false, false];
|
||||
|
||||
private _tape = [];
|
||||
private _tape: TapeData = [];
|
||||
private _tapeOffset = 0;
|
||||
private _tapeNext = 0;
|
||||
private _tapeNext: number = 0;
|
||||
private _tapeCurrent = false;
|
||||
|
||||
constructor(private readonly cpu: CPU6502, private readonly vm: any) {
|
||||
@ -107,7 +107,7 @@ export default class Apple2IO implements PageHandler {
|
||||
this._calcSampleRate();
|
||||
}
|
||||
|
||||
_debug(..._args: any) {
|
||||
_debug(..._args: any[]) {
|
||||
// debug.apply(this, arguments);
|
||||
}
|
||||
|
||||
@ -318,8 +318,8 @@ export default class Apple2IO implements PageHandler {
|
||||
reset() {
|
||||
for (let slot = 0; slot < 8; slot++) {
|
||||
const card = this._slot[slot];
|
||||
if (card && card.reset) {
|
||||
card.reset();
|
||||
if (card) {
|
||||
card.reset?.();
|
||||
}
|
||||
}
|
||||
this.vm.reset();
|
||||
@ -327,20 +327,20 @@ export default class Apple2IO implements PageHandler {
|
||||
|
||||
blit() {
|
||||
const card = this._slot[3];
|
||||
if (card && card.blit) {
|
||||
return card.blit();
|
||||
if (card) {
|
||||
return card.blit?.();
|
||||
}
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
read(page: byte, off: byte) {
|
||||
let result = 0;
|
||||
let result: number = 0;
|
||||
let slot;
|
||||
let card;
|
||||
|
||||
switch (page) {
|
||||
case 0xc0:
|
||||
result = this.ioSwitch(off, undefined);
|
||||
result = this.ioSwitch(off, undefined) || 0;
|
||||
break;
|
||||
case 0xc1:
|
||||
case 0xc2:
|
||||
@ -411,7 +411,7 @@ export default class Apple2IO implements PageHandler {
|
||||
this._annunciators = state.annunciators;
|
||||
}
|
||||
|
||||
setSlot(slot: slot, card: byte) {
|
||||
setSlot(slot: slot, card: Card) {
|
||||
this._slot[slot] = card;
|
||||
}
|
||||
|
||||
@ -453,7 +453,7 @@ export default class Apple2IO implements PageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
setTape(tape: any) { // TODO(flan): Needs typing.
|
||||
setTape(tape: TapeData) { // TODO(flan): Needs typing.
|
||||
debug('Tape length: ' + tape.length);
|
||||
this._tape = tape;
|
||||
this._tapeOffset = -1;
|
||||
@ -470,8 +470,8 @@ export default class Apple2IO implements PageHandler {
|
||||
tick() {
|
||||
this._tick();
|
||||
for (let idx = 0; idx < 8; idx++) {
|
||||
if (this._slot[idx] && this._slot[idx].tick) {
|
||||
this._slot[idx].tick();
|
||||
if (this._slot[idx]) {
|
||||
this._slot[idx].tick?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
265
js/canvas.ts
265
js/canvas.ts
@ -10,11 +10,21 @@
|
||||
*/
|
||||
|
||||
import { base64_decode, base64_encode } from './base64';
|
||||
import { byte, memory, Memory, Restorable } from './types';
|
||||
import { byte, memory, Memory } from './types';
|
||||
import { allocMemPages } from './util';
|
||||
import {
|
||||
Color,
|
||||
GraphicsState,
|
||||
HiresPage,
|
||||
LoresPage,
|
||||
Region,
|
||||
VideoModes,
|
||||
VideoModesState,
|
||||
bank,
|
||||
pageNo
|
||||
} from './videomodes';
|
||||
|
||||
let enhanced = false;
|
||||
let multiScreen = false;
|
||||
let textMode = true;
|
||||
let mixedMode = false;
|
||||
let hiresMode = false;
|
||||
@ -30,41 +40,6 @@ let highColorHGRMode = false;
|
||||
let highColorTextMode = false;
|
||||
let oneSixtyMode = false;
|
||||
|
||||
type bank = 0 | 1;
|
||||
type pageNo = 1 | 2;
|
||||
|
||||
interface Color {
|
||||
0: byte, // red
|
||||
1: byte, // green
|
||||
2: byte, // blue
|
||||
}
|
||||
|
||||
interface Region {
|
||||
top: number,
|
||||
bottom: number,
|
||||
left: number,
|
||||
right: number,
|
||||
}
|
||||
|
||||
|
||||
interface GraphicsState {
|
||||
page: byte;
|
||||
mono: boolean;
|
||||
buffer: string[];
|
||||
}
|
||||
|
||||
interface VideoModesState {
|
||||
grs: [gr1: GraphicsState, _gr2: GraphicsState],
|
||||
hgrs: [hgr1: GraphicsState, hgr2: GraphicsState],
|
||||
textMode: boolean,
|
||||
mixedMode: boolean,
|
||||
hiresMode: boolean,
|
||||
pageMode: pageNo,
|
||||
_80colMode: boolean,
|
||||
altCharMode: boolean,
|
||||
an3: boolean,
|
||||
}
|
||||
|
||||
function dim(c: Color): Color {
|
||||
return [
|
||||
c[0] * 0.75 & 0xff,
|
||||
@ -151,6 +126,12 @@ const dcolors: Color[] = [
|
||||
[255, 255, 255], // 0xf white
|
||||
];
|
||||
|
||||
const notDirty: Region = {
|
||||
top: 385,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
|
||||
/****************************************************************************
|
||||
*
|
||||
@ -158,40 +139,31 @@ const dcolors: Color[] = [
|
||||
*
|
||||
***************************************************************************/
|
||||
|
||||
export class LoresPage implements Memory, Restorable<GraphicsState> {
|
||||
export class LoresPage2D implements LoresPage {
|
||||
// $00-$3F inverse
|
||||
// $40-$7F flashing
|
||||
// $80-$FF normal
|
||||
|
||||
private _imageData: { data: number[]; };
|
||||
private _buffer: memory[] = [];
|
||||
private _refreshing = false;
|
||||
private _monoMode = false;
|
||||
private _blink = false;
|
||||
private _dirty: Region = {
|
||||
top: 385,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
|
||||
dirty: Region = {...notDirty}
|
||||
imageData: ImageData;
|
||||
|
||||
constructor(private page: number,
|
||||
private readonly charset: any,
|
||||
private readonly e: any,
|
||||
private readonly context: any) {
|
||||
this._init();
|
||||
}
|
||||
|
||||
_init() {
|
||||
this._imageData = this.context.createImageData(560, 384);
|
||||
private readonly charset: memory,
|
||||
private readonly e: boolean) {
|
||||
this.imageData = new ImageData(560, 384);
|
||||
for (let idx = 0; idx < 560 * 384 * 4; idx++) {
|
||||
this._imageData.data[idx] = 0xff;
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
this._buffer[0] = allocMemPages(0x4);
|
||||
this._buffer[1] = allocMemPages(0x4);
|
||||
}
|
||||
|
||||
_drawPixel(data: number[], off: number, color: Color) {
|
||||
_drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
data[off + 0] = data[off + 4] = c0;
|
||||
data[off + 1] = data[off + 5] = c1;
|
||||
@ -202,7 +174,7 @@ export class LoresPage implements Memory, Restorable<GraphicsState> {
|
||||
data[nextOff + 2] = data[nextOff + 6] = c2;
|
||||
}
|
||||
|
||||
_drawHalfPixel(data: number[], off: number, color: Color) {
|
||||
_drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
data[off + 0] = c0;
|
||||
data[off + 1] = c1;
|
||||
@ -263,16 +235,16 @@ export class LoresPage implements Memory, Restorable<GraphicsState> {
|
||||
const ee = adj >> 7;
|
||||
const row = ab | cd | ee;
|
||||
|
||||
const data = this._imageData.data;
|
||||
const data = this.imageData.data;
|
||||
if ((row < 24) && (col < 40)) {
|
||||
let y = row << 4;
|
||||
if (y < this._dirty.top) { this._dirty.top = y; }
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
y += 16;
|
||||
if (y > this._dirty.bottom) { this._dirty.bottom = y; }
|
||||
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
|
||||
let x = col * 14;
|
||||
if (x < this._dirty.left) { this._dirty.left = x; }
|
||||
if (x < this.dirty.left) { this.dirty.left = x; }
|
||||
x += 14;
|
||||
if (x > this._dirty.right) { this._dirty.right = x; }
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
|
||||
let color;
|
||||
if (textMode || hiresMode || (mixedMode && row > 19)) {
|
||||
@ -474,29 +446,6 @@ export class LoresPage implements Memory, Restorable<GraphicsState> {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
blit(mixed: boolean = false): boolean {
|
||||
if (this._dirty.top === 385) { return false; }
|
||||
let top = this._dirty.top;
|
||||
const bottom = this._dirty.bottom;
|
||||
const left = this._dirty.left;
|
||||
const right = this._dirty.right;
|
||||
|
||||
if (mixed) {
|
||||
if (bottom < 320) { return false; }
|
||||
if (top < 320) { top = 320; }
|
||||
}
|
||||
this.context.putImageData(
|
||||
this._imageData, 0, 0, left, top, right - left, bottom - top
|
||||
);
|
||||
this._dirty = {
|
||||
top: 385,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
start() {
|
||||
setInterval(() => this.blink(), 267);
|
||||
return this._start();
|
||||
@ -574,14 +523,9 @@ export class LoresPage implements Memory, Restorable<GraphicsState> {
|
||||
*
|
||||
***************************************************************************/
|
||||
|
||||
export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
private _imageData: { data: number[]; };
|
||||
private _dirty: Region = {
|
||||
top: 385,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
export class HiresPage2D implements HiresPage {
|
||||
public imageData: ImageData;
|
||||
dirty: Region = {...notDirty}
|
||||
|
||||
private _buffer: memory[] = [];
|
||||
private _refreshing = false;
|
||||
@ -589,21 +533,16 @@ export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
private _monoMode = false;
|
||||
|
||||
constructor(
|
||||
private page: number,
|
||||
private readonly context: any) {
|
||||
this._init();
|
||||
}
|
||||
|
||||
_init() {
|
||||
this._imageData = this.context.createImageData(560, 384);
|
||||
private page: number) {
|
||||
this.imageData = new ImageData(560, 384);
|
||||
for (let idx = 0; idx < 560 * 384 * 4; idx++) {
|
||||
this._imageData.data[idx] = 0xff;
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
this._buffer[0] = allocMemPages(0x20);
|
||||
this._buffer[1] = allocMemPages(0x20);
|
||||
}
|
||||
|
||||
_drawPixel(data: number[], off: number, color: Color) {
|
||||
_drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
|
||||
data[off + 0] = data[off + 4] = c0;
|
||||
@ -615,7 +554,7 @@ export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
data[nextOff + 2] = data[nextOff + 6] = c2;
|
||||
}
|
||||
|
||||
_drawHalfPixel(data: number[], off: number, color: Color) {
|
||||
_drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
data[off + 0] = c0;
|
||||
data[off + 1] = c1;
|
||||
@ -630,7 +569,7 @@ export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
// 160x192 pixels alternate 3 and 4 base pixels wide
|
||||
//
|
||||
|
||||
_draw3Pixel(data: number[], off: number, color: Color) {
|
||||
_draw3Pixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
|
||||
data[off + 0] = data[off + 4] = data[off + 8] = c0;
|
||||
@ -642,7 +581,7 @@ export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
data[nextOff + 2] = data[nextOff + 6] = data[nextOff + 10] = c2;
|
||||
}
|
||||
|
||||
_draw4Pixel(data: number[], off: number, color: Color) {
|
||||
_draw4Pixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
|
||||
data[off + 0] = data[off + 4] = data[off + 8] = data[off + 12] = c0;
|
||||
@ -703,21 +642,21 @@ export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
const rowa = ab | cd | e,
|
||||
rowb = base >> 10;
|
||||
|
||||
const data = this._imageData.data;
|
||||
const data = this.imageData.data;
|
||||
let dx, dy;
|
||||
if ((rowa < 24) && (col < 40)) {
|
||||
if (!multiScreen && !hiresMode) {
|
||||
if (!hiresMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let y = rowa << 4 | rowb << 1;
|
||||
if (y < this._dirty.top) { this._dirty.top = y; }
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
y += 2;
|
||||
if (y > this._dirty.bottom) { this._dirty.bottom = y; }
|
||||
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
|
||||
let x = col * 14 - 2;
|
||||
if (x < this._dirty.left) { this._dirty.left = x; }
|
||||
if (x < this.dirty.left) { this.dirty.left = x; }
|
||||
x += 18;
|
||||
if (x > this._dirty.right) { this._dirty.right = x; }
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
|
||||
dy = rowa << 4 | rowb << 1;
|
||||
let bz, b0, b1, b2, b3, b4, c, hb;
|
||||
@ -919,29 +858,6 @@ export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
blit(mixed: boolean = false) {
|
||||
if (this._dirty.top === 385) { return false; }
|
||||
const top = this._dirty.top;
|
||||
let bottom = this._dirty.bottom;
|
||||
const left = this._dirty.left;
|
||||
const right = this._dirty.right;
|
||||
|
||||
if (mixed) {
|
||||
if (top > 320) { return false; }
|
||||
if (bottom > 320) { bottom = 320; }
|
||||
}
|
||||
this.context.putImageData(
|
||||
this._imageData, 0, 0, left, top, right - left, bottom - top
|
||||
);
|
||||
this._dirty = {
|
||||
top: 385,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
start() {
|
||||
return this._start();
|
||||
}
|
||||
@ -979,19 +895,28 @@ export class HiresPage implements Memory, Restorable<GraphicsState> {
|
||||
}
|
||||
}
|
||||
|
||||
export class VideoModes implements Restorable<VideoModesState> {
|
||||
export class VideoModes2D implements VideoModes {
|
||||
private _grs: LoresPage[];
|
||||
private _hgrs: HiresPage[];
|
||||
private _flag = 0;
|
||||
private _context: CanvasRenderingContext2D | null;
|
||||
private _left: number;
|
||||
private _top: number;
|
||||
|
||||
public ready = Promise.resolve();
|
||||
|
||||
constructor(
|
||||
gr: LoresPage,
|
||||
hgr: HiresPage,
|
||||
gr2: LoresPage,
|
||||
hgr2: HiresPage,
|
||||
private canvas: HTMLCanvasElement,
|
||||
private e: boolean) {
|
||||
this._grs = [gr, gr2];
|
||||
this._hgrs = [hgr, hgr2];
|
||||
this._context = this.canvas.getContext('2d');
|
||||
this._left = (this.canvas.width - 560) / 2;
|
||||
this._top = (this.canvas.height - 384) / 2;
|
||||
}
|
||||
|
||||
private _refresh() {
|
||||
@ -1108,10 +1033,6 @@ export class VideoModes implements Restorable<VideoModesState> {
|
||||
enhanced = on;
|
||||
}
|
||||
|
||||
multiScreen(on: boolean) {
|
||||
multiScreen = on;
|
||||
}
|
||||
|
||||
isText() {
|
||||
return textMode;
|
||||
}
|
||||
@ -1140,25 +1061,56 @@ export class VideoModes implements Restorable<VideoModesState> {
|
||||
return altCharMode;
|
||||
}
|
||||
|
||||
blit() {
|
||||
let blitted = false;
|
||||
if (multiScreen) {
|
||||
blitted = this._grs[0].blit() || blitted;
|
||||
blitted = this._grs[1].blit() || blitted;
|
||||
blitted = this._hgrs[0].blit() || blitted;
|
||||
blitted = this._hgrs[1].blit() || blitted;
|
||||
} else {
|
||||
if (hiresMode && !textMode) {
|
||||
if (mixedMode) {
|
||||
blitted = this._grs[pageMode - 1].blit(true) || blitted;
|
||||
blitted = this._hgrs[pageMode - 1].blit(true) || blitted;
|
||||
} else {
|
||||
blitted = this._hgrs[pageMode - 1].blit();
|
||||
}
|
||||
} else {
|
||||
blitted = this._grs[pageMode - 1].blit();
|
||||
}
|
||||
updateImage(
|
||||
mainData: ImageData,
|
||||
mainDirty: Region,
|
||||
mixData?: ImageData | null,
|
||||
mixDirty?: Region | null
|
||||
) {
|
||||
if (!this._context) {
|
||||
throw new Error('No 2D context');
|
||||
}
|
||||
if (mainDirty.bottom !== -1 || (mixDirty && mixDirty.bottom !== -1)) {
|
||||
if (mixData) {
|
||||
this._context.putImageData(
|
||||
mainData, this._left, this._top, 0, 0, 560, 320
|
||||
);
|
||||
this._context.putImageData(
|
||||
mixData, this._left, this._top, 0, 320, 560, 64
|
||||
);
|
||||
} else {
|
||||
this._context.putImageData(mainData, this._top, this._left);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
blit(altData?: ImageData) {
|
||||
let blitted = false;
|
||||
const hgr = this._hgrs[pageMode - 1];
|
||||
const gr = this._grs[pageMode - 1];
|
||||
|
||||
if (altData) {
|
||||
blitted = this.updateImage(
|
||||
altData,
|
||||
{top: 0, left: 0, right: 560, bottom: 384}
|
||||
);
|
||||
} else if (hiresMode && !textMode) {
|
||||
blitted = this.updateImage(
|
||||
hgr.imageData,
|
||||
hgr.dirty,
|
||||
mixedMode ? gr.imageData : null,
|
||||
mixedMode ? gr.dirty : null
|
||||
);
|
||||
} else {
|
||||
blitted = this.updateImage(
|
||||
gr.imageData, gr.dirty
|
||||
);
|
||||
}
|
||||
hgr.dirty = {...notDirty};
|
||||
gr.dirty = {...notDirty};
|
||||
return blitted;
|
||||
}
|
||||
|
||||
@ -1197,6 +1149,7 @@ export class VideoModes implements Restorable<VideoModesState> {
|
||||
this._hgrs[0].mono(on);
|
||||
this._hgrs[1].mono(on);
|
||||
}
|
||||
|
||||
getText() {
|
||||
return this._grs[pageMode - 1].getText();
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
import { allocMemPages, debug } from '../util';
|
||||
import { ROM, VIDEO_ROM } from '../roms/cards/videoterm';
|
||||
|
||||
export default function Videoterm(io, context) {
|
||||
export default function Videoterm(_io) {
|
||||
debug('Videx Videoterm');
|
||||
|
||||
var LOC = {
|
||||
@ -78,7 +78,7 @@ export default function Videoterm(io, context) {
|
||||
function _init() {
|
||||
var idx;
|
||||
|
||||
_imageData = context.createImageData(560, 384);
|
||||
_imageData = new ImageData(560, 384);
|
||||
for (idx = 0; idx < 560 * 384 * 4; idx++) {
|
||||
_imageData.data[idx] = 0xff;
|
||||
}
|
||||
@ -252,11 +252,10 @@ export default function Videoterm(io, context) {
|
||||
_shouldRefresh = false;
|
||||
}
|
||||
if (_dirty) {
|
||||
context.putImageData(_imageData, 0, 0);
|
||||
_dirty = false;
|
||||
return true;
|
||||
return _imageData;
|
||||
}
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
942
js/gl.ts
Normal file
942
js/gl.ts
Normal file
@ -0,0 +1,942 @@
|
||||
/* Copyright 2010-2021 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { base64_decode, base64_encode } from './base64';
|
||||
import { byte, memory, Memory, Restorable } from './types';
|
||||
import { allocMemPages } from './util';
|
||||
|
||||
import { screenEmu } from 'apple2shader';
|
||||
import {
|
||||
Color,
|
||||
GraphicsState,
|
||||
HiresPage,
|
||||
LoresPage,
|
||||
Region,
|
||||
VideoModes,
|
||||
VideoModesState,
|
||||
bank,
|
||||
pageNo
|
||||
} from './videomodes';
|
||||
|
||||
let enhanced = false;
|
||||
let textMode = true;
|
||||
let mixedMode = false;
|
||||
let hiresMode = false;
|
||||
let pageMode: pageNo = 1;
|
||||
let _80colMode = false;
|
||||
let altCharMode = false;
|
||||
let an3 = false;
|
||||
let doubleHiresMode = false;
|
||||
|
||||
const tmpCanvas = document.createElement('canvas');
|
||||
const tmpContext = tmpCanvas.getContext('2d');
|
||||
|
||||
const buildScreen = (mainData: ImageData, mixData?: ImageData | null) => {
|
||||
if (!tmpContext) {
|
||||
throw new Error('No webgl context');
|
||||
}
|
||||
|
||||
const details = screenEmu.C.NTSC_DETAILS;
|
||||
const { width, height } = details.imageSize;
|
||||
const { x, y } = _80colMode ? details.topLeft80Col : details.topLeft;
|
||||
|
||||
tmpCanvas.width = width;
|
||||
tmpCanvas.height = height;
|
||||
tmpContext.fillStyle = 'rgba(0,0,0,1)';
|
||||
tmpContext.fillRect(0, 0, width, height);
|
||||
|
||||
if (mixData) {
|
||||
tmpContext.putImageData(mainData, x, y, 0, 0, 560, 160);
|
||||
tmpContext.putImageData(mixData, x, y, 0, 160, 560, 32);
|
||||
} else {
|
||||
tmpContext.putImageData(mainData, x, y);
|
||||
}
|
||||
return tmpContext.getImageData(0, 0, width, height);
|
||||
};
|
||||
|
||||
// Color constants
|
||||
const whiteCol: Color = [255, 255, 255];
|
||||
const blackCol: Color = [0, 0, 0];
|
||||
|
||||
const notDirty: Region = {
|
||||
top: 385,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
|
||||
/****************************************************************************
|
||||
*
|
||||
* Text/Lores Graphics
|
||||
*
|
||||
***************************************************************************/
|
||||
|
||||
export class LoresPageGL implements LoresPage {
|
||||
// $00-$3F inverse
|
||||
// $40-$7F flashing
|
||||
// $80-$FF normal
|
||||
|
||||
private _buffer: memory[] = [];
|
||||
private _monoMode = false;
|
||||
private _refreshing = false;
|
||||
private _blink = false;
|
||||
|
||||
dirty: Region = {...notDirty}
|
||||
imageData: ImageData;
|
||||
|
||||
constructor(private page: number,
|
||||
private readonly charset: memory,
|
||||
private readonly e: boolean) {
|
||||
this.imageData = new ImageData(560, 192);
|
||||
for (let idx = 0; idx < 560 * 192 * 4; idx++) {
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
this._buffer[0] = allocMemPages(0x4);
|
||||
this._buffer[1] = allocMemPages(0x4);
|
||||
}
|
||||
|
||||
_drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
data[off + 0] = data[off + 4] = c0;
|
||||
data[off + 1] = data[off + 5] = c1;
|
||||
data[off + 2] = data[off + 6] = c2;
|
||||
}
|
||||
|
||||
_drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
data[off + 0] = c0;
|
||||
data[off + 1] = c1;
|
||||
data[off + 2] = c2;
|
||||
}
|
||||
|
||||
bank0(): Memory {
|
||||
return {
|
||||
start: () => this._start(),
|
||||
end: () => this._end(),
|
||||
read: (page, off) => this._read(page, off, 0),
|
||||
write: (page, off, val) => this._write(page, off, val, 0),
|
||||
};
|
||||
}
|
||||
|
||||
bank1(): Memory {
|
||||
return {
|
||||
start: () => this._start(),
|
||||
end: () => this._end(),
|
||||
read: (page, off) => this._read(page, off, 1),
|
||||
write: (page, off, val) => this._write(page, off, val, 1),
|
||||
};
|
||||
}
|
||||
|
||||
// These are used by both bank 0 and 1
|
||||
|
||||
private _start() {
|
||||
return (0x04 * this.page);
|
||||
}
|
||||
|
||||
private _end() { return (0x04 * this.page) + 0x03; }
|
||||
|
||||
private _read(page: byte, off: byte, bank: bank) {
|
||||
const addr = (page << 8) | off, base = addr & 0x3FF;
|
||||
return this._buffer[bank][base];
|
||||
}
|
||||
|
||||
private _write(page: byte, off: byte, val: byte, bank: bank) {
|
||||
const addr = (page << 8) | off;
|
||||
const base = addr & 0x3FF;
|
||||
let fore, back;
|
||||
|
||||
if (this._buffer[bank][base] == val && !this._refreshing) {
|
||||
return;
|
||||
}
|
||||
this._buffer[bank][base] = val;
|
||||
|
||||
const col = (base % 0x80) % 0x28;
|
||||
const adj = off - col;
|
||||
|
||||
// 000001cd eabab000 -> 000abcde
|
||||
const ab = (adj & 0x18);
|
||||
const cd = (page & 0x03) << 1;
|
||||
const ee = adj >> 7;
|
||||
const row = ab | cd | ee;
|
||||
|
||||
const data = this.imageData.data;
|
||||
if ((row < 24) && (col < 40)) {
|
||||
let y = row << 3;
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
y += 8;
|
||||
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
|
||||
let x = col * 14;
|
||||
if (x < this.dirty.left) { this.dirty.left = x; }
|
||||
x += 14;
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
|
||||
if (textMode || hiresMode || (mixedMode && row > 19)) {
|
||||
let inverse;
|
||||
if (this.e) {
|
||||
if (!_80colMode && !altCharMode) {
|
||||
inverse = ((val & 0xc0) == 0x40) && this._blink;
|
||||
}
|
||||
} else {
|
||||
inverse = !((val & 0x80) || (val & 0x40) && this._blink);
|
||||
}
|
||||
|
||||
fore = inverse ? blackCol : whiteCol;
|
||||
back = inverse ? whiteCol : blackCol;
|
||||
|
||||
if (_80colMode) {
|
||||
if (!enhanced) {
|
||||
val = (val >= 0x40 && val < 0x60) ? val - 0x40 : val;
|
||||
} else if (!altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
|
||||
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = this.charset[val * 8 + jdx];
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x01) ? back : fore;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
}
|
||||
offset += 553 * 4;
|
||||
}
|
||||
} else {
|
||||
val = this._buffer[0][base];
|
||||
|
||||
if (!enhanced) {
|
||||
val = (val >= 0x40 && val < 0x60) ? val - 0x40 : val;
|
||||
} else if (!altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
let offset = (col * 14 + row * 560 * 8) * 4;
|
||||
|
||||
if (this.e) {
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = this.charset[val * 8 + jdx];
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x01) ? back : fore;
|
||||
this._drawPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 8;
|
||||
}
|
||||
offset += 546 * 4;
|
||||
}
|
||||
} else {
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = this.charset[val * 8 + jdx] << 1;
|
||||
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x80) ? fore : back;
|
||||
this._drawPixel(data, offset, color);
|
||||
b <<= 1;
|
||||
offset += 8;
|
||||
}
|
||||
offset += 546 * 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!_80colMode && bank == 1) {
|
||||
return;
|
||||
}
|
||||
if (_80colMode && !an3) {
|
||||
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
|
||||
b |= (b << 4);
|
||||
b |= (b << 8);
|
||||
if (col & 0x1) {
|
||||
b >>= 2;
|
||||
}
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x01) ? whiteCol : blackCol;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
}
|
||||
offset += 553 * 4;
|
||||
}
|
||||
} else {
|
||||
let offset = (col * 14 + row * 560 * 8) * 4;
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
|
||||
b |= (b << 4);
|
||||
b |= (b << 8);
|
||||
if (col & 0x1) {
|
||||
b >>= 2;
|
||||
}
|
||||
for (let idx = 0; idx < 14; idx++) {
|
||||
const color = (b & 0x0001) ? whiteCol : blackCol;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
}
|
||||
offset += 546 * 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
let addr = 0x400 * this.page;
|
||||
this._refreshing = true;
|
||||
for (let idx = 0; idx < 0x400; idx++, addr++) {
|
||||
this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
|
||||
if (_80colMode) {
|
||||
this._write(addr >> 8, addr & 0xff, this._buffer[1][idx], 1);
|
||||
}
|
||||
}
|
||||
this._refreshing = false;
|
||||
}
|
||||
|
||||
mono(on: boolean) {
|
||||
this._monoMode = on;
|
||||
}
|
||||
|
||||
blink() {
|
||||
let addr = 0x400 * this.page;
|
||||
this._refreshing = true;
|
||||
this._blink = !this._blink;
|
||||
for (let idx = 0; idx < 0x400; idx++, addr++) {
|
||||
const b = this._buffer[0][idx];
|
||||
if ((b & 0xC0) == 0x40) {
|
||||
this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
|
||||
}
|
||||
}
|
||||
this._refreshing = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
setInterval(() => this.blink(), 267);
|
||||
return this._start();
|
||||
}
|
||||
|
||||
end() {
|
||||
return this._end();
|
||||
}
|
||||
|
||||
read(page: byte, off: byte) {
|
||||
return this._read(page, off, 0);
|
||||
}
|
||||
|
||||
write(page: byte, off: byte, val: byte) {
|
||||
return this._write(page, off, val, 0);
|
||||
}
|
||||
|
||||
getState(): GraphicsState {
|
||||
return {
|
||||
page: this.page,
|
||||
mono: this._monoMode,
|
||||
buffer: [
|
||||
base64_encode(this._buffer[0]),
|
||||
base64_encode(this._buffer[1])
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: GraphicsState) {
|
||||
this.page = state.page;
|
||||
this._buffer[0] = base64_decode(state.buffer[0]);
|
||||
this._buffer[1] = base64_decode(state.buffer[1]);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private rowToBase(row: number) {
|
||||
const ab = (row >> 3) & 3;
|
||||
const cd = (row >> 1) & 0x3;
|
||||
const e = row & 1;
|
||||
return (cd << 8) | (e << 7) | (ab << 5) | (ab << 3);
|
||||
}
|
||||
|
||||
private mapCharCode(charCode: byte) {
|
||||
charCode &= 0x7F;
|
||||
if (charCode < 0x20) {
|
||||
charCode += 0x40;
|
||||
}
|
||||
if (!this.e && (charCode >= 0x60)) {
|
||||
charCode -= 0x40;
|
||||
}
|
||||
return charCode;
|
||||
}
|
||||
|
||||
getText() {
|
||||
let buffer = '', line, charCode;
|
||||
let row, col, base;
|
||||
for (row = 0; row < 24; row++) {
|
||||
base = this.rowToBase(row);
|
||||
line = '';
|
||||
if (this.e && _80colMode) {
|
||||
for (col = 0; col < 80; col++) {
|
||||
charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
|
||||
line += String.fromCharCode(charCode);
|
||||
}
|
||||
} else {
|
||||
for (col = 0; col < 40; col++) {
|
||||
charCode = this.mapCharCode(this._buffer[0][base + col]);
|
||||
line += String.fromCharCode(charCode);
|
||||
}
|
||||
}
|
||||
line = line.trimRight();
|
||||
buffer += line + '\n';
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
*
|
||||
* Hires Graphics
|
||||
*
|
||||
***************************************************************************/
|
||||
|
||||
export class HiresPageGL implements Memory, Restorable<GraphicsState> {
|
||||
|
||||
private _buffer: memory[] = [];
|
||||
private _refreshing = false;
|
||||
private _monoMode = false;
|
||||
|
||||
dirty: Region = {
|
||||
top: 193,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
imageData: ImageData;
|
||||
|
||||
constructor(
|
||||
private page: number) {
|
||||
this.imageData = new ImageData(560, 192);
|
||||
for (let idx = 0; idx < 560 * 192 * 4; idx++) {
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
this._buffer[0] = allocMemPages(0x20);
|
||||
this._buffer[1] = allocMemPages(0x20);
|
||||
}
|
||||
|
||||
_drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
|
||||
data[off + 0] = data[off + 4] = c0;
|
||||
data[off + 1] = data[off + 5] = c1;
|
||||
data[off + 2] = data[off + 6] = c2;
|
||||
}
|
||||
|
||||
_drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
|
||||
data[off + 0] = c0;
|
||||
data[off + 1] = c1;
|
||||
data[off + 2] = c2;
|
||||
}
|
||||
|
||||
bank0(): Memory {
|
||||
return {
|
||||
start: () => this._start(),
|
||||
end: () => this._end(),
|
||||
read: (page, off) => this._read(page, off, 0),
|
||||
write: (page, off, val) => this._write(page, off, val, 0),
|
||||
};
|
||||
}
|
||||
|
||||
bank1(): Memory {
|
||||
return {
|
||||
start: () => this._start(),
|
||||
end: () => this._end(),
|
||||
read: (page, off) => this._read(page, off, 1),
|
||||
write: (page, off, val) => this._write(page, off, val, 1),
|
||||
};
|
||||
}
|
||||
|
||||
_start() { return (0x20 * this.page); }
|
||||
|
||||
_end() { return (0x020 * this.page) + 0x1f; }
|
||||
|
||||
_read(page: byte, off: byte, bank: bank) {
|
||||
const addr = (page << 8) | off, base = addr & 0x1FFF;
|
||||
return this._buffer[bank][base];
|
||||
}
|
||||
|
||||
_write(page: byte, off: byte, val: byte, bank: bank) {
|
||||
const addr = (page << 8) | off;
|
||||
const base = addr & 0x1FFF;
|
||||
|
||||
if (this._buffer[bank][base] == val && !this._refreshing) {
|
||||
return;
|
||||
}
|
||||
this._buffer[bank][base] = val;
|
||||
|
||||
// let hbs = val & 0x80;
|
||||
|
||||
const col = (base % 0x80) % 0x28;
|
||||
const adj = off - col;
|
||||
|
||||
// 000001cd eabab000 -> 000abcde
|
||||
const ab = (adj & 0x18);
|
||||
const cd = (page & 0x03) << 1;
|
||||
const e = adj >> 7;
|
||||
|
||||
const rowa = ab | cd | e,
|
||||
rowb = base >> 10;
|
||||
|
||||
const data = this.imageData.data;
|
||||
let dx, dy;
|
||||
if ((rowa < 24) && (col < 40)) {
|
||||
if (!hiresMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let y = rowa << 3 | rowb;
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
y += 1;
|
||||
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
|
||||
let x = col * 14 - 2;
|
||||
if (x < this.dirty.left) { this.dirty.left = x; }
|
||||
x += 18;
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
|
||||
dy = rowa << 3 | rowb;
|
||||
let bz, b0, b1, b2, b3, b4, c, hb;
|
||||
if (doubleHiresMode) {
|
||||
val &= 0x7f;
|
||||
|
||||
// Every 4 bytes is 7 pixels
|
||||
// 2 bytes per bank
|
||||
|
||||
// b0 b1 b2 b3
|
||||
// c0 c1 c2 c3 c4 c5 c6
|
||||
// 76543210 76543210 76543210 76543210
|
||||
// 1111222 2333344 4455556 6667777
|
||||
|
||||
const mod = col % 2, mcol = col - mod, baseOff = base - mod;
|
||||
bz = this._buffer[0][baseOff - 1];
|
||||
b0 = this._buffer[1][baseOff];
|
||||
b1 = this._buffer[0][baseOff];
|
||||
b2 = this._buffer[1][baseOff + 1];
|
||||
b3 = this._buffer[0][baseOff + 1];
|
||||
b4 = this._buffer[1][baseOff + 2];
|
||||
c = [
|
||||
0,
|
||||
((b0 & 0x0f) >> 0), // 0
|
||||
((b0 & 0x70) >> 4) | ((b1 & 0x01) << 3), // 1
|
||||
((b1 & 0x1e) >> 1), // 2
|
||||
((b1 & 0x60) >> 5) | ((b2 & 0x03) << 2), // 3
|
||||
((b2 & 0x3c) >> 2), // 4
|
||||
((b2 & 0x40) >> 6) | ((b3 & 0x07) << 1), // 5
|
||||
((b3 & 0x78) >> 3), // 6
|
||||
0
|
||||
], // 7
|
||||
hb = [
|
||||
0,
|
||||
b0 & 0x80, // 0
|
||||
b0 & 0x80, // 1
|
||||
b1 & 0x80, // 2
|
||||
b2 & 0x80, // 3
|
||||
b2 & 0x80, // 4
|
||||
b3 & 0x80, // 5
|
||||
b3 & 0x80, // 6
|
||||
0
|
||||
]; // 7
|
||||
if (col > 0) {
|
||||
c[0] = (bz & 0x78) >> 3;
|
||||
hb[0] = bz & 0x80;
|
||||
}
|
||||
if (col < 39) {
|
||||
c[8] = b4 & 0x0f;
|
||||
hb[8] = b4 & 0x80;
|
||||
}
|
||||
dx = mcol * 14;
|
||||
let offset = dx * 4 + dy * 280 * 4 * 2;
|
||||
|
||||
for (let idx = 1; idx < 8; idx++) {
|
||||
// hbs = hb[idx];
|
||||
let bits = c[idx - 1] | (c[idx] << 4) | (c[idx + 1] << 8);
|
||||
for (let jdx = 0; jdx < 4; jdx++, offset += 4) {
|
||||
if (bits & 0x10) {
|
||||
this._drawHalfPixel(data, offset, whiteCol);
|
||||
} else {
|
||||
this._drawHalfPixel(data, offset, blackCol);
|
||||
}
|
||||
bits >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._refreshing) {
|
||||
this._refreshing = true;
|
||||
const bb: bank = bank ? 0 : 1;
|
||||
for (let rr = addr - 1; rr <= addr + 1; rr++) {
|
||||
const vv = this._buffer[bb][rr - 0x2000 * this.page];
|
||||
this._write(rr >> 8, rr & 0xff, vv, bb);
|
||||
}
|
||||
this._refreshing = false;
|
||||
}
|
||||
} else {
|
||||
val = this._buffer[0][base];
|
||||
const hbs = val & 0x80;
|
||||
val &= 0x7f;
|
||||
dx = col * 14 - 2;
|
||||
b0 = col > 0 ? this._buffer[0][base - 1] : 0;
|
||||
b2 = col < 39 ? this._buffer[0][base + 1] : 0;
|
||||
val |= (b2 & 0x3) << 7;
|
||||
let v1 = b0 & 0x40,
|
||||
v2 = val & 0x1,
|
||||
color;
|
||||
|
||||
let offset = dx * 4 + dy * 560 * 4 + (hbs ? 4 : 0);
|
||||
|
||||
for (let idx = 0; idx < 9; idx++, offset += 8) {
|
||||
val >>= 1;
|
||||
|
||||
if (v1) {
|
||||
color = whiteCol;
|
||||
} else {
|
||||
color = blackCol;
|
||||
}
|
||||
|
||||
if (dx > -1 && dx < 560) {
|
||||
this._drawPixel(data, offset, color);
|
||||
}
|
||||
dx += 2;
|
||||
|
||||
v1 = v2;
|
||||
v2 = val & 0x01;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
let addr = 0x2000 * this.page;
|
||||
this._refreshing = true;
|
||||
for (let idx = 0; idx < 0x2000; idx++, addr++) {
|
||||
const page = addr >> 8;
|
||||
const off = addr & 0xff;
|
||||
this._write(page, off, this._buffer[0][idx], 0);
|
||||
if (_80colMode) {
|
||||
this._write(page, off, this._buffer[1][idx], 1);
|
||||
}
|
||||
}
|
||||
this._refreshing = false;
|
||||
}
|
||||
|
||||
mono(on: boolean) {
|
||||
this._monoMode = on;
|
||||
}
|
||||
|
||||
start() {
|
||||
return this._start();
|
||||
}
|
||||
|
||||
end() {
|
||||
return this._end();
|
||||
}
|
||||
|
||||
read(page: byte, off: byte) {
|
||||
return this._read(page, off, 0);
|
||||
}
|
||||
|
||||
write(page: byte, off: byte, val: byte) {
|
||||
return this._write(page, off, val, 0);
|
||||
}
|
||||
|
||||
getState(): GraphicsState {
|
||||
return {
|
||||
page: this.page,
|
||||
mono: this._monoMode,
|
||||
buffer: [
|
||||
base64_encode(this._buffer[0]),
|
||||
base64_encode(this._buffer[1])
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: GraphicsState) {
|
||||
this.page = state.page;
|
||||
this._buffer[0] = base64_decode(state.buffer[0]);
|
||||
this._buffer[1] = base64_decode(state.buffer[1]);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
export class VideoModesGL implements VideoModes {
|
||||
private _grs: LoresPage[];
|
||||
private _hgrs: HiresPage[];
|
||||
private _flag = 0;
|
||||
private _sv: any;
|
||||
private _displayConfig: any;
|
||||
private _monoMode: boolean = false;
|
||||
|
||||
ready: Promise<void>
|
||||
|
||||
constructor(
|
||||
gr: LoresPage,
|
||||
hgr: HiresPage,
|
||||
gr2: LoresPage,
|
||||
hgr2: HiresPage,
|
||||
private canvas: HTMLCanvasElement,
|
||||
private e: boolean) {
|
||||
this._grs = [gr, gr2];
|
||||
this._hgrs = [hgr, hgr2];
|
||||
this._sv = new screenEmu.ScreenView(this.canvas);
|
||||
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this._sv.initOpenGL();
|
||||
|
||||
(window as any)._sv = this._sv;
|
||||
|
||||
this._displayConfig = new screenEmu.DisplayConfiguration();
|
||||
this._displayConfig.displayResolution = new screenEmu.Size(this.canvas.width, this.canvas.height);
|
||||
this._displayConfig.displayResolution = new screenEmu.Size(this.canvas.width, this.canvas.height);
|
||||
this._displayConfig.displayScanlineLevel = 0.5;
|
||||
this._displayConfig.videoWhiteOnly = true;
|
||||
this._displayConfig.videoSaturation = 0.8;
|
||||
this._displayConfig.videoSize = new screenEmu.Size(1.25, 1.15);
|
||||
this._displayConfig.videoCenter = new screenEmu.Point(0.01, 0.02);
|
||||
// this._displayConfig.videoDecoder = 'CANVAS_CXA2025AS';
|
||||
this._sv.displayConfiguration = this._displayConfig;
|
||||
}
|
||||
|
||||
private _refresh() {
|
||||
doubleHiresMode = !an3 && hiresMode && _80colMode;
|
||||
|
||||
this._grs[0].refresh();
|
||||
this._grs[1].refresh();
|
||||
this._hgrs[0].refresh();
|
||||
this._hgrs[1].refresh();
|
||||
|
||||
if (this._displayConfig) {
|
||||
this._displayConfig.videoWhiteOnly = textMode || this._monoMode;
|
||||
this._sv.displayConfiguration = this._displayConfig;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
reset() {
|
||||
textMode = true;
|
||||
mixedMode = false;
|
||||
hiresMode = true;
|
||||
pageMode = 1;
|
||||
|
||||
_80colMode = false;
|
||||
altCharMode = false;
|
||||
|
||||
this._flag = 0;
|
||||
an3 = true;
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
text(on: boolean) {
|
||||
const old = textMode;
|
||||
textMode = on;
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
_80col(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = _80colMode;
|
||||
_80colMode = on;
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
altchar(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = altCharMode;
|
||||
altCharMode = on;
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
hires(on: boolean) {
|
||||
const old = hiresMode;
|
||||
hiresMode = on;
|
||||
if (!on) {
|
||||
this._flag = 0;
|
||||
}
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
an3(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = an3;
|
||||
an3 = on;
|
||||
|
||||
if (on) {
|
||||
this._flag = ((this._flag << 1) | (_80colMode ? 0x0 : 0x1)) & 0x3;
|
||||
}
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
doubleHires(on: boolean) {
|
||||
this.an3(!on);
|
||||
}
|
||||
|
||||
mixed(on: boolean) {
|
||||
const old = mixedMode;
|
||||
mixedMode = on;
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
page(pageNo: pageNo) {
|
||||
const old = pageMode;
|
||||
pageMode = pageNo;
|
||||
if (old != pageNo) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
enhanced(on: boolean) {
|
||||
enhanced = on;
|
||||
}
|
||||
|
||||
isText() {
|
||||
return textMode;
|
||||
}
|
||||
|
||||
isMixed() {
|
||||
return mixedMode;
|
||||
}
|
||||
|
||||
isPage2() {
|
||||
return pageMode == 2;
|
||||
}
|
||||
|
||||
isHires() {
|
||||
return hiresMode;
|
||||
}
|
||||
|
||||
isDoubleHires() {
|
||||
return doubleHiresMode;
|
||||
}
|
||||
|
||||
is80Col() {
|
||||
return _80colMode;
|
||||
}
|
||||
|
||||
isAltChar() {
|
||||
return altCharMode;
|
||||
}
|
||||
|
||||
updateImage(
|
||||
mainData: ImageData,
|
||||
mainDirty: Region,
|
||||
mixData?: ImageData | null,
|
||||
mixDirty?: Region | null
|
||||
) {
|
||||
let blitted = false;
|
||||
if (mainDirty.bottom !== -1 || (mixDirty && mixDirty.bottom !== -1)) {
|
||||
const imageData = buildScreen(mainData, mixData);
|
||||
const imageInfo = new screenEmu.ImageInfo(imageData);
|
||||
this._sv.image = imageInfo;
|
||||
blitted = true;
|
||||
}
|
||||
this._sv.vsync();
|
||||
return blitted;
|
||||
}
|
||||
|
||||
blit(altData?: ImageData) {
|
||||
let blitted = false;
|
||||
const hgr = this._hgrs[pageMode - 1];
|
||||
const gr = this._grs[pageMode - 1];
|
||||
|
||||
if (altData) {
|
||||
blitted = this.updateImage(
|
||||
altData,
|
||||
{ top: 0, left: 0, right: 560, bottom: 384 }
|
||||
);
|
||||
} else if (hiresMode && !textMode) {
|
||||
blitted = this.updateImage(
|
||||
hgr.imageData, hgr.dirty,
|
||||
mixedMode ? gr.imageData : null, mixedMode ? gr.dirty : null,
|
||||
);
|
||||
} else {
|
||||
blitted = this.updateImage(
|
||||
gr.imageData, gr.dirty
|
||||
);
|
||||
}
|
||||
hgr.dirty = {...notDirty};
|
||||
gr.dirty = {...notDirty};
|
||||
|
||||
return blitted;
|
||||
}
|
||||
|
||||
getState(): VideoModesState {
|
||||
return {
|
||||
grs: [this._grs[0].getState(), this._grs[1].getState()],
|
||||
hgrs: [this._hgrs[0].getState(), this._hgrs[1].getState()],
|
||||
textMode: textMode,
|
||||
mixedMode: mixedMode,
|
||||
hiresMode: hiresMode,
|
||||
pageMode: pageMode,
|
||||
_80colMode: _80colMode,
|
||||
altCharMode: altCharMode,
|
||||
an3: an3
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: VideoModesState) {
|
||||
textMode = state.textMode;
|
||||
mixedMode = state.mixedMode;
|
||||
hiresMode = state.hiresMode;
|
||||
pageMode = state.pageMode;
|
||||
_80colMode = state._80colMode;
|
||||
altCharMode = state.altCharMode;
|
||||
an3 = state.an3;
|
||||
|
||||
this._grs[0].setState(state.grs[0]);
|
||||
this._grs[1].setState(state.grs[1]);
|
||||
this._hgrs[0].setState(state.hgrs[0]);
|
||||
this._hgrs[1].setState(state.hgrs[1]);
|
||||
}
|
||||
|
||||
mono(on: boolean) {
|
||||
this._grs[0].mono(on);
|
||||
this._grs[1].mono(on);
|
||||
this._hgrs[0].mono(on);
|
||||
this._hgrs[1].mono(on);
|
||||
|
||||
this._monoMode = on;
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
getText() {
|
||||
return this._grs[pageMode - 1].getText();
|
||||
}
|
||||
}
|
27
js/main2.js
27
js/main2.js
@ -59,8 +59,9 @@ switch (romVersion) {
|
||||
}
|
||||
|
||||
var options = {
|
||||
canvas: document.getElementById('screen'),
|
||||
gl: prefs.readPref('gl_canvas', 'true') === 'true',
|
||||
screen: [],
|
||||
multiScreen: false,
|
||||
rom: rom,
|
||||
characterRom: characterRom,
|
||||
e: false,
|
||||
@ -69,28 +70,6 @@ var options = {
|
||||
tick: updateUI
|
||||
};
|
||||
|
||||
var canvas1 = document.getElementById('screen');
|
||||
var canvas2 = document.getElementById('screen2');
|
||||
var canvas3 = document.getElementById('screen3');
|
||||
var canvas4 = document.getElementById('screen4');
|
||||
|
||||
options.screen[0] = canvas1.getContext('2d');
|
||||
if (canvas4) {
|
||||
options.multiScreen = true;
|
||||
options.screen[1] = canvas2.getContext('2d');
|
||||
options.screen[2] = canvas3.getContext('2d');
|
||||
options.screen[3] = canvas4.getContext('2d');
|
||||
} else if (canvas2) {
|
||||
options.multiScreen = true;
|
||||
options.screen[1] = options.screen[0];
|
||||
options.screen[2] = canvas2.getContext('2d');
|
||||
options.screen[3] = options.screen[2];
|
||||
} else {
|
||||
options.screen[1] = options.screen[0];
|
||||
options.screen[2] = options.screen[0];
|
||||
options.screen[3] = options.screen[0];
|
||||
}
|
||||
|
||||
var apple2 = new Apple2(options);
|
||||
var cpu = apple2.getCPU();
|
||||
var io = apple2.getIO();
|
||||
@ -99,7 +78,7 @@ var printer = new Printer('#printer-modal .paper');
|
||||
|
||||
var lc = new LanguageCard(io, rom);
|
||||
var parallel = new Parallel(io, printer);
|
||||
var videoTerm = new VideoTerm(io, options.screen[0]);
|
||||
var videoTerm = new VideoTerm(io);
|
||||
var slinky = new RAMFactor(io, 1024 * 1024);
|
||||
var disk2 = new DiskII(io, driveLights, sectors);
|
||||
var clock = new Thunderclock(io);
|
||||
|
26
js/main2e.js
26
js/main2e.js
@ -42,8 +42,8 @@ switch (romVersion) {
|
||||
}
|
||||
|
||||
var options = {
|
||||
screen: [],
|
||||
multiScreen: false,
|
||||
gl: prefs.readPref('gl_canvas') === 'true',
|
||||
canvas: document.getElementById('screen'),
|
||||
rom: rom,
|
||||
characterRom: characterRom,
|
||||
e: true,
|
||||
@ -52,28 +52,6 @@ var options = {
|
||||
tick: updateUI
|
||||
};
|
||||
|
||||
var canvas1 = document.getElementById('screen');
|
||||
var canvas2 = document.getElementById('screen2');
|
||||
var canvas3 = document.getElementById('screen3');
|
||||
var canvas4 = document.getElementById('screen4');
|
||||
|
||||
options.screen[0] = canvas1.getContext('2d');
|
||||
if (canvas4) {
|
||||
options.multiScreen = true;
|
||||
options.screen[1] = canvas2.getContext('2d');
|
||||
options.screen[2] = canvas3.getContext('2d');
|
||||
options.screen[3] = canvas4.getContext('2d');
|
||||
} else if (canvas2) {
|
||||
options.multiScreen = true;
|
||||
options.screen[1] = options.screen[0];
|
||||
options.screen[2] = canvas2.getContext('2d');
|
||||
options.screen[3] = options.screen[2];
|
||||
} else {
|
||||
options.screen[1] = options.screen[0];
|
||||
options.screen[2] = options.screen[0];
|
||||
options.screen[3] = options.screen[0];
|
||||
}
|
||||
|
||||
var apple2 = new Apple2(options);
|
||||
var io = apple2.getIO();
|
||||
var cpu = apple2.getCPU();
|
||||
|
@ -14,7 +14,7 @@ import RAM from './ram';
|
||||
import { debug, toHex } from './util';
|
||||
import { byte, Memory } from './types';
|
||||
import Apple2IO from './apple2io';
|
||||
import { HiresPage, LoresPage, VideoModes } from './canvas';
|
||||
import { HiresPage, LoresPage, VideoModes } from './videomodes';
|
||||
|
||||
/*
|
||||
* I/O Switch locations
|
||||
|
14
js/prefs.ts
14
js/prefs.ts
@ -16,13 +16,17 @@ export default class Prefs {
|
||||
havePrefs() {
|
||||
return havePrefs;
|
||||
}
|
||||
readPref(name: string): string | null {
|
||||
if (havePrefs)
|
||||
return window.localStorage.getItem(name);
|
||||
return null;
|
||||
|
||||
readPref(name: string, defaultValue: string | null = null) {
|
||||
if (havePrefs) {
|
||||
return window.localStorage.getItem(name) ?? defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
writePref(name: string, value: string) {
|
||||
if (havePrefs)
|
||||
if (havePrefs) {
|
||||
window.localStorage.setItem(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
js/types.ts
21
js/types.ts
@ -1,7 +1,7 @@
|
||||
|
||||
/**
|
||||
* Extracts the members of a constant array as a type. Used as:
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const SOME_VALUES = ['a', 'b', 1, 2] as const;
|
||||
* type SomeValues = MemberOf<typeof SOME_VALUES>; // 'a' | 'b' | 1 | 2
|
||||
@ -41,6 +41,21 @@ export interface Memory {
|
||||
write(page: byte, offset: byte, value: byte): void;
|
||||
}
|
||||
|
||||
/* An interface card */
|
||||
export interface Card extends Memory {
|
||||
/* Reset the card */
|
||||
reset(): void;
|
||||
|
||||
/* Draw card to canvas */
|
||||
blit?(): ImageData;
|
||||
|
||||
/* Process period events */
|
||||
tick?(): void;
|
||||
|
||||
/* Read or Write an I/O switch */
|
||||
ioSwitch(off: byte, val?: byte): byte | undefined;
|
||||
}
|
||||
|
||||
export const DISK_FORMATS = [
|
||||
'2mg',
|
||||
'd13',
|
||||
@ -70,6 +85,8 @@ export interface DiskIIDrive extends Drive {
|
||||
dirty: boolean,
|
||||
}
|
||||
|
||||
export type TapeData = Array<[duration: number, high: boolean]>;
|
||||
|
||||
export interface Restorable<T> {
|
||||
getState(): T;
|
||||
setState(state: T): void;
|
||||
@ -79,4 +96,4 @@ export interface Restorable<T> {
|
||||
export type TypedArrayMutableProperties = 'copyWithin' | 'fill' | 'reverse' | 'set' | 'sort';
|
||||
export interface ReadonlyUint8Array extends Omit<Uint8Array, TypedArrayMutableProperties> {
|
||||
readonly [n: number]: number
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +164,9 @@ function loadingStop () {
|
||||
MicroModal.close('loading-modal');
|
||||
|
||||
if (!paused) {
|
||||
_apple2.run();
|
||||
vm.ready.then(() => {
|
||||
_apple2.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -704,22 +706,23 @@ function _keyup(evt) {
|
||||
}
|
||||
|
||||
export function updateScreen() {
|
||||
var green = document.querySelector('#green_screen').checked;
|
||||
var mono = document.querySelector('#mono_screen').checked;
|
||||
var scanlines = document.querySelector('#show_scanlines').checked;
|
||||
var gl = document.querySelector('#gl_canvas').checked;
|
||||
|
||||
var screen = document.querySelector('#screen');
|
||||
var overscan = document.querySelector('.overscan');
|
||||
if (scanlines) {
|
||||
if (scanlines && !gl) {
|
||||
overscan.classList.add('scanlines');
|
||||
} else {
|
||||
overscan.classList.remove('scanlines');
|
||||
}
|
||||
if (green) {
|
||||
screen.classList.add('green');
|
||||
if (mono && !gl) {
|
||||
screen.classList.add('mono');
|
||||
} else {
|
||||
screen.classList.remove('green');
|
||||
screen.classList.remove('mono');
|
||||
}
|
||||
vm.mono(green);
|
||||
vm.mono(mono);
|
||||
}
|
||||
|
||||
export function updateCPU() {
|
||||
@ -780,7 +783,9 @@ function _mousemove(evt) {
|
||||
export function pauseRun() {
|
||||
var label = document.querySelector('#pause-run i');
|
||||
if (paused) {
|
||||
_apple2.run();
|
||||
vm.ready.then(() => {
|
||||
_apple2.run();
|
||||
});
|
||||
label.classList.remove('fa-play');
|
||||
label.classList.add('fa-pause');
|
||||
} else {
|
||||
@ -921,6 +926,8 @@ export function initUI(apple2, disk2, smartPort, printer, e) {
|
||||
_apple2.stop();
|
||||
processHash(hash);
|
||||
} else {
|
||||
_apple2.run();
|
||||
vm.ready.then(() => {
|
||||
_apple2.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
75
js/videomodes.ts
Normal file
75
js/videomodes.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Memory, Restorable, byte } from './types';
|
||||
|
||||
export type bank = 0 | 1;
|
||||
export type pageNo = 1 | 2;
|
||||
|
||||
export interface Color {
|
||||
0: byte, // red
|
||||
1: byte, // green
|
||||
2: byte, // blue
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
top: number,
|
||||
bottom: number,
|
||||
left: number,
|
||||
right: number,
|
||||
}
|
||||
|
||||
export interface GraphicsState {
|
||||
page: byte;
|
||||
mono: boolean;
|
||||
buffer: string[];
|
||||
}
|
||||
|
||||
export interface VideoModesState {
|
||||
grs: [gr1: GraphicsState, gr2: GraphicsState],
|
||||
hgrs: [hgr1: GraphicsState, hgr2: GraphicsState],
|
||||
textMode: boolean,
|
||||
mixedMode: boolean,
|
||||
hiresMode: boolean,
|
||||
pageMode: pageNo,
|
||||
_80colMode: boolean,
|
||||
altCharMode: boolean,
|
||||
an3: boolean,
|
||||
}
|
||||
|
||||
export interface VideoPage extends Memory, Restorable<GraphicsState> {
|
||||
imageData: ImageData
|
||||
dirty: Region;
|
||||
|
||||
bank0(): Memory
|
||||
bank1(): Memory
|
||||
|
||||
mono: (on: boolean) => void
|
||||
refresh: () => void
|
||||
}
|
||||
|
||||
export interface LoresPage extends VideoPage {
|
||||
getText: () => string
|
||||
}
|
||||
|
||||
export interface HiresPage extends VideoPage {
|
||||
|
||||
}
|
||||
|
||||
export interface VideoModes extends Restorable<VideoModesState> {
|
||||
page(pageNo: number): void
|
||||
|
||||
blit(altData?: ImageData): boolean
|
||||
|
||||
reset(): void
|
||||
|
||||
_80col(on: boolean): void
|
||||
altchar(on: boolean): void
|
||||
doubleHires(on: boolean): void
|
||||
enhanced(on: boolean): void
|
||||
|
||||
is80Col(): boolean
|
||||
isAltChar(): boolean
|
||||
isDoubleHires(): boolean
|
||||
isHires(): boolean
|
||||
isMixed(): boolean
|
||||
isPage2(): boolean
|
||||
isText(): boolean
|
||||
}
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -5,10 +5,10 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "apple2js",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"apple2shader": "whscullin/apple2shader",
|
||||
"micromodal": "^0.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -3534,6 +3534,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/apple2shader": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "git+ssh://git@github.com/whscullin/apple2shader.git#e87a445148bbcc7f4dbfa2609902071631f96bbc",
|
||||
"integrity": "sha512-nk+2iHnKtWHsUvRFiG77x5A5GmScQKc+MHDJOFB6GYmqTomhakyPdfw1Dkupj+iDQc6EmelUgbnF/weZoTtQ1Q==",
|
||||
"license": "GPL-2.0"
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
@ -20734,6 +20740,11 @@
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"apple2shader": {
|
||||
"version": "git+ssh://git@github.com/whscullin/apple2shader.git#e87a445148bbcc7f4dbfa2609902071631f96bbc",
|
||||
"integrity": "sha512-nk+2iHnKtWHsUvRFiG77x5A5GmScQKc+MHDJOFB6GYmqTomhakyPdfw1Dkupj+iDQc6EmelUgbnF/weZoTtQ1Q==",
|
||||
"from": "apple2shader@whscullin/apple2shader"
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
|
@ -44,6 +44,7 @@
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"apple2shader": "whscullin/apple2shader",
|
||||
"micromodal": "^0.4.2"
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,11 @@
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
"src/types/*"
|
||||
"types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"js/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
3
types/apple2shader.d.ts
vendored
Normal file
3
types/apple2shader.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare module 'apple2shader';
|
||||
|
||||
declare const apple2shader: any;
|
Loading…
Reference in New Issue
Block a user