PHP Classes

File: public_html/vendor/riptermjs/BGI.js

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   public_html/vendor/riptermjs/BGI.js   Download  
File: public_html/vendor/riptermjs/BGI.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 101,824 bytes
 

Contents

Class file image Download
/** * BGI.js * Copyright (c) 2020-2026 Carl Gorringe * https://carl.gorringe.org * https://github.com/cgorringe/RIPtermJS * * Borland Graphics Interface (BGI) for JavaScript * https://en.wikipedia.org/wiki/Borland_Graphics_Interface * * This is a subset of the library which emulates only some BGI functions. * * Used source code from: * https://github.com/genpfault/sdl-bgi * https://github.com/buhtu4ek/OsXBGI * * BGI Docs: * https://www.cs.colorado.edu/~main/bgi/doc/ * http://math.ubbcluj.ro/~sberinde/wingraph/ * **/ /** * This Source Code is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. **/ // not using as a module for now, but // uncomment this line if you do want to use as a module: //export default class BGI { // class static constants moved to end of file. // don't want both below... // packed ARGB values (0xAARRGGBB) in C++ style static get bgi_palette () { return [ 0xFF000000, // 00 BLACK 0xFF0000AA, // 01 BLUE 0xFF00AA00, // 02 GREEN 0xFF00AAAA, // 03 CYAN 0xFFAA0000, // 04 RED 0xFFAA00AA, // 05 MAGENTA 0xFFAA5500, // 06 BROWN 0xFFAAAAAA, // 07 LIGHTGRAY 0xFF555555, // 08 DARKGRAY 0xFF5555FF, // 09 LIGHTBLUE 0xFF55FF55, // 0A LIGHTGREEN 0xFF55FFFF, // 0B LIGHTCYAN 0xFFFF5555, // 0C LIGHTRED 0xFFFF55FF, // 0D LIGHTMAGENTA 0xFFFFFF55, // 0E YELLOW 0xFFFFFFFF // 0F WHITE ]; } // RGBA32 format: R, G, B, A static get ega_palette () { return [ 0x00, 0x00, 0x00, 0xFF, // 00 BLACK 0x00, 0x00, 0xAA, 0xFF, // 01 BLUE 0x00, 0xAA, 0x00, 0xFF, // 02 GREEN 0x00, 0xAA, 0xAA, 0xFF, // 03 CYAN 0xAA, 0x00, 0x00, 0xFF, // 04 RED 0xAA, 0x00, 0xAA, 0xFF, // 05 MAGENTA 0xAA, 0x55, 0x00, 0xFF, // 06 BROWN 0xAA, 0xAA, 0xAA, 0xFF, // 07 LIGHTGRAY 0x55, 0x55, 0x55, 0xFF, // 08 DARKGRAY 0x55, 0x55, 0xFF, 0xFF, // 09 LIGHTBLUE 0x55, 0xFF, 0x55, 0xFF, // 0A LIGHTGREEN 0x55, 0xFF, 0xFF, 0xFF, // 0B LIGHTCYAN 0xFF, 0x55, 0x55, 0xFF, // 0C LIGHTRED 0xFF, 0x55, 0xFF, 0xFF, // 0D LIGHTMAGENTA 0xFF, 0xFF, 0x55, 0xFF, // 0E YELLOW 0xFF, 0xFF, 0xFF, 0xFF, // 0F WHITE ]; } static get line_patterns () { return [ 0xFFFF, // 00 SOLID_LINE : 1111111111111111 0xCCCC, // 01 DOTTED_LINE : 1100110011001100 0xFC78, // 02 CENTER_LINE : 1111110001111000 0xF8F8, // 03 DASHED_LINE : 1111100011111000 0xFFFF, // 04 USERBIT_LINE ]; } static get fill_patterns () { return [ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // 00 EMPTY_FILL [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], // 01 SOLID_FILL [0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00], // 02 LINE_FILL [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80], // 03 LTSLASH_FILL [0xE0, 0xC1, 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70], // 04 SLASH_FILL [0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x87, 0xC3, 0xE1], // 05 BKSLASH_FILL [0xA5, 0xD2, 0x69, 0xB4, 0x5A, 0x2D, 0x96, 0x4B], // 06 LTBKSLASH_FILL [0xFF, 0x88, 0x88, 0x88, 0xFF, 0x88, 0x88, 0x88], // 07 HATCH_FILL [0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81], // 08 XHATCH_FILL [0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33], // 09 INTERLEAVE_FILL [0x80, 0x00, 0x08, 0x00, 0x80, 0x00, 0x08, 0x00], // 0A WIDE_DOT_FILL [0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00], // 0B CLOSE_DOT_FILL [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55], // 0C USER_FILL / PabloDraw ]; } //////////////////////////////////////////////////////////////////////////////// // Contructor & init methods // Pass an object to constructor with these keys: // ctx: CanvasRenderingContext2D() - required // width: int - optional if ctx derived from a <canvas> // height: int - optional if ctx derived from a <canvas> // log: function (type, msg) - optional callback function with 2 arguments. constructor (args) { // public fields this.width = 1; this.height = 1; this.aspect = { xasp: 1, yasp: 1 }; // use 372, 480 for RipTerm this.isBuffered = true; // true = copy pixels to context, false = using context data store this.colorMask = 0x0F; // 0xF = 16-color mode, 0xFF = 256-color mode this.fonts = {}; // key is file, e.g. 'GOTH.CHR' (may rethink this later) this.mouseX = 0; this.mouseY = 0; // index is font size: 0=8x8, 1=7x8, 2=8x14, 3=7x14, 4=16x14. // value is an array of 256 elements for each ASCII char, // each element is an array of ints, each being a scanline of height values. // e.g. this.bitfonts[0][65][2] is font size 0, ASCII char 65, 3rd scanline which returns an 8-bit int. this.bitfonts = []; this.stateStack = []; this.icons = {}; // key is uppercased file+ext, e.g. 'EXAMPLE.ICN', value is image obj used in putimage(). this.svgActive = false; // log callback function if ('log' in args) { this.logFunc = args.log; } // need to pass this in to find font files if ('fontsPath' in args) { this.fontsPath = args.fontsPath; } // need to pass this in to find icon files if ('iconsPath' in args) { this.iconsPath = args.iconsPath; } // set to 'window' to bind class methods to global namespace if ('root' in args) { this.initRoot(args.root); } this.initContext(args.ctx, args.width, args.height); // which assigns these: // this.ctx : CanvasRenderingContext2D() // this.pixels : Uint8ClampedArray() // this.imgData : ImageData() // this.width, this.height, this.isBuffered this.graphdefaults(); } infoDefaults () { return { bgcolor: BGI.BLACK, fgcolor: BGI.WHITE, // need to set fill.fpattern whenever fill.style is set fill: { style: BGI.SOLID_FILL, color: BGI.WHITE, fpattern: BGI.fill_patterns[BGI.SOLID_FILL] }, line: { style: BGI.SOLID_LINE, upattern: 0xFFFF, thickness: 1 }, cp: { x: 0, y: 0 }, // current position vp: { left: 0, top: 0, // viewport right: (this.width - 1), bottom: (this.height - 1), width: this.width, height: this.height, clip: false }, // ap: { left: 0, top: 0, right: (this.width - 1), bottom: (this.height - 1), clip: 0 }, // active port / page? writeMode: 0, // 0=COPY, 1=XOR, 2=OR, 3=AND, 4=NOT font: { width: 0, height: 0 }, // ?? fontMag: { x: 1.0, y: 1.0 }, // font magnification text: { font: 0, direction: 0, charsize: 0, horiz: 0, vert: 0 }, }; } // Initializes BGI with a canvas context. [not in BGI] // ctx comes from canvas.getContext('2d'). // ctx can come from canvas.getContext('2d', { pixelFormat: 'A8' }) if using a node canvas. // width & height are optional if ctx comes from a browser canvas. /* // Example browser code: const canvas = document.getElementById('canvas') const ctx = canvas.getContext('2d') // ctx in RGBA32 format uses Uint8ClampedArray 4 bytes-per-pixel. // Example node code: const { createCanvas, loadImage } = require('canvas') const canvas = createCanvas(640, 350) const ctx = canvas.getContext('2d', { pixelFormat: 'A8' }) // ctx is indexed Uint8ClampedArray 1 byte-per-pixel. */ initContext (ctx, width, height) { this.ctx = ctx if (width && height) { // used provided size this.width = Math.floor(width); this.height = Math.floor(height); } else if (ctx && ctx.canvas) { // get size from browser canvas this.width = Math.floor(ctx.canvas.width); this.height = Math.floor(ctx.canvas.height); } else { this.log('err', 'Missing "ctx" canvas context passed to BGI() constructor!'); return; } // needed to access context pixel data ?? //this.imgData = ctx.getImageData(0, 0, this.width, this.height); this.imgData = ctx.createImageData(this.width, this.height); // Do we need to use this for 'A8' pixel format? // this.imgData = new ImageData(new Uint8ClampedArray(size), width, height) // check to see if ctx is already using 8-bit indexed pixel data. // only works in Chrome & Edge, not Firefox nor Safari. if (typeof ctx.getContextAttributes === 'function') { let attr = ctx.getContextAttributes(); this.isBuffered = (attr && (attr.pixelFormat === 'A8')) ? false : true; /* && this.imgData && this.imgData.data */ } else { this.isBuffered = true; } if (this.isBuffered) { // create a new byte buffer for pixel data, which is usually the case in browsers. this.pixels = new Uint8ClampedArray(this.width * this.height) } else { // use existing context data, which may be used in node canvas. [NOT TESTED] // does this a reference or a copy? this.pixels = this.imgData.data; } // this pixel buffer is used during flood fill this.fillpixels = new Uint8ClampedArray(this.width * this.height) // setup mousemove handler to update mouse positions whenever the mouse moves. this.registermousehandler(0, (function(x, y) { this.mouseX = x; this.mouseY = y; }).bind(this)); } // Loads all the vector font .CHR files in BGI.fontFileList[] async initFonts () { // load vector fonts await Promise.all(BGI.fontFileList.map(async fname => { if (fname.length > 3) { this.fonts[fname] = await this.fetchCHRFont(fname); } })); // load bitmap fonts // index is font size: 0=8x8, 1=7x8, 2=8x14, 3=7x14, 4=16x14. this.bitfonts[0] = await this.fetchPNGFont('8x8.png', 8, 8); this.bitfonts[1] = await this.fetchPNGFont('w1-7x8.png', 7, 8); this.bitfonts[2] = await this.fetchPNGFont('8x14.png', 8, 14); this.bitfonts[3] = await this.fetchPNGFont('w3-7x14.png', 7, 14); this.bitfonts[4] = await this.fetchPNGFont('w4-16x14.png', 16, 14); } // TODO: rethink this or replace // int width, int height, const char* title="Windows BGI", int left=0, int top=0, bool dbflag=false, bool closeflag=true // return value is an id to later use in setcurrentwindow() initwindow (width, height) { /* // do I need "this" ? this._imageData = new ImageData(width, height); this.pixels = new Uint8ClampedArray(width * height); this.info.max.x = width - 1; this.info.max.y = height - 1; */ // don't need these here, as they're reset in graphdefaults() //info.vp = { x1:0, y1:0, x2:0, y2:0, clip:0 }; //info.ap = { x1:0, y1:0, x2:0, y2:0, clip:0 }; this.graphdefaults(); return 0; } // (EXPERIMENTAL) // Assign functions to root window so that they may be called directly // without needing the object part. initRoot (root = window) { // TODO: Should rename some methods in BGI class to begin with '_' to exclude from root. // (e.g. 'log' ==> '_log') // TODO: Redo BGI constants so that they may be included in root. // enumerate all the methods in this class that we want to bind to root let proto = Object.getPrototypeOf(this); Object.getOwnPropertyNames(proto).forEach(name => { if ((name !== 'constructor') && (name !== 'log') && (name.startsWith('_') === false)) { root[name] = this[name].bind(this); } }); } //////////////////////////////////////////////////////////////////////////////// // Extra methods // sends msg to provided log function, else send to console if none provided. log (type, msg) { if (typeof this.logFunc === 'function') { this.logFunc(type, msg); } else { if (type === 'err') { console.error(msg); } else { console.log(msg); } } } unimplemented (method) { this.log('bgi', method + '() not implemented.'); } // updates the screen refresh (defaults = {}) { const { ctx = this.ctx, img = this.imgData, pix = this.pixels, pal = this.palette, isBuffered = this.isBuffered, } = defaults; if (!(ctx && img && pix && pal)) { return false; } // copy pixel buffer to img if (isBuffered) { // copy 8-bit pixels to 32-bit imgData const numpix = pix.length; let pi = 0; for (let i=0, j=0; i < numpix; i++, j+=4) { pi = pix[i] * 4; img.data[j + 0] = pal[pi + 0]; // R img.data[j + 1] = pal[pi + 1]; // G img.data[j + 2] = pal[pi + 2]; // B img.data[j + 3] = pal[pi + 3]; // A } } /* else { // using 8-bit imgData (NOT TESTED) let numpix = pix.length; for (let i=0; i < numpix; i++) { img.data[i] = pix[i]; } // assumes this.pixels point to this.imgData.data //ctx.putImageData(this.imgData, 0, 0); } */ ctx.putImageData(img, 0, 0); return true; } // conditionally refreshes the screen or schedule it update () { // TODO this.refresh(); } // saves this.info to a stack pushState () { this.stateStack.push( JSON.parse(JSON.stringify(this.info)) ); } // restores this.info from a stack popState () { if (this.stateStack.length > 0) { this.info = this.stateStack.pop(); } } // Rearranges colors in the given palette applying NOT of index. // returns a new palette. invertedPalette (defaults = {}) { const { pal = this.palette, mask = this.colorMask, } = defaults; const plength = (mask + 1) * 4; let pal2 = Array(plength).fill(0); let pi1=0, pi2=0; for (let c=0; c <= mask; c++) { pi1 = c * 4; pi2 = (~c & mask) * 4; // RGBA32 format: R, G, B, A (0-255) pal2[pi2 + 0] = pal[pi1 + 0]; pal2[pi2 + 1] = pal[pi1 + 1]; pal2[pi2 + 2] = pal[pi1 + 2]; pal2[pi2 + 3] = pal[pi1 + 3]; } return pal2; } //////////////////////////////////////////////////////////////////////////////// // Drawing methods // Draws a pixel offset and clipped by current viewport. putpixel (x, y, color = this.info.fgcolor, wmode = this.info.writeMode) { this._putpixel(x, y, color, wmode); } _putpixel (x, y, color = this.info.fgcolor, wmode = this.info.writeMode, buf = this.pixels) { // should these be using .floor() ? x = Math.round(x); y = Math.round(y); const vp = this.info.vp; x += vp.left; y += vp.top; if (!vp.clip || ((x >= vp.left) && (x <= vp.right) && (y >= vp.top) && (y <= vp.bottom))) { this._putpixel_abs(x, y, color, wmode, buf); } } // x,y in absolute coordinates _putpixel_abs (x, y, color = this.info.fgcolor, wmode = this.info.writeMode, buf = this.pixels) { if ((x >= 0) && (x < this.width) && (y >= 0) && (y < this.height)) { switch (wmode) { default: case BGI.COPY_PUT: buf[y * this.width + x] = color; break; case BGI.XOR_PUT: buf[y * this.width + x] ^= color; break; case BGI.OR_PUT: buf[y * this.width + x] |= color; break; case BGI.AND_PUT: buf[y * this.width + x] &= color; break; case BGI.NOT_PUT: buf[y * this.width + x] = (~color & this.colorMask); } } } // floodfill putpixel using fill pattern. // uses info.fill.color, info.bgcolor, and info.fill.fpattern that must be pre-set. // pixel will be offset and clipped by current viewport. ff_putpixel (x, y, color = this.info.fill.color, wmode = this.info.writeMode) { const bgcolor = this.info.bgcolor; const fpattern = this.info.fill.fpattern; // array const vp = this.info.vp; x += vp.left; y += vp.top; if (!vp.clip || ((x >= vp.left) && (x <= vp.right) && (y >= vp.top) && (y <= vp.bottom))) { // draw pixel if bit in pattern is 1 if ( (fpattern[y % 8] >> (7 - (x % 8))) & 1) { // forground pixel this._putpixel_abs(x, y, color, wmode); } else { // background pixel this._putpixel_abs(x, y, bgcolor, wmode); } } } thick_putpixel (x, y, color = this.info.fgcolor, wmode = this.info.writeMode) { // could be more efficient //this._putpixel(x-1, y-1, color, wmode); this._putpixel(x , y-1, color, wmode); //this._putpixel(x+1, y-1, color, wmode); this._putpixel(x-1, y , color, wmode); this._putpixel(x , y , color, wmode); this._putpixel(x+1, y , color, wmode); //this._putpixel(x-1, y+1, color, wmode); this._putpixel(x , y+1, color, wmode); //this._putpixel(x+1, y+1, color, wmode); /* // TESTing this._putpixel(x-1, y-1, color, wmode); this._putpixel(x , y-1, color, wmode); this._putpixel(x+1, y-1, color, wmode); this._putpixel(x-1, y , color, wmode); //this._putpixel(x , y , color, wmode); this._putpixel(x+1, y , color, wmode); this._putpixel(x-1, y+1, color, wmode); this._putpixel(x , y+1, color, wmode); this._putpixel(x+1, y+1, color, wmode); */ } // NOT USED circle_bresenham (cx, cy, radius, thickness = this.info.line.thickness) { const putpix = (thickness === 3) ? (a, b) => this.thick_putpixel(a, b) : (a, b) => this._putpixel(a, b); let xx = -radius, yy = 0, err = 2 - (2 * radius); do { putpix(cx - xx, cy + yy); putpix(cx - yy, cy - xx); putpix(cx + xx, cy - yy); putpix(cx + yy, cy + xx); radius = err; if (radius <= yy) { err += ++yy * 2 + 1; } if ((radius > xx) || (err > yy)) { err += ++xx * 2 + 1; } } while (xx < 0); } // Bresenham's ellipse algorithm ellipse_bresenham (cx, cy, xradius, yradius, thickness = this.info.line.thickness) { // FIXME: thin ellipses not drawn correctly in C_WELL.RIP //this.log('bgi', `ellipse_bresenham: ${cx}, ${cy}, ${xradius}, ${yradius}, ${thickness}`); // DEBUG //console.log('ellipse_bresenham') // DEBUG if (xradius < 1) { xradius = 1; } if (yradius < 1) { yradius = 1; } // TEST: adjusting thick ellipse /* if (thickness === 3) { xradius -= 0.5; cx -= 0.5; yradius -= 0.5; // -0.5 //cy += 0.5; } */ const putpix = (thickness === 3) ? (a, b) => this.thick_putpixel(a, b) : (a, b) => this._putpixel(a, b); const xrad2 = 2 * xradius * xradius; const yrad2 = 2 * yradius * yradius; let x = -xradius, y = 0; let e2 = yradius, dx = (1+2*x)*e2*e2; //let e2 = yradius, dx = (2*x)*e2*e2; // TEST let dy = x*x, err = dx+dy; //this.log('bgi', 'ellipse_bresenham TEST2'); // DEBUG // draw x-axis pixels twice to correct XOR_PUT mode putpix(cx - x, cy); putpix(cx + x, cy); do { putpix(cx - x, cy + y); putpix(cx + x, cy + y); putpix(cx + x, cy - y); putpix(cx - x, cy - y); e2 = err << 1; if (e2 <= dy) { y++; dy += xrad2; err += dy; } // y step //if (e2 < dy) { y++; dy += xrad2; err += dy; } // y step TEST if (e2 >= dx || (err << 1) > dy) { x++; dx += yrad2; err += dx; } // x step //if (e2 >= dx || (2 * err) >= dy) { x++; dx += yrad2; err += dx; } // x step TEST } while (x < 0); // finish tip of ellipse (keep) while (y <= yradius) { putpix(cx, cy + y); putpix(cx, cy - y); y++; } } // FOR TESTING ONLY // does poorly on tall ellipses, and draws extra points. // ellipse_naive8 (cx, cy, xradius, yradius, thickness = this.info.line.thickness) { //this.log('bgi', `ellipse_naive8: ${cx}, ${cy}, ${xradius}, ${yradius}, ${thickness}`); // DEBUG const putpix = (thickness === 3) ? (a, b) => this.thick_putpixel(a, b) : (a, b) => this._putpixel(a, b); const xrad2 = xradius * xradius; const yasp = yradius / xradius; let x = 0, y = 0, xe = 0, ye = 0; do { y = Math.sqrt(xrad2 - (x * x)); ye = y * yasp; xe = x * yasp; putpix(cx + x, cy + ye); putpix(cx + y, cy + xe); putpix(cx - x, cy + ye); putpix(cx - y, cy + xe); putpix(cx + x, cy - ye); putpix(cx + y, cy - xe); putpix(cx - x, cy - ye); putpix(cx - y, cy - xe); x += 1; } while (x < y) } // Bresenham's ellipse algorithm (alt implementation) // Trying to make this match RipTerm original, even if it's worse. // // Using this article to help develop this: // https://funloop.org/post/2021-03-15-bresenham-circle-drawing-algorithm.html // ellipse_bresenham2 (cx, cy, xradius, yradius, thickness = this.info.line.thickness) { // if not a circle, call the old function instead if (xradius !== yradius) { this.ellipse_bresenham(cx, cy, xradius, yradius, thickness); return; } //this.log('bgi', `ellipse_bresenham2: ${cx}, ${cy}, ${xradius}, ${yradius}, ${thickness}`); // DEBUG // only works for circles for now // (matches single thickness circles) // elipse equation is: // (x^2 / xrad^2) + (y^2 / yrad^2) = 1 const putpix = (thickness === 3) ? (a, b) => this.thick_putpixel(a, b) : (a, b) => this._putpixel(a, b); const r = xradius; let x = 0, y = -yradius, fm = 1 - r, de = 3, dne = -(r << 1) + 5; do { putpix(cx + x, cy + y); putpix(cx + y, cy + x); putpix(cx - x, cy + y); putpix(cx - y, cy + x); putpix(cx + x, cy - y); putpix(cx + y, cy - x); putpix(cx - x, cy - y); putpix(cx - y, cy - x); if (fm < 0) { fm += de; dne += 2; } else { fm += dne; dne += 4; y++; } de += 2; x++; } while (x < -y); } // Draw an elliptical arc using Bresenham's algorithm. // stangle and endangle are in degrees counterclockwise, 0=right, 90=up... // NEW - TESTs show better than others // (may run slower than past attempts) arc_bresenham (cx, cy, stangle, endangle, xradius, yradius, thickness = this.info.line.thickness) { //this.log('bgi', `arc_bresenham: ${cx}, ${cy}, ${stangle}, ${endangle}, ${xradius}, ${yradius}, ${thickness}`); // DEBUG if (xradius < 1) { xradius = 1; } if (yradius < 1) { yradius = 1; } let asp_ratio = (yradius / xradius); const DEG_OVER_PI = 180.0 / Math.PI; //console.log('asp_ratio: ' + asp_ratio); // DEBUG // TODO: this is really close // this causes the overall arc drawing to slow down, wish we could speed it up let isInArc = (x, y) => { var angle = Math.floor(Math.atan2(cy - y, Math.round((x - cx) * asp_ratio)) * DEG_OVER_PI); if (angle < 0) { angle += 360 } // angle += 2 * Math.PI //console.log('angle:' + angle + ' start:' + stangle + ' end:' + endangle) // DEBUG return ((angle >= stangle) && (angle < endangle)) || (angle <= endangle - 360); } const putpix = (thickness === 3) ? (a, b) => this.thick_putpixel(a, b) : (a, b) => this._putpixel(a, b); const xrad2 = 2 * xradius * xradius; const yrad2 = 2 * yradius * yradius; let x = -xradius, y = 0; let e2 = yradius, dx = (1+2*x)*e2*e2; let dy = x*x, err = dx+dy; // draw x-axis pixels twice to correct XOR_PUT mode if (isInArc(cx - x, cy)) { putpix(cx - x, cy) } if (isInArc(cx + x, cy)) { putpix(cx + x, cy) } do { /* let points = [ [cx - x, cy + y], [cx + x, cy + y], [cx + x, cy - y], [cx - x, cy - y] ]; for (let [px, py] of points) { if (isInArc(px, py)) { putpix(px, py) } //putpix(px, py) } */ if (isInArc(cx - x, cy + y)) { putpix(cx - x, cy + y) } if (isInArc(cx + x, cy + y)) { putpix(cx + x, cy + y) } if (isInArc(cx + x, cy - y)) { putpix(cx + x, cy - y) } if (isInArc(cx - x, cy - y)) { putpix(cx - x, cy - y) } e2 = 2 * err; if (e2 <= dy) { y++; dy += xrad2; err += dy; } // y step if (e2 >= dx || (2 * err) > dy) { x++; dx += yrad2; err += dx; } // x step } while (x < 0); // finish tip of ellipse (keep) while (y <= yradius) { if (isInArc(cx, cy + y)) { putpix(cx, cy + y) } if (isInArc(cx, cy - y)) { putpix(cx, cy - y) } y++; } } // Bresenham's ellipse algorithm modified for filling fillellipse_bresenham (cx, cy, xradius, yradius) { //this.log('bgi', `fillellipse_bresenham: ${cx}, ${cy}, ${xradius}, ${yradius}`); // DEBUG const xrad2 = 2 * xradius * xradius; const yrad2 = 2 * yradius * yradius; let x = -xradius, y = 0; let e2 = yradius, dx = (1+2*x)*e2*e2; let dy = x*x, err = dx+dy; // center line fix for XOR_PUT mode for (let px = x; px <= -x; px++) { this.ff_putpixel(cx + px, cy); } let last_y = y; do { if (y != last_y) { // prevent repeat drawing same line for XOR_PUT mode. for (let px = x; px <= -x; px++) { this.ff_putpixel(cx + px, cy - y); this.ff_putpixel(cx + px, cy + y); } last_y = y; } e2 = 2 * err; if (e2 <= dy) { y++; dy += xrad2; err += dy; } // y step if (e2 >= dx || (2 * err) > dy) { x++; dx += yrad2; err += dx; } // x step } while (x <= 0); // finish tip of ellipse (likely not needed?) //while (y <= yradius) { // this._putpixel(cx, cy + y); // this._putpixel(cx, cy - y); // y++; //} } // Draws an elliptical arc using slower method of trig and lines // CLOSEST so far for thickness == 3!! // thickness=3, off by 1 pixel at 0°,45°,135°??, ?, ? // // FIXME: doesn't work well with writeMode = BGI.XOR_PUT and thickness = 3 // due to same pixels drawn multiple times. // arc_lines (cx, cy, stangle, endangle, xradius, yradius, thickness = this.info.line.thickness) { //this.log('bgi', `arc_lines: ${cx}, ${cy}, ${stangle}, ${endangle}, ${xradius}, ${yradius}, ${thickness}`); // DEBUG // following copied from ripscript.js v2 drawOvalArc() // TODO: find smoother algorithm const twoPiD = 2 * Math.PI / 360; if (stangle > endangle) { endangle += 360; } let x1 = cx + Math.floor(xradius * Math.cos(stangle * twoPiD)); let y1 = cy - Math.floor(yradius * Math.sin(stangle * twoPiD)); let x2, y2; // draw arc counter-clockwise for (let n = stangle; n <= endangle; n += 1) { // test with: Math.floor() .round() .trunc() x2 = cx + Math.floor(xradius * Math.cos(n * twoPiD)); y2 = cy - Math.floor(yradius * Math.sin(n * twoPiD)); this._line(x1, y1, x2, y2, this.info.fgcolor, BGI.COPY_PUT, BGI.SOLID_LINE, thickness); x1 = x2; y1 = y2; } } // TEST // draws an elliptical arc using slower method of trig and pixels arc_pixels (cx, cy, stangle, endangle, xradius, yradius, thickness = this.info.line.thickness) { // TODO: find smoother algorithm const putpix = (thickness === 3) ? (a, b) => this.thick_putpixel(a, b) : (a, b) => this._putpixel(a, b); const twoPiD = 2 * Math.PI / 360; if (stangle > endangle) { endangle += 360; } let x, y; // calculate a single pixel step along the circumference let step = 1 / (twoPiD * Math.max(xradius, yradius)); // draw arc counter-clockwise for (let n = stangle; n <= endangle; n += step) { // test with: Math.floor() .round() .trunc() x = cx + Math.round(xradius * Math.cos(n * twoPiD)); y = cy - Math.round(yradius * Math.sin(n * twoPiD)); putpix(x, y); } } // Bresenham's line algorithm line_bresenham (x1, y1, x2, y2, color, wmode, upattern = 0xFFFF) { const dx = Math.abs(x2 - x1), //dy = Math.abs(y2 - y1) >> 1 << 1; // force even TEST #5 (didn't fix) dy = Math.abs(y2 - y1); let xi1, xi2, yi1, yi2, den, num, numadd, x = x1, y = y1, numpixels; if (x2 >= x1) { xi1 = xi2 = 1; } else { xi1 = xi2 = -1; } if (y2 >= y1) { yi1 = yi2 = 1; } else { yi1 = yi2 = -1; } if (dx >= dy) { xi1 = yi2 = 0; den = dx; num = dx >> 1; //den = dx - 1; // TEST #2 didn't fix //den = num << 1; // TEST #4 not that close numadd = dy; numpixels = dx; } else { xi2 = yi1 = 0; den = dy; num = dy >> 1; // TESTING (TO REMOVE) //den = dy - 1; // TEST #2 partial //den = dy + 1; // TEST #6 //num = dy; // TEST #5 //den = num << 1; // TEST #4 on to something!? NOT same as TEST #2 // if dy=205, dy-1 = 204, and num<<1 = 204 // no difference, so why does it render differently?? // is it because den is forced to be an integer when using << ? // AH, renders same for THAT case, but not in cases when dy is even! //num = Math.round(dy / 2); // didn't fix //num = dy / 2; // try floats (no change!?) //numadd = dx + 1; // closer on lines, but not on rest TEST #6 //numadd = dx - 1; // didn't fix //numpixels = dy + 1; // TEST #1 numadd = dx; numpixels = dy; } // TESTING (TO REMOVE) //if ((dx % 2 === 1) && (dx > 6)) { //if (x1 === 220) { // console.log({ x1, y1, x2, y2, dx, dy, den, num, numadd }); // DEBUG //} for (let c=0; c <= numpixels; c++) { if ((upattern >> (c % 16)) & 1) { this._putpixel(x, y, color, wmode); } num += numadd; if (num >= den) { //if (num > den) { // TEST didn't fix //num -= (den - 1); // TEST #3 partial num -= den; x += xi1; y += yi1; } x += xi2; y += yi2; } } // Returns the octant (1-8) where (x, y) lies, used by line(). // TODO: needs further testing on edge cases ('>=' vs '>') octant (x, y) { // x=40, y=-40 // GARFIELD.RIP - OK //return (x >= 0) ? ( (y >= 0) ? (( x > y) ? 1 : 2) : (( x > -y) ? 8 : 7) ) // : ( (y >= 0) ? ((-x > y) ? 4 : 3) : ((-x > -y) ? 5 : 6) ); // Fixes L_LINE.RIP, but breaks GARFIELD.RIP floodfill! //return (x >= 0) ? ( (y >= 0) ? (( x > y) ? 1 : 2) : (( x >= -y) ? 8 : 7) ) // : ( (y >= 0) ? ((-x > y) ? 4 : 3) : ((-x > -y) ? 5 : 6) ); // this solves L_LINE2.RIP but still breaks GARFIELD.RIP // ** BEST GUESS SO FAR ** return (x >= 0) ? ( (y > 0) ? (( x >= y) ? 1 : 2) : (( x >= -y) ? 8 : 7) ) : ( (y >= 0) ? ((-x > y) ? 4 : 3) : ((-x >= -y) ? 5 : 6) ); // this fixes GARFIELD.RIP (see slanted black line at right edge of canvas) // compare to screenshot, it looks like floodfill() is responsible for not leaking at that point. // so not fixable in this function! //return (x >= 0) ? ( (y > 0) ? (( x >= y) ? 1 : 2) : (( x > -y) ? 8 : 7) ) // : ( (y >= 0) ? ((-x > y) ? 4 : 3) : ((-x >= -y) ? 5 : 6) ); } //////////////////////////////////////////////////////////////////////////////// // Font methods // download and parse given .CHR font file. // filename = filename without path, including '.CHR' (e.g. 'BOLD.CHR') // uses this.fontsPath // returns font object or undefined. // async fetchCHRFont (filename) { if (typeof filename !== 'string') { this.log('err', 'fetchCHRFont() missing "filename"!'); return; } if (typeof this.fontsPath !== 'string') { this.log('err', 'must pass in "fontsPath" to BGI()!'); return; } const url = this.fontsPath + '/' + filename; this.log('fnt', 'Fetching CHR font: ' + url); let request = new Request(url); try { const response = await fetch(request); if (!response.ok) throw new Error("Request failed"); const buffer = await response.arrayBuffer(); const dview = new DataView(buffer); if (dview.getUint8(0x80) === 43) { // 43 = '+' // Save font metadata let font = {}; font.numchars = dview.getUint16(0x81, true); // true = little-endian font.firstchar = dview.getUint8(0x84); // ASCII value of first char font.top = dview.getInt8(0x88); // top of capital, going up from baseline font.baseline = dview.getInt8(0x89); // usually 0? font.bottom = dview.getInt8(0x8A); // bottom descender, usually negative // Create a copy of the char widths byte array buffer to store into font object. const widthOffset = 0x90 + (font.numchars * 2); const fontWidths = new Uint8Array(buffer, widthOffset, font.numchars); font.widths = fontWidths.slice(0); // widths are indexed 0 to numchars-1 //console.log(font); // DEBUG // Must read offsets to char defs stored in file, as some reuse the same def. (e.g. SANS.CHR) const charOffsets = new Uint16Array(buffer, 0x90, font.numchars); const dataOffset = dview.getUint16(0x85, true) + 0x80; const defdata = new Uint8Array(buffer, dataOffset); // length to EOF font.data = []; // array of Uint8Array slices, indexed 0 to numchars-1 // Each pair of bytes is an (x, y) coord, and 2-bit opcode encoded in each high bit. // All we care about here is to slice whenever both high bits are 0, // which means End of Character definition. if ((defdata.length % 2) !== 0) { this.log('err', 'Font data length should be a multiple of 2, so something is wrong.'); return; } // Scan definition data starting at each offset in charOffsets[] to create sliced copies. let byte1, byte2, pos = 0, spos = 0; for (let i=0; i < font.numchars; i++) { spos = charOffsets[i]; pos = spos; do { byte1 = defdata[pos]; byte2 = defdata[pos + 1]; pos += 2; } while (!(((byte1 & 0x80) === 0) && ((byte2 & 0x80) === 0)) && (pos < defdata.length - 1)) font.data.push( defdata.slice(spos, pos) ); } return font; } else { this.log('fnt', 'CHR file marker not correct'); return; } } catch (e) { this.log('err', `fetchCHRFont error: ${e}`); return; } } // end fetchCHRFont() // uses and updates this.info.cp, and this.fonts // value = ASCII code value of a single character as int. // fontname = filename including .CHR (e.g. 'GOTH.CHR') MAY CHANGE. // scale = 1 to 10, indexes into BGI.fontScales[] for actual scale factor. drawChar (value, fontname, scale, dir) { // current postion (cp) should only update when dir is horizontal, not vertical! (see specs v2.0) // check for valid input if ((fontname in this.fonts) === false) { this.log('fnt', `${fontname} font not found!`); return; } const font = this.fonts[fontname]; const color = this.info.fgcolor; const actualScale = (scale < BGI.fontScales.length) ? BGI.fontScales[scale] : 1; if ((value < font.firstchar) || (value >= font.firstchar + font.numchars)) { this.log('fnt', `value ${value} out of range in ${fontname}`); return; } // scan char data const chardata = font.data[value - font.firstchar]; let xbyte, ybyte, dx, dy, x, y; let numcmds = chardata.length; const x0 = this.info.cp.x; const y0 = this.info.cp.y; for (let i=0; i < numcmds; i+=2) { // read next byte pair xbyte = chardata[i]; ybyte = chardata[i + 1]; // convert the one's-complement 7-bit signed values dx = (xbyte & 0x40) ? -(-xbyte & 0x3F) : (xbyte & 0x3F); dy = (ybyte & 0x40) ? -(-ybyte & 0x3F) : (ybyte & 0x3F); // multiply by scale factor dx = Math.trunc(dx * actualScale); dy = Math.trunc(dy * actualScale); // handle direction x = x0 + ((dir === BGI.HORIZ_DIR) ? dx : -dy); y = y0 + ((dir === BGI.HORIZ_DIR) ? -dy : -dx); if (((xbyte & 0x80) !== 0) && (ybyte & 0x80) === 0) { // move pointer when high bits 1 & 0 //console.log('move', xbyte, ybyte, dx, dy); // DEBUG this.moveto(x, y); } else if (((xbyte & 0x80) !== 0) && (ybyte & 0x80) !== 0) { // draw line when high bits 1 & 1 //console.log('draw', xbyte, ybyte, dx, dy); // DEBUG this.lineto(x, y, color, BGI.COPY_PUT, BGI.SOLID_LINE, BGI.NORM_WIDTH); } else { //console.log('????', xbyte, ybyte, dx, dy); // DEBUG } } } // Downloads a PNG file containing a bitmap font. // xsize & ysize is the size in pixels of an individual character. (e.g. 8x8) // PNG file must arrange all ASCII chars in a 16 x 16 grid. // PNG size must be at least (16 * xsize) by (16 * ysize) pixels. // OFF pixels are black (0,0,0), else they are ON pixels. // filename = filename without path, including '.png' (e.g. '8x8.png') // will use this.fontsPath + '/' + filename for full path. // Returns an array of 256 elements for each ASCII char, // each element is an array of ints, each being a scanline of height values. // e.g. [65][2] is ASCII char 65, 3rd scanline which returns an 8-bit int. // async fetchPNGFont (filename, xsize, ysize) { // checking args if (typeof filename !== 'string') { this.log('err', 'fetchPNGFont() missing "filename"!'); return; } if (typeof this.fontsPath !== 'string') { this.log('err', 'must pass in "fontsPath" to BGI()!'); return; } const url = this.fontsPath + '/' + filename; this.log('fnt', 'Fetching PNG font: ' + url); let img = new Image(); return new Promise((resolve, reject) => { img.onload = () => { const imgW = img.naturalWidth; const imgH = img.naturalHeight; // PNG size must be at least (16 * xsize) by (16 * ysize) pixels. const minW = xsize * 16, minH = ysize * 16; if ((imgW < minW) || (imgH < minH)) { //this.log('err', `PNG size is ${imgW}x${imgH} but needs to be at least ${minW}x${minH}!`); reject(new Error(`PNG size is ${imgW}x${imgH} but needs to be at least ${minW}x${minH}!`)); } // preparing font image const can1 = document.createElement('canvas'); can1.setAttribute('width', imgW); can1.setAttribute('height', imgH); const ctx1 = can1.getContext('2d'); ctx1.drawImage(img, 0, 0); const imgdata1 = ctx1.getImageData(0, 0, imgW, imgH); // imgdata1.data is Uint8ClampedArray(w * h * 4) // RGBA // for every ASCII char let abitfont = []; let pos = 0, bit = 0; for (let i=0; i < 256; i++) { // upper-left pixel of char in PNG let x0 = (i % 16) * xsize; let y0 = Math.floor(i / 16) * ysize; let bitchar = []; // array of scanline values (1 for each row) for (let y=0; y < ysize; y++) { let scanline = 0; // looping x in reverse so bits are shifted on scanline in reverse order for (let x = xsize - 1; x >= 0; x--) { pos = ((y0 + y) * imgW + x0 + x) * 4; bit = (imgdata1.data[pos] != 0) ? 1 : 0; // just checking red value scanline = (scanline << 1) | bit; } bitchar.push(scanline); } abitfont.push(bitchar); } resolve(abitfont); }; // end onload img.onerror = (error) => { reject(new Error("Failed to load PNG font")); }; img.src = url; }); // end Promise } // Uses and updates this.info.cp, this.bitfonts, info.fgcolor // value: ASCII code value of a single character as int. // fontnum: size value 0-4 // scale: 1 to 10 ? // dir: 0 = horizontal, 1 = vertical // TODO: should pass in info.fgcolor ? // TODO: pass in flag to prevent overriding this.info.cp ? // drawPNGChar (value, fontnum, scale, dir, x0 = this.info.cp.x, y0 = this.info.cp.y) { if (value < 0 || value > 255) { return; } if (scale < 1) { scale = 1; } let bitchar = this.bitfonts[fontnum][value]; // FIXME: may be undefined! if (bitchar === undefined) { return; } const ysize = bitchar.length; const xsize = 8; // TODO: need to store sizes in lookup table! // loop thru each scanline let scanline, bit, x1, y1; for (let y=0; y < ysize; y++) { scanline = bitchar[y]; for (let x=0; x < xsize; x++) { bit = scanline & 1; scanline = scanline >> 1; if (bit) { if (scale > 1) { // square pixel if (dir === BGI.HORIZ_DIR) { x1 = x0 + (x * scale); y1 = y0 + (y * scale); } else { x1 = x0 + (y * scale); y1 = y0 - (x * scale); } this._bar(x1, y1, x1 + scale - 1, y1 + scale - 1, this.info.fgcolor, this.info.writeMode, BGI.SOLID_FILL); } else { // single pixel if (dir === BGI.HORIZ_DIR) { this._putpixel(x0 + x, y0 + y); // TODO: try _putpixel_abs() later? } else { this._putpixel(x0 + y, y0 - x); } } } } } // increment current position if (dir === BGI.HORIZ_DIR) { this.info.cp.x += (xsize * scale); } else { this.info.cp.y -= (xsize * scale); } } //////////////////////////////////////////////////////////////////////////////// // Image methods // Download and parse given .ICN or .HIC icon image file. // Assumes 16-color images, 4-bits per pixel. // filename = filename without path. // uses this.iconsPath // returns: image object or undefined. // { width:int, height:int, data:Uint8ClampedArray, url:string } // async fetchIcon (url) { if (typeof url !== 'string') { this.log('err', 'fetchIcon() missing "url"!'); return; } this.log('bgi', `Fetching icon: ${url}`); let request = new Request(url); try { const response = await fetch(request); if (!response.ok) throw new Error("Request failed"); const buffer = await response.arrayBuffer(); // no magic number in ICN files to test against const dview = new DataView(buffer); const imgWidth = dview.getUint16(0, true) + 1; // true = little-endian const imgHeight = dview.getUint16(2, true) + 1; // reject 1x1 images and those that can't fit in canvas if ((imgWidth > 1) && (imgWidth <= this.width) && (imgHeight > 1) && (imgHeight <= this.height)) { // decoding 4 bit-planes (16 colors) let data = new Uint8ClampedArray(imgWidth * imgHeight + 8); // 8 extra slack pixels const fileData = new Uint8Array(buffer, 4); // byte 4 to EOF // calculate num bytes per bit-plane scan line, rounded up to 8-bit multiple const byteWidth = Math.ceil(imgWidth / 8); // read 4 bit-planes * byteWidth bytes each, per scanline (y) let x, i=0, o=0, bp0, bp1, bp2, bp3, pix; const off2 = byteWidth, off1 = byteWidth * 2, off0 = byteWidth * 3; for (let y=0; y < imgHeight; y++) { x = 0; i = y * byteWidth * 4; // input index o = y * imgWidth; // output index for (let b=0; b < byteWidth; b++) { bp3 = fileData[i + b]; bp2 = fileData[i + b + off2]; bp1 = fileData[i + b + off1]; bp0 = fileData[i + b + off0]; // convert bits from these 4 bit-plane bytes into 8 byte pixels (16-colors) for (let p=7; p >= 0; p--) { pix = ((bp3 & 1) << 3) | ((bp2 & 1) << 2) | ((bp1 & 1) << 1) | (bp0 & 1); bp3 = bp3 >> 1; bp2 = bp2 >> 1; bp1 = bp1 >> 1; bp0 = bp0 >> 1; data[o+p] = pix; } o += 8; } } return { width: imgWidth, height: imgHeight, data: data, url: url }; } else { this.log('bgi', `invalid icon? (${imgWidth} x ${imgHeight})`); return; } } catch (e) { this.log('err', `fetchIcon error: ${e}`); return; } } // Converts image.data to a base64 encoded href string. // Uses current palette and data to create an embedded PNG. // Doesn't handle writeMode, stores as BGI.COPY_PUT. // Returns "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." // imageToDataURL (image, defaults = {}) { const { pal = this.palette } = defaults; if (!(image && image.width && image.height && image.data)) { this.log('err', 'imageToDataURL() missing "image"!'); return; } // can use image.data byte buffer, don't need a new one const w = image.width, h = image.height; // create a temp canvas to write image to const canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; const ctx = canvas.getContext("2d"); const imgData = ctx.createImageData(w, h); // copy 8-bit image.data to 32-bit canvas using current global palette this.refresh({ ctx:ctx, img:imgData, pix:image.data, pal:pal, isBuffered:true }); // output canvas as a PNG data URL return canvas.toDataURL("image/png"); } //////////////////////////////////////////////////////////////////////////////// // Standard BGI functions // SEE https://www.cs.colorado.edu/~main/bgi/doc/ // To convert Pascal syntax to JS, simply convert function names to lowercase. // Draw a circular arc centered at (x, y), from stangle to endangle in degrees, // counterclockwise with 0 = 3 o'clock, 90 = 12 o'clock, etc. // doesn't use linestyle. arc (cx, cy, stangle, endangle, radius, thickness = this.info.line.thickness) { this._arc(cx, cy, stangle, endangle, radius, thickness); } _arc (cx, cy, stangle, endangle, radius, thickness = this.info.line.thickness) { // adjust radius based on aspect ratio // TODO: this may be off? const yradius = Math.floor( radius * (this.aspect.xasp / this.aspect.yasp) ); this._ellipse(cx, cy, stangle, endangle, radius, yradius, thickness); } // Draws and fills a rectangle, using fill style and color. Has no border. (see fillrect) // For a border, use bar3d with depth = 0. bar (left, top, right, bottom, color = this.info.fill.color, wmode = this.info.writeMode, fillstyle = this.info.fill.style ) { this._bar(left, top, right, bottom, color, wmode, fillstyle); } _bar (left, top, right, bottom, color = this.info.fill.color, wmode = this.info.writeMode, fillstyle = this.info.fill.style ) { // swap if (left > right) { let tmp = left; left = right; right = tmp; } if (top > bottom) { let tmp = top; top = bottom; bottom = tmp; } const fillcolor = (fillstyle === BGI.EMPTY_FILL) ? this.info.bgcolor : color; if (fillstyle === BGI.SOLID_FILL) { // draw filled solid bar for (let y = top; y <= bottom; y++) { for (let x = left; x <= right; x++) { this._putpixel(x, y, fillcolor, wmode); } } } else { // draw filled patterned bar for (let y = top; y <= bottom; y++) { for (let x = left; x <= right; x++) { this.ff_putpixel(x, y, fillcolor, wmode); } } } } // Draws a 3D filled rectangle using fill color and pattern. // Outline drawn using line style and fg color. // if topflag != 0, draws a top. bar3d (left, top, right, bottom, depth, topflag) { /* topflag = true false +----------+ + / /| /| +----------+ | +----------+ | |XXXXXXXXXX| + |XXXXXXXXXX| + |XXXXXXXXXX|/ |XXXXXXXXXX|/ +----------+ +----------+ Outline uses info.fgcolor & info.line... Fill uses info.fill.color & fill.style, or info.bgcolor if using EMPTY_FILL. All use info.writeMode. Sides aren't filled in. */ // swap if (left > right) { let tmp = left; left = right; right = tmp; } if (top > bottom) { let tmp = top; top = bottom; bottom = tmp; } // draw filled bar const fillcolor = (this.info.fill.style === BGI.EMPTY_FILL) ? this.info.bgcolor : this.info.fill.color; this.bar(left, top, right, bottom, fillcolor) // following drawn using info.fgcolor if (depth > 0) { // TODO: doesn't seem to fill in 3d part, only draws an outline? // also calling line() would have thickness and line pattern if (topflag) { this._line(left, top, left + depth, top - depth); this._line(left + depth, top - depth, right + depth, top - depth); } this._line(right, top, right + depth, top - depth); this._line(right, bottom, right + depth, bottom - depth); this._line(right + depth, bottom - depth, right + depth, top - depth); } this.rectangle(left, top, right, bottom); } // doesn't use linestyle circle (cx, cy, radius, thickness = this.info.line.thickness) { this._arc(cx, cy, 0, 360, radius); } _circle (cx, cy, radius, thickness = this.info.line.thickness) { this._arc(cx, cy, 0, 360, radius); } // Clears the screen, filling it with the current background color. // Resets CP to (0,0). cleardevice (bgcolor = this.info.bgcolor) { this.info.cp.x = 0; this.info.cp.y = 0; this.pixels.fill(bgcolor); this.fillpixels.fill(0); this.icons = {}; // TEST: do we want to do this here? (clears image ids) } // Clears the viewport, filling it with the current background color. // Resets cp to (0,0). clearviewport (bgcolor = this.info.bgcolor) { this.info.cp.x = 0; this.info.cp.y = 0; const vp = this.info.vp; for (let y = vp.top; y <= vp.bottom; y++) { const row = y * this.width; for (let x = vp.left; x <= vp.right; x++) { this.pixels[row + x] = bgcolor; } /* // maybe or maybe not faster const st = (y * this.width) + vp.left; const en = (y * this.width) + vp.right + 1; this.pixels.fill(bgcolor, st, en) */ } } // not implemented (STUB) detectgraph (graphdriver, graphmode) { // int *graphdriver, int *graphmode this.unimplemented('detectgraph') } // Draws a Cubic Bezier. [NOT IN BGI - RIPscrip extension] // numsegments is number of segments in curve. // cntpoints is an array of 8 ints (4 control points): // [x1, y1, x2, y2, x3, y3, x4, y4] // (x1, y1) and (x4, y4) are on the curve, while others are not. // uses line style, thickness, and write mode. drawbezier (numsegments, cntpoints) { this._drawbezier(numsegments, cntpoints); } // Calculate adaptive segment count based on control polygon length. // Algorithm reverse-engineered from RSKETCH.EXE (Wayne Thomas, 1993) via Ghidra. // Found at function FUN_1000_62b9 in the binary. // Formula: segments = (|P1-P0| + |P2-P1| + |P3-P2|) / 10, clamped to [5, 500] _calculateBezierSegments (cntpoints) { const [x1, y1, x2, y2, x3, y3, x4, y4] = cntpoints; const d01 = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); const d12 = Math.sqrt((x3 - x2) ** 2 + (y3 - y2) ** 2); const d23 = Math.sqrt((x4 - x3) ** 2 + (y4 - y3) ** 2); const segments = Math.floor((d01 + d12 + d23) / 10); return Math.max(5, Math.min(500, segments)); } _drawbezier (numsegments, cntpoints) { // Cubic Bezier formula (Bernstein polynomial): // p = (1-t)^3 *P0 + 3*t*(1-t)^2*P1 + 3*t^2*(1-t)*P2 + t^3*P3 // where t is 0 to 1 if (!(cntpoints && (cntpoints.length >= 8))) { this.log('bgi', `drawbezier() invalid args! numsegments: ${numsegments}, cntpoints: ${cntpoints}`); return; } // Use adaptive subdivision if numsegments is 0, null, or invalid // This matches the algorithm from period-correct DOS implementations if (!numsegments || numsegments < 1) { numsegments = this._calculateBezierSegments(cntpoints); } const [x1, y1, x2, y2, x3, y3, x4, y4] = cntpoints; let step = 1 / numsegments; let xp = x1, yp = y1, xn, yn; for (let t = step; t < 1; t += step) { let t1 = 1 - t; xn = Math.floor( t1*t1*t1 * x1 + 3 * t * t1*t1 * x2 + 3 * t*t * t1 * x3 + t*t*t * x4 ); yn = Math.floor( t1*t1*t1 * y1 + 3 * t * t1*t1 * y2 + 3 * t*t * t1 * y3 + t*t*t * y4 ); this._line(xp, yp, xn, yn); xp = xn; yp = yn; } this._line(xp, yp, x4, y4); } // Draws a closed polygon. // polypoints is an array of ints: [x1, y1, x2, y2, ... xn, yn] // where n = numpoints. drawpoly (numpoints, polypoints, color = this.info.fgcolor) { this._drawpoly(numpoints, polypoints, color); } _drawpoly (numpoints, polypoints, color = this.info.fgcolor) { // polypoints array of ints if (!(numpoints && polypoints && (numpoints >= 2) && (polypoints.length >= numpoints * 2))) { this.log('bgi', `drawpoly() invalid args! numpoints: ${numpoints}, polypoints: ${polypoints}`); return; } for (let n=0; n < numpoints - 1; n++) { this._line(polypoints[2*n], polypoints[2*n + 1], polypoints[2*n + 2], polypoints[2*n + 3], color); } // close the polygon this._line(polypoints[2 * numpoints - 2], polypoints[2 * numpoints - 1], polypoints[0], polypoints[1], color); } // Draws a polyline. [NOT IN BGI] // polypoints is an array of ints: [x1, y1, x2, y2, ... xn, yn] // where n = numpoints. drawpolyline (numpoints, polypoints, color = this.info.fgcolor) { this._drawpolyline(numpoints, polypoints, color); } _drawpolyline (numpoints, polypoints, color = this.info.fgcolor) { // polypoints array of ints if (!(numpoints && polypoints && (numpoints >= 2) && (polypoints.length >= numpoints * 2))) { this.log('bgi', `drawpolyline() invalid args! numpoints: ${numpoints}, polypoints: ${polypoints}`); return; } for (let n=0; n < numpoints - 1; n++) { this._line(polypoints[2*n], polypoints[2*n + 1], polypoints[2*n + 2], polypoints[2*n + 3], color); } } // Draw an elliptical arc centered at (x, y), from stangle to endangle in degrees, // counterclockwise with 0 = 3 o'clock, 90 = 12 o'clock, etc. // doesn't use linestyle. ellipse (cx, cy, stangle, endangle, xradius, yradius, thickness = this.info.line.thickness) { this._ellipse(cx, cy, stangle, endangle, xradius, yradius, thickness); } _ellipse (cx, cy, stangle, endangle, xradius, yradius, thickness = this.info.line.thickness) { // need these if (stangle === endangle) { return } if ((xradius === 0) && (yradius === 0) && (thickness > 1)) { return } if (xradius < 1) { xradius = 1; } if (yradius < 1) { yradius = 1; } // fix for some arcs if (stangle > endangle) { endangle += 360 } // Angle span determines rendering path. // RIPTERM.EXE (FUN_368f_2c06) uses three thresholds: // span > 349° ? full ellipse (handles near-complete arcs) // span < 2° ? line approximation // else ? arc segments const angleSpan = endangle - stangle; if (angleSpan > 349) { // Full ellipse path ? RIPTERM treats spans > 349° as complete ellipses // to avoid visible gaps from floating-point endpoint mismatch. if (thickness === 1) { this.ellipse_bresenham(cx, cy, xradius, yradius, thickness); } else { this.arc_lines(cx, cy, 0, 360, xradius, yradius, thickness); } } else if (angleSpan < 2) { // Very small arc ? RIPTERM approximates as a line between endpoints. const rad_st = stangle * Math.PI / 180; const rad_en = endangle * Math.PI / 180; const x0 = Math.round(cx + xradius * Math.cos(rad_st)); const y0 = Math.round(cy - yradius * Math.sin(rad_st)); const x1 = Math.round(cx + xradius * Math.cos(rad_en)); const y1 = Math.round(cy - yradius * Math.sin(rad_en)); this.line(x0, y0, x1, y1); } else { // Partial arc if (thickness === 1) { this.arc_bresenham(cx, cy, stangle, endangle, xradius, yradius, thickness); } else { this.arc_lines(cx, cy, stangle, endangle, xradius, yradius, thickness); } } } fillellipse (cx, cy, xradius, yradius) { this._fillellipse(cx, cy, xradius, yradius); } _fillellipse (cx, cy, xradius, yradius) { // TODO: don't know if these are correct, or should we exit? if (xradius < 1) { xradius = 1; } if (yradius < 1) { yradius = 1; } // TODO: should this be outlined here or in ripterm code? this.fillellipse_bresenham(cx, cy, xradius, yradius) } // Draw a filled polygon, using current fill pattern, fill color and bgcolor. // pp is an array of ints: [x1, y1, x2, y2, ... xn, yn] // where n = numpoints. fillpoly (numpoints, pp) { this._fillpoly(numpoints, pp); } //_fillpoly (numpoints, pp) { // TODO: uncomment after done testing _fillpoly (numpoints, pp, debugFlag = false) { // temp // polypoints array of ints // code based on: http://alienryderflex.com/polygon_fill/ const vp = this.info.vp; let i, j, x, y, xnode, xval; // scan thru all rows in viewport for (y = vp.top; y <= vp.bottom; y++) { xnode = []; // build node list j = numpoints - 1; for (i=0; i < numpoints; i++) { const tx1 = pp[j*2], ty1 = pp[j*2+1]; const tx2 = pp[i*2], ty2 = pp[i*2+1]; //const tx1 = pp[j*2] + 1, ty1 = pp[j*2+1]; // TEST //const tx2 = pp[i*2] + 1, ty2 = pp[i*2+1]; // TEST //if ( ((pp[i*2+1] < y) && (pp[j*2+1] >= y)) || ((pp[j*2+1] < y) && (pp[i*2+1] >= y)) ) { //if ( ((ty2 < y) && (ty1 >= y)) || ((ty1 < y) && (ty2 >= y)) ) { // orig if ( ((ty2 <= y) && (ty1 > y)) || ((ty1 <= y) && (ty2 > y)) ) { // better for SKATE.RIP //if ( ((ty2 <= y) && (ty1 > y)) || ((ty1 <= y) && (ty2 > y)) || ((ty1 === y) && (ty2 === y)) ) { //if ( ((ty2 <= y) && (ty1 >= y)) || ((ty1 <= y) && (ty2 >= y)) ) { // FIXME: there are off-by-one pixels along edges of polygons. // I tried lots of combinations, but could not solve it... // Note for GHOST.RIP: off pixels are always on left-hand side // while polyline wraps around clockwise. Off-pixel increases left side +1. //xval = (y - pp[i*2+1]) / (pp[j*2+1] - pp[i*2+1]) * (pp[j*2] - pp[i*2]) + pp[i*2]; //xval = (y - ty2) / (ty1 - ty2) * (tx1 - tx2) + tx2; // orig // fix for divide by 0 if ty1 - ty2 == 0 xval = (ty1 === ty2) ? tx2 : (y - ty2) / (ty1 - ty2) * (tx1 - tx2) + tx2; // TEST #1: still not solving it (TO REMOVE) //xval = (ty1 === ty2) ? tx2 : (y - ty2) / (ty1 - ty2) * (tx1 - tx2 + 1) + tx2; //xval = (ty1 === ty2) ? tx2 : Math.trunc((y - ty2) / (ty1 - ty2)) * (tx1 - tx2) + tx2; // REMOVE //xval = (ty1 === ty2) ? tx2 : (y - ty2) * (tx1 - tx2) / (ty1 - ty2) + tx2; // TEST (same) //TO REMOVE // tried this, didn't solve bug /* const oct = this.octant(tx2 - tx1, ty1 - ty2); if ((oct === 1) || (oct === 4) || (oct ===5) || (oct === 8)) { xnode.push( Math.ceil(xval) ); } else { xnode.push( Math.floor(xval) ); } */ // tried all these (REMOVE) //xnode.push( Math.ceil(xval) ); //xnode.push( Math.floor(xval) ); // orig //xnode.push( Math.trunc(xval) ); //xnode.push( xval | 0 ); // same as trunc() but slightly faster? //xnode.push( Math.round(xval) | 0 ); // TEST #2 // this solves trees in SKATE.RIP // doesn't solve GHOST.RIP, OUT-AD.RIP, or others. //xnode.push( ((ty1 - ty2) > 0) ? Math.ceil(xval) : Math.floor(xval) ); // TEST #3: store floats (getting closer) xnode.push(xval); } j = i; } // sort nodes if (xnode.length == 0) continue; xnode.sort(function(a, b) { return a - b; }); /* // DEBUG: must set y to value to test const TEST_Y = 200; if (debugFlag && (y === TEST_Y)) { this.log('bgi', `fillpoly: y=${y}, xnode=${xnode}`); // DEBUG } */ // draw pixels between node pairs for (i=0; i < xnode.length; i+=2) { //const x0 = xnode[i], x1 = xnode[i+1]; // orig const x0 = Math.ceil(xnode[i]), x1 = Math.floor(xnode[i+1]); // TEST #3 (getting closer) for (x = x0; x <= x1; x++) { this.ff_putpixel(x, y); } } } // this fixes filled polygon border issues // not sure if bgcolor should just be 0, but since bgcolor is never set != 0, it doesn't matter if (this.info.fgcolor != this.info.bgcolor) { this._drawpoly(numpoints, pp); } } /* // TO REMOVE // TEST: FLIPPING x & y axis (SAME ISSUE AS ABOVE) fillpoly2 (numpoints, pp) { // polypoints array of ints // code based on: http://alienryderflex.com/polygon_fill/ const vp = this.info.vp; let i, j, x, y, ynode, yval; // scan thru all rows in viewport for (x = vp.left; x <= vp.right; x++) { ynode = []; // build node list j = numpoints - 1; for (i=0; i < numpoints; i++) { if ( ((pp[i*2] < x) && (pp[j*2] >= x)) || ((pp[j*2] < x) && (pp[i*2] >= x)) ) { yval = (x - pp[i*2]) / (pp[j*2] - pp[i*2]) * (pp[j*2+1] - pp[i*2+1]) + pp[i*2+1]; //ynode.push( Math.round(yval) ); //ynode.push( Math.floor(yval) ); //ynode.push( Math.trunc(yval) ); ynode.push( Math.ceil(yval) ); } j = i; } // sort nodes if (ynode.length == 0) continue; ynode.sort(function(a, b) { return a - b; }); // draw pixels between node pairs for (i=0; i < ynode.length; i+=2) { // clip to viewport edges if (ynode[i] > vp.bottom) break; if (ynode[i+1] >= vp.top) { if (ynode[i+1] > vp.bottom) { ynode[i+1] = vp.bottom; } if (ynode[i] < vp.top) { ynode[i] = vp.top; } for (y = ynode[i]; y <= ynode[i+1]; y++) { this.ff_putpixel(x, y, this.info.fill.color, BGI.COPY_PUT); } } } } // this fixes filled polygon border issues if (this.info.fgcolor !== this.info.bgcolor) { this.drawpoly(numpoints, pp); } } */ // NOT IN SDL_bgi ? // Draws and fills a rectangle, using fgcolor, line & fill style. Includes border. fillrect (left, top, right, bottom) { this.bar(left, top, right, bottom); this.rectangle(left, top, right, bottom); } _fillrect (left, top, right, bottom) { this._bar(left, top, right, bottom); this._rectangle(left, top, right, bottom); } // Flood Fill // uses info.fill.color, info.fill.fpattern, & info.bgcolor. // uses info.writeMode but not sure if it should. // border or color = boundry color. // Returns if (x0, y0) is outside current viewport, or is already border color. // Clears then writes pixels of color 0xFF to this.fillpixels[] // which can be read after return to find out the fill area. // This floods horizontally before vertically in order to support a bug. floodfill (x0, y0, border) { this._floodfill(x0, y0, border); } _floodfill (x0, y0, border) { const vp = this.info.vp; this.fillpixels.fill(0); if ((x0 < 0) || (x0 >= vp.width ) || (y0 < 0) || (y0 >= vp.height)) { return } if (this.getpixel(x0, y0) === border) { return } let stack = []; stack.push([x0, y0]); let popped, count = 0; while (popped = stack.pop()) { let [x, y] = popped; let x1 = x, x2 = x + 1; // find left end of line (x1) while ((x1 >= 0) && (this.getpixel(x1, y) != border)) { x1--; } x1++; // find right end of line (x2) while ((x2 < vp.width) && (this.getpixel(x2, y) != border)) { x2++; } x2--; let spanUp = false, spanDown = false; for (x = x1; x <= x2; x++) { this.ff_putpixel(x, y, this.info.fill.color, BGI.COPY_PUT); this._putpixel(x, y, BGI.WHITE, BGI.COPY_PUT, this.fillpixels); count++; // intentional bug to prevent flooding up or down // through pixels along edges of viewport. if ((x <= 0) || (x >= vp.width - 1)) { continue } if (!spanUp && (y > 0) && (this.getpixel(x, y-1) !== border) && (this.getpixel(x, y-1, this.fillpixels) !== BGI.WHITE)) { stack.push([x, y-1]); spanUp = true; } else if (spanUp && (y > 0) && (this.getpixel(x, y-1) === border)) { spanUp = false; } if (!spanDown && (y < vp.height - 1) && (this.getpixel(x, y+1) != border) && (this.getpixel(x, y+1, this.fillpixels) !== BGI.WHITE)) { stack.push([x, y+1]); spanDown = true; } else if (spanDown && (y < vp.height - 1) && (this.getpixel(x, y+1) === border)) { spanDown = false; } } } } getarccoords (arccoords) { // *** // struct arccoordstype *arccoords } // returns an object {xasp:int, yasp:int} getaspectratio (/* xasp, yasp */) { // *** // int *xasp, int *yasp return this.aspect; } getbkcolor (/* void */) { return this.info.bgcolor; // int } getcolor (/* void */) { return this.info.fgcolor; // int } getdefaultpalette (/* void */) { // return struct palettetype* return BGI.ega_palette; // TODO: might change } // not implemented (STUB) getdrivername () { this.unimplemented('getdrivername'); return ''; } getfillpattern (pattern) { // *** // char *pattern } getfillsettings (/* fillinfo */) { // *** // struct fillsettingstype *fillinfo return this.info.fill; } getgraphmode (/* void */) { return 0; // int } // Grab and return a byte array of image data. // returns this image object: // { x:int, y:int, width:int, height:int, data:Uint8ClampedArray } // uses this.pixels[] getimage (left, top, right, bottom) { return this._getimage(left, top, right, bottom); } _getimage (left, top, right, bottom) { // void *bitmap // swap if (left > right) { let tmp = left; left = right; right = tmp; } if (top > bottom) { let tmp = top; top = bottom; bottom = tmp; } let w = (right - left + 1), h = (bottom - top + 1); let data = new Uint8ClampedArray(w * h); let i, o=0; for (let y=top; y <= bottom; ++y) { i = y * this.width + left; for (let x=left; x <= right; ++x) { data[o++] = this.pixels[i++]; } } return {x: left, y: top, width: w, height: h, data: data}; } getlinesettings (/* lineinfo */) { // *** // struct linesettingstype *lineinfo return this.info.line; } getmaxcolor (/* void */) { return BGI.MAXCOLORS; // ok for now } getmaxmode (/* void */) { return 0; // int } getmaxx (/* void */) { return this.width - 1; // int } getmaxy (/* void */) { return this.height - 1; // int } getmodename (mode_number) { return ''; } getmoderange(graphdriver, lomode, himode) { // int graphdriver, int *lomode, int *himode } getpalette (/* palette */) { // *** // struct palettetype *palette // C++ version fills pointer to palette struct provided, returns void. // js version returns a RGBA32 palette array. return this.palette; } // returns number of colors in the RGBA32 palette array getpalettesize (/* void */) { return Math.floor(this.palette.length / 4); // int } // Get pixel NOT offset by current viewport, else return 0. // (if changed to offset, remember to fix it in floodfill().) getpixel (x, y, buf = this.pixels) { // since this is only called from floodfill(), we know they're already ints // x = Math.round(x); // y = Math.round(y); // offset by viewport const vp = this.info.vp; x += vp.left; y += vp.top; if ((x >= 0) && (x < this.width) && (y >= 0) && (y < this.height)) { return buf[y * this.width + x]; // int } return 0; // ??? } // returns object: { font: 0, direction: 0, charsize: 0, horiz: 0, vert: 0 } gettextsettings () { // OLD struct textsettingstype *texttypeinfo return JSON.parse(JSON.stringify(this.info.text)); // copy obj } getviewsettings (viewport) { // *** // struct viewporttype *viewport } getx (/* void */) { return this.info.cp.x; // int } gety (/* void */) { return this.info.cp.y; // int } graphdefaults (/* void */) { /* graphdefaults resets all graphics settings to their defaults: sets the viewport to the entire screen. moves the current position to (0,0). sets the default palette colors, background color, and drawing color. sets the default fill style and pattern. sets the default text font and justification. */ this.info = this.infoDefaults(); // RGBA32 [r, g, b, a] same as npm's canvas //this.palette = new Uint8ClampedArray(256 * 4); this.palette = Uint8ClampedArray.from(BGI.ega_palette); // for now } // not implemented (STUB) grapherrormsg (errorcode) { // errorcode from graphresult() return value. this.unimplemented('grapherrormsg'); return ''; } // not implemented (STUB) graphresult (/* void */) { // returns error code for the last graphics operation that reported an error this.unimplemented('graphresult'); return 0; } // returns size of memory in bytes required to store a bit image imagesize (x1, y1, x2, y2) { this.unimplemented('imagesize'); return 0; // unsigned int } // initializes the graphics system (old DOS way) // don't implement? (STUB) initgraph (width, height, pathtodriver) { // modified args // int *graphdriver, int *graphmode, char *pathtodriver // original args // pathtodriver = is also path to *.CHR files // in Windows instead use: initwindow(w, h) this.unimplemented('initgraph'); } // don't implement (STUB) // old DOS function installuserdriver () { // char *name, int huge (*detect)(void) this.unimplemented('installuserdriver'); return 0; } // Installs a font file from disk. // name = filename of font .CHR file (e.g. "USER.CHR") not including path? // returns a font ID that can be passed to settextstyle() // TODO: not done! installuserfont (name) { let idnum = BGI.fontFileIds[name]; // TODO: load font file? return (idnum) ? idnum : 0; // TEST } // Draws a line in the current color, using the current write mode, line style, and thickness // between the two points without updating the current position (info.cp). line (x1, y1, x2, y2, color = this.info.fgcolor, wmode = this.info.writeMode, linestyle = this.info.line.style, thickness = this.info.line.thickness) { this._line(x1, y1, x2, y2, color, wmode, linestyle, thickness); } _line (x1, y1, x2, y2, color = this.info.fgcolor, wmode = this.info.writeMode, linestyle = this.info.line.style, thickness = this.info.line.thickness) { // TODO: pass in 'linesettingstype' object instead? // if y2 < y1, swap points if (y2 < y1) { let tmp = y1; y1 = y2; y2 = tmp; tmp = x1; x1 = x2; x2 = tmp; } // TEST // Trying to fix bottom edge lines in FRACTMTN.RIP // if points outside viewport, move to inside. Not sure if this should be canvas bounds. // Moving point inside viewport fixes RIPDELIC.RIP const vp = this.info.vp; if (y1 < vp.top) { y1 = vp.top; } if (y1 > vp.bottom) { return; } if (y2 < vp.top) { return; } //if (y2 > vp.bottom) { y2 = vp.bottom + 1; } if (y2 > vp.bottom) { y2 = vp.bottom; } if (x1 < vp.left) { x1 = vp.left; } if (x1 > vp.right) { return; } if (x2 < vp.left) { return; } if (x2 > vp.right) { x2 = vp.right; } // set pattern // TODO: should this be here, or passed into line()? let upattern = (linestyle < BGI.USERBIT_LINE) ? BGI.line_patterns[linestyle] : this.info.line.upattern; // first line this.line_bresenham(x1, y1, x2, y2, color, wmode, upattern); // 2nd & 3rd lines if (thickness === BGI.THICK_WIDTH) { const oct = this.octant(x2 - x1, y1 - y2); if ((oct === 1) || (oct === 4) || (oct ===5) || (oct === 8)) { this.line_bresenham(x1, y1 - 1, x2, y2 - 1, color, wmode, upattern); this.line_bresenham(x1, y1 + 1, x2, y2 + 1, color, wmode, upattern); } else { this.line_bresenham(x1 - 1, y1, x2 - 1, y2, color, wmode, upattern); this.line_bresenham(x1 + 1, y1, x2 + 1, y2, color, wmode, upattern); } } } // Draws a line from the CP (current position) to a point that is a relative distance (dx,dy) // from the CP. The CP is advanced by (dx,dy). linerel (dx, dy, color = this.info.fgcolor, wmode = this.info.writeMode, linestyle = this.info.line.style, thickness = this.info.line.thickness) { this.line(this.info.cp.x, this.info.cp.y, this.info.cp.x + dx, this.info.cp.y + dy, color, wmode, linestyle, thickness); this.info.cp.x += dx; this.info.cp.y += dy; } // Draws a line from the CP (current position) to (x,y), then moves the CP to (x,y). lineto (x, y, color = this.info.fgcolor, wmode = this.info.writeMode, linestyle = this.info.line.style, thickness = this.info.line.thickness) { this.line(this.info.cp.x, this.info.cp.y, x, y, color, wmode, linestyle, thickness); this.info.cp.x = x; this.info.cp.y = y; } _lineto (x, y, color = this.info.fgcolor, wmode = this.info.writeMode, linestyle = this.info.line.style, thickness = this.info.line.thickness) { this._line(this.info.cp.x, this.info.cp.y, x, y, color, wmode, linestyle, thickness); this.info.cp.x = x; this.info.cp.y = y; } // Moves the CP by (dx, dy) pixels. moverel (dx, dy) { this.info.cp.x += dx; this.info.cp.y += dy; } // Moves the CP to the position (x, y) relative to the viewport. moveto (x, y) { this.info.cp.x = x; this.info.cp.y = y; } // Draws text using current info.cp position, info.text.charsize and info.text.direction outtext (text) { this._outtext(text); } _outtext (text) { if (typeof text !== "string") { return } // set yoffset for scalable fonts first let yoffset = 0; const fontnum = this.info.text.font; const scale = this.info.text.charsize; if (fontnum === BGI.DEFAULT_FONT) { // move cp if vertical if (this.info.text.direction === BGI.VERT_DIR) { this.moverel(-this.textheight(text), this.textwidth(text) - scale); } // loop thru each character in text string text.split('').forEach(c => { const cvalue = c.charCodeAt(0) & 0xFF; // to strip out 2nd byte this.drawPNGChar(cvalue, 0, this.info.text.charsize, this.info.text.direction); }); } else if (fontnum < BGI.fontFileList.length) { const fontname = BGI.fontFileList[fontnum]; if (this.info.text.direction === BGI.HORIZ_DIR) { this.moverel(0, this.textheight(text)); } else { this.moverel(this.textheight(text), this.textwidth(text)); } // loop thru each character in text string text.split('').forEach(c => { const cvalue = c.charCodeAt(0) & 0xFF; // to strip out 2nd byte this.drawChar(cvalue, fontname, scale, this.info.text.direction); }); } } // Draws text after offseting position by (x, y). outtextxy (x, y, text) { this.moveto(x, y); this.outtext(text); } _outtextxy (x, y, text) { this.moveto(x, y); this._outtext(text); } // fills with fill color and pattern. // outlines with fgcolor and thickness. doesn't use line style. pieslice (cx, cy, stangle, endangle, radius) { if ((radius < 1) || (stangle === endangle)) { return; } // TODO: test against reference // adjust radius based on aspect ratio const yradius = Math.floor( radius * (this.aspect.xasp / this.aspect.yasp) ); this.sector(cx, cy, stangle, endangle, radius, yradius); } _pieslice (cx, cy, stangle, endangle, radius) { if ((radius < 1) || (stangle === endangle)) { return; } // TODO: test against reference const yradius = Math.floor( radius * (this.aspect.xasp / this.aspect.yasp) ); this._sector(cx, cy, stangle, endangle, radius, yradius); } // Put byte array of image data on to pixels[] // image is an object: // { x:int, y:int, width:int, height:int, data:Uint8ClampedArray } // wmode = write mode { 0=COPY_PUT, 1=XOR_PUT, 2=OR_PUT, 3=AND_PUT, 4=NOT_PUT } // Image is offset by viewport. // May draw past right edge of vp, or exits early if past right edge of canvas. // Will draw past bottom edge of vp and clip at edge of canvas. // putimage (left, top, image, wmode, defaults = {}) { return this._putimage(left, top, image, wmode, defaults); } _putimage (left, top, image, wmode, defaults = {}) { // ORIGINAL // putimage (left, top, bitmap, op) // bitmap = points to where image stored // op = see putimage_ops enum (0=COPY, 1=XOR, 2=OR, 3=AND, 4=NOT) let { pix = this.pixels, info = this.info, width = this.width, height = this.height, colorMask = this.colorMask, } = defaults; if (!(image && image.data)) { this.log('err', 'putimage() missing image!'); return false; } // offset by viewport if (info && info.vp) { left += info.vp.left; top += info.vp.top; } let data = image.data; // Uint8ClampedArray let right = left + image.width - 1; let bottom = top + image.height - 1; let i=0, o=0, yi=0; // NOTE: the follow occurs under RIP 1.54 // but may clip to viewport under 2.0? // (according to specs, not tested.) // exit early if image past right edge of canvas if (right > width - 1) { this.log('bgi', `putimage() ignored: right edge (${right}) past canvas edge.`); return false; } // draws past bottom of vp, but clips at canvas bottom if (bottom > height - 1) { bottom = height - 1; } switch (wmode) { default: case BGI.COPY_PUT: for (let y=top; y <= bottom; ++y) { i = yi * image.width; o = y * width + left; for (let x=left; x <= right; ++x) { pix[o++] = data[i++]; } yi++; } break; case BGI.XOR_PUT: for (let y=top; y <= bottom; ++y) { i = yi * image.width; o = y * width + left; for (let x=left; x <= right; ++x) { pix[o++] ^= data[i++]; } yi++; } break; case BGI.OR_PUT: for (let y=top; y <= bottom; ++y) { i = yi * image.width; o = y * width + left; for (let x=left; x <= right; ++x) { pix[o++] |= data[i++]; } yi++; } break; case BGI.AND_PUT: for (let y=top; y <= bottom; ++y) { i = yi * image.width; o = y * width + left; for (let x=left; x <= right; ++x) { pix[o++] &= data[i++]; } yi++; } break; case BGI.NOT_PUT: for (let y=top; y <= bottom; ++y) { i = yi * image.width; o = y * width + left; for (let x=left; x <= right; ++x) { pix[o++] = ~data[i++] & colorMask; } yi++; } } return true; } // returns a non-uniform random integer >= 0 and < range. random (range) { return Math.floor(Math.random() * range); } // Fetch an ICN file from cache or online. // returns an image or empty object // async readimagefile (filename) { // readimagefile (filename, left, top, right, bottom) { // const char* filename=NULL, int left=0, int top=0, int right=INT_MAX, int bottom=INT_MAX // reads a BMP, GIF, JPG, ICON, EMF, or WMF image file. // displays it in part of the current active window. // unknown: would x2,y2 crop the image if under size of image? (I'd think so) // NOT DONE: need to check BGI spec! // retrieve image from cache first, if available let image = this.icons[filename]; if (!image) { const url = this.iconsPath + '/' + filename; image = await this.fetchIcon(url); if (image) { // store in cache this.icons[filename] = image; } } // this doesn't display the image, but instead returns it for use in putimage() return image || {}; } // draws in current line style, thickness, and drawing color rectangle (left, top, right, bottom, color = this.info.fgcolor, wmode = this.info.writeMode) { this._rectangle(left, top, right, bottom, color, wmode); } _rectangle (left, top, right, bottom, color = this.info.fgcolor, wmode = this.info.writeMode) { // swap if (left > right) { let tmp = left; left = right; right = tmp; } if (top > bottom) { let tmp = top; top = bottom; bottom = tmp; } this._line(left, top, right, top, color, wmode); this._line(right, top, right, bottom, color, wmode); this._line(right, bottom, left, bottom, color, wmode); this._line(left, bottom, left, top, color, wmode); } // don't implement these (DOS only) (STUB) registerbgidriver () { // registerbgidriver(void (*driver)(void)) { } this.unimplemented('registerbgidriver'); } registerbgifont () { //registerbgifont(void (*font)(void)) { } this.unimplemented('registerbgifont'); } // don't implement (DOS only) (STUB) // only makes sense when switching to text mode in DOS. restorecrtmode (/* void */) { this.unimplemented('restorecrtmode'); } // Draws and fills an elliptical pie slice centered at (x, y). _sector (cx, cy, stangle, endangle, xradius, yradius) { this.sector(cx, cy, stangle, endangle, xradius, yradius); } sector (cx, cy, stangle, endangle, xradius, yradius) { // Outlines using current color, filled using pattern & color. // from setfillstyle & setfillpattern. // Angles in degrees, counter-clockwise: 0=3o'clock, 90=12o'clock // draw a single pixel at center instead if (stangle === endangle) { this.putpixel(cx, cy); return; } // TODO: don't know if these are correct, or should we exit? if (xradius < 1) { xradius = 1; } if (yradius < 1) { yradius = 1; } // swap if start > end (unlike elliptical arcs!) // some pies may not be represented if angles > 360 aren't allowed! if (stangle > endangle) { let t = stangle; stangle = endangle; endangle = t; } // draw outline // (doesn't use same algorithm as thick ellipses!) //this._ellipse(cx, cy, stangle, endangle, xradius, yradius); this.arc_bresenham(cx, cy, stangle, endangle, xradius, yradius); // calculate start and end pixels for pie wedge let endangle2 = endangle % 360; // fix for specific case const twoPiD = 2 * Math.PI / 360; let x1 = cx + Math.floor(xradius * Math.cos(stangle * twoPiD)); let y1 = cy - Math.floor(yradius * Math.sin(stangle * twoPiD)); let x2 = cx + Math.floor(xradius * Math.cos(endangle2 * twoPiD)); let y2 = cy - Math.floor(yradius * Math.sin(endangle2 * twoPiD)); // draw pie wedge lines to center this._line(cx, cy, x1, y1); this._line(cx, cy, x2, y2); // TODO: Pie filling // May need something like a modified fillellipse_bresenham() // which I haven't figured out how to do yet... // Alternately use floodfill, which is not perfect. // also floodfill likely wouldn't work in XOR_PUT write mode! // To pick a point for the floodfill, take the angle // halfway between the start & end, find that point // along ellipse, then take midpoint to center. // fill pie using floodfill let half_angle = (endangle - stangle) / 2 + stangle; // assuming stangle < endangle let fx = Math.round((xradius * Math.cos(half_angle * twoPiD)) / 2 + cx); let fy = Math.round((yradius * -Math.sin(half_angle * twoPiD)) / 2 + cy); this._floodfill (fx, fy, this.info.fgcolor); } // active page where all subsequent graphics output goes setactivepage (page) { } setallpalette (p) { // void setallpalette (struct palettetype *palette); // This is a little different and might not want to use? // To set a palette color to an RGB color, use setrgbpalette() } // Used to make sure circles are round. // SEE getaspectratio() setaspectratio (xasp, yasp) { this.aspect = { xasp: xasp, yasp: yasp }; } // sets background to given index color. // WIN version doesn't change the 0 pallete, so it's not instant, // but any future drawings will use what is currently in color at index [color]. setbkcolor (color) { this.info.bgcolor = color; } // sets current drawing color: 0 to getmaxcolor(), usually 0-15. WIN allows RGB COLOR() setcolor (color) { this.info.fgcolor = color; } // fpattern is an array of 8 ints (0-255), each byte is 8 pixels in the pattern. setfillpattern (fpattern, color) { // void setfillpattern(char *upattern, int color); this.info.fill.color = color; if (fpattern && (fpattern.length === 8)) { this.info.fill.style = BGI.USER_FILL; this.info.fill.fpattern = fpattern; } } // style is index 0-11 into fill_patterns[] // pattern bits 0=current bg color, 1=color (passed in, not global color) setfillstyle (style, color) { this.info.fill.color = color; if ((style >= 0) && (style <= BGI.USER_FILL)) { this.info.fill.style = style; this.info.fill.fpattern = BGI.fill_patterns[style]; } } // don't implement (STUB) setgraphbufsize (size) { // unsigned setgraphbufsize(unsigned bufsize); // tells how much memory to allocate for internal graphics buffer used by floodfill. this.unimplemented('setgraphbufsize'); } // don't implement?? (STUB) setgraphmode (mode) { // selects graphics mode different than default set by initgraph() // clears screen, resets graphics to defaults. this.unimplemented('setgraphmode'); } setlinestyle (linestyle, upattern = 0xFFFF, thickness = 1) { // void setlinestyle (int linestyle, unsigned upattern, int thickness); // linestyle value from line_styles enum. // upattern is a 16-bit int (e.g. 0xFFFF is a solid line). // upattern only applies if linestyle == 4 (USERBIT_LINE) // The linestyle doesn't effect arcs, circles, ellipses, or pie slices. Only thickness does. this.info.line.style = linestyle; this.info.line.upattern = upattern; this.info.line.thickness = thickness; } // Set colornum index in palette (0-15) to color (0-15), or RGB color using COLOR() in WIN. // TODO: should we support EGA palette colors (0-63)? setpalette (colornum, color) { if (BGI.IS_RGB_COLOR(color)) { this.setrgbpalette(colornum, BGI.RED_VALUE(color), BGI.GREEN_VALUE(color), BGI.BLUE_VALUE(color)); } else if (BGI.IS_BGI_COLOR(color)) { // lookup bgi color const c = BGI.bgi_palette[color]; this.setrgbpalette(colornum, BGI.RED_VALUE(c), BGI.GREEN_VALUE(c), BGI.BLUE_VALUE(c)); } else { this.log('bgi', `setpalette() color [${color}] not supported!`); } } // colornum can be 0-15 or 0-255 depending on the grapics mode? // red, green, blue are 0-255, but only the top 6 bits of byte are used in BGI and WIN. // (but this implementation will use all 8 bits.) setrgbpalette (colornum, red, green, blue, alpha = 255, defaults = {}) { // setrgbpalette(int colornum, int red, int green, int blue); const { pal = this.palette } = defaults; const pi = colornum * 4; if (pi < pal.length) { // RGBA32 format: R, G, B, A pal[pi + 0] = red; pal[pi + 1] = green; pal[pi + 2] = blue; pal[pi + 3] = alpha; } else { this.log('bgi', `setrgbpalette() colornum [${colornum}] out of range!`); } } // TODO // affects text written settextjustify (horiz, vert) { // SEE text_just enums // SEE https://www.cs.colorado.edu/~main/bgi/doc/settextjustify.html // Text output after a call to settextjustify is justified around the current position (CP) horizontally // and vertically, as specified. The default justification settings are LEFT_TEXT (for horizontal) and // TOP_TEXT (for vertical). If horiz is equal to LEFT_TEXT and direction equals HORIZ_DIR, the CP's // x component is advanced after a call to outtext(string) by textwidth(string). } settextstyle (font, direction, charsize) { // SEE https://www.cs.colorado.edu/~main/bgi/doc/settextstyle.html this.info.text.font = font; this.info.text.direction = direction; this.info.text.charsize = charsize; } // TODO setusercharsize (multx, divx, multy, divy) { // setusercharsize gives finer control over the size of text from stroked fonts. // SEE https://www.cs.colorado.edu/~main/bgi/doc/setusercharsize.html } // Sets the current viewport for graphics output. // Current position (CP) is moved to (0,0) in the new window. (so coords change??) // If clip is true, all drawings will be clipped to the current viewport. setviewport (x1, y1, x2, y2, clip) { // swap if (x1 > x2) { let tmp = x1; x1 = x2; x2 = tmp; } if (y1 > y2) { let tmp = y1; y1 = y2; y2 = tmp; } if ((x1 >= 0) && (x2 < this.width) && (y1 >= 0) && (y2 < this.height)) { this.info.vp.left = x1; this.info.vp.top = y1; this.info.vp.right = x2; this.info.vp.bottom = y2; this.info.vp.width = x2 - x1 + 1; this.info.vp.height = y2 - y1 + 1; this.info.vp.clip = clip; this.info.cp.x = 0; this.info.cp.y = 0; this.log('bgi', `viewport set to (${x1},${y1})-(${x2},${y2})`); // DEBUG } } _resetViewport () { this.info.vp = { left: 0, top: 0, right: (this.width - 1), bottom: (this.height - 1), width: this.width, height: this.height, clip: false }; } // likely won't implement (STUB) // makes page the visual graphics page. setvisualpage (page) { this.unimplemented('setvisualpage'); } // mode = COPY_PUT (0), XOR_PUT (1) // works only with line(), linerel(), lineto(), rectangle(), drawpoly() setwritemode (mode) { //this.log('bgi', `write mode set to ${mode}`); // DEBUG this.info.writeMode = mode; } // check every char in text and return true if at least 1 char has a descender. _textHasDescender (text) { if (typeof text !== "string") { return false; } let drop = false; text.split('').forEach(c => { let cvalue = c.charCodeAt(0) & 0xFF; // strip out 2nd byte if (BGI.charDescenders[cvalue] === 1) { drop = true; } }); return drop; } // Takes current font size and multiplication factor, and determines the height of text in pixels. // flags (optional): // 0 = BGI.VAR_HEIGHT: // Scans text for any descenders and returns full height if there are any, else only main height. // 1 = BGI.MAIN_HEIGHT: Returns main height between the baseline and top of text. // 2 = BGI.DROP_HEIGHT: Returns drop height from the baseline to bottom of descender. // 3 = BGI.FULL_HEIGHT: Returns main + drop height. (default) // 4 = BGI.ABOVE_HEIGHT: Returns distance from top of cell to top of text. textheight (text, flags = BGI.FULL_HEIGHT) { if (typeof text !== "string") { return 0; } let th = 0; const fontnum = this.info.text.font; const scale = this.info.text.charsize; if (fontnum === BGI.DEFAULT_FONT) { let dropv = this._textHasDescender(text) ? 1 : 0; switch (flags) { case BGI.VAR_HEIGHT: th = scale * (8 + dropv); break; case BGI.MAIN_HEIGHT: th = scale * 8; break; case BGI.DROP_HEIGHT: th = scale * dropv; break; case BGI.ABOVE_HEIGHT: th = 0; break; default: th = scale * 8; // full height } } else if (fontnum < BGI.fontFileList.length) { const fontname = BGI.fontFileList[fontnum]; const font = this.fonts[fontname]; if (font) { const actualScale = (scale < BGI.fontScales.length) ? BGI.fontScales[scale] : 1; let h = 0; switch (flags) { case BGI.VAR_HEIGHT: // check every char in text and add drop height only if at least 1 char has a descender. h = this._textHasDescender(text) ? (font.top - font.bottom) : font.top; th = Math.trunc(h * actualScale); break; case BGI.MAIN_HEIGHT: th = Math.trunc(font.top * actualScale); break; case BGI.DROP_HEIGHT: th = Math.trunc(-font.bottom * actualScale); break; case BGI.ABOVE_HEIGHT: th = Math.round(BGI.fontPadAbove[fontnum] * actualScale); // round() or trunc()?? break; default: // BGI.FULL_HEIGHT th = Math.trunc((font.top - font.bottom) * actualScale); } } } return th; } // takes the string length, current font size, and multiplication factor, and determines the width of text in pixels. textwidth (text) { if (typeof text !== "string") { return 0; } let tw = 0; const fontnum = this.info.text.font; const scale = this.info.text.charsize; if (fontnum === BGI.DEFAULT_FONT) { tw = text.length * 8 * scale; } else if (fontnum < BGI.fontFileList.length) { // CHR font widths const fontname = BGI.fontFileList[fontnum]; const font = this.fonts[fontname]; if (font) { const actualScale = (scale < BGI.fontScales.length) ? BGI.fontScales[scale] : 1; // loop thru each character in text string text.split('').forEach(c => { let cvalue = c.charCodeAt(0) & 0xFF; // to strip out 2nd byte let charwidth = font.widths[cvalue - font.firstchar]; tw += (charwidth * actualScale); }); } } return Math.floor(tw); } //////////////////////////////////////////////////////////////////////////////// // BGI for Windows Color functions // Do we need EGA palette specific functions? // Didn't check this code against reference. // r,g,b range 0-255 // returns packed ARGB values (0xAARRGGBB) in C++ style static COLOR (r, g, b) { return 0xFF000000 | r << 16 | g << 8 | b; } static IS_BGI_COLOR (color) { return ((color >= 0) && (color < 16)); } static IS_RGB_COLOR (color) { return ((color & 0xFF000000) !== 0); } // these return the color component of an RGB color static RED_VALUE (rgb_color) { return (rgb_color & 0xFF0000) >> 16; } static GREEN_VALUE (rgb_color) { return (rgb_color & 0xFF00) >> 8; } static BLUE_VALUE (rgb_color) { return (rgb_color & 0xFF); } //////////////////////////////////////////////////////////////////////////////// // BGI for Windows functions (some not present) closegraph (win) { this.refresh(); } // NOT IN SDL_bgi // returns an RGB color from a BGI/index color converttorgb (color) { return 0; } async delay (millisec) { return new Promise(resolve => setTimeout(resolve, millisec)); } getactivepage (/* void */) { this.unimplemented('getactivepage'); } getdisplaycolor () { } // EXTRA getch (/* void */) { this.unimplemented('getch'); return 0; // int } bgi_getch = this.getch; getvisualpage (/* void */) { this.unimplemented('getvisualpage'); return 0; // int } // returns true if a char is in input buffer, else false. kbhit (/* void */) { return false; // int } // returns the most recent x coord of the mouse within the graphics window. mousex (/* void */) { return this.mouseX; } // returns the most recent y coord of the mouse within the graphics window. mousey (/* void */) { return this.mouseY; } // Calculate mouse coordinates. // provided Event e, returns translated mouse coords [x, y] // FIXME: may be off by 1 pixel? // _mouseCoords (e) { // clientWidth or Height could be off if canvas padding not 0? const x = Math.floor(e.offsetX * (e.target.width / e.target.clientWidth)) - 1; const y = Math.floor(e.offsetY * (e.target.height / e.target.clientHeight)) - 1; return [x, y]; } // 2nd arg is a callback function sent x,y mouse position, // which is currently called whenever the mouse moves over canvas. // 'kind' is an object, see "mouse events" class constants. // original: registermousehandler(int kind, void h(int x, int y)); // registermousehandler (kind, callback) { const etype = kind.type || ''; const ebutton = kind.button; if ((typeof callback === 'function') && (etype !== '')) { this.ctx.canvas.addEventListener(etype, (e) => { if ((e.button === ebutton) || (ebutton === -1)) { const [x, y] = this._mouseCoords(e); callback(x, y); } }); } } setcurrentwindow (win) { // not really needed? Assumes multiple windows. // We can use multiple instances of the BGI class, one for each canvas, // instead of using multiple windows. this.unimplemented('setcurrentwindow'); } // NOT IN BGI nor WIN EXTRA, but from WinGraph (not official) // floodmode = // BORDER_FLOOD: fill area is bounded by color // SURFACE_FLOOD: fill regions containing color setfloodmode (floodmode) { } } //////////////////////////////////////////////////////////////////////////////// if (typeof window !== 'undefined') { window.BGI = BGI; } // class constants // fonts BGI.DEFAULT_FONT=0; BGI.TRIPLEX_FONT=1; BGI.SMALL_FONT=2; BGI.SANSSERIF_FONT=3; BGI.GOTHIC_FONT=4; BGI.SCRIPT_FONT=5; BGI.SIMPLEX_FONT=6; BGI.TRIPLEX_SCR_FONT=7; BGI.COMPLEX_FONT=8; BGI.EUROPEAN_FONT=9; BGI.BOLD_FONT=10; // text alignment used in settextjustify() BGI.HORIZ_DIR=0; BGI.VERT_DIR=1; // text_just enum BGI.LEFT_TEXT=0; BGI.CENTER_TEXT=1; BGI.RIGHT_TEXT=2; BGI.BOTTOM_TEXT=0; BGI.TOP_TEXT=2; // or is it?? top=0 bottom=1 baseline=2 // BGI colors BGI.BLACK=0; BGI.BLUE=1; BGI.GREEN=2; BGI.CYAN=3; BGI.RED=4; BGI.MAGENTA=5; BGI.BROWN=6; BGI.LIGHTGRAY=7; BGI.DARKGRAY=8; BGI.LIGHTBLUE=9; BGI.LIGHTGREEN=10; BGI.LIGHTCYAN=11; BGI.LIGHTRED=12; BGI.LIGHTMAGENTA=13; BGI.YELLOW=14; BGI.WHITE=15; BGI.MAXCOLORS=15; BGI.BGI_COLORS=16; // do we need this? // line style, thickness, and drawing mode BGI.SOLID_LINE=0; BGI.DOTTED_LINE=1; BGI.CENTER_LINE=2; BGI.DASHED_LINE=3; BGI.USERBIT_LINE=4; // line_styles enum BGI.NORM_WIDTH=1; BGI.THICK_WIDTH=3; BGI.COPY_PUT=0; BGI.XOR_PUT=1; BGI.OR_PUT=2; BGI.AND_PUT=3; BGI.NOT_PUT=4; // putimage_ops enum // fill styles BGI.EMPTY_FILL=0; BGI.SOLID_FILL=1; BGI.LINE_FILL=2; BGI.LTSLASH_FILL=3; BGI.SLASH_FILL=4; BGI.BKSLASH_FILL=5; BGI.LTBKSLASH_FILL=6; BGI.HATCH_FILL=7; BGI.XHATCH_FILL=8; BGI.INTERLEAVE_FILL=9; BGI.WIDE_DOT_FILL=10; BGI.CLOSE_DOT_FILL=11; BGI.USER_FILL=12; // numbers BGI.PI_CONV = (3.1415926 / 180.0); // the following are not in BGI spec BGI.fontFileIds = { 'TRIP.CHR': BGI.TRIPLEX_FONT, 'LITT.CHR': BGI.SMALL_FONT, 'SANS.CHR': BGI.SANSSERIF_FONT, 'GOTH.CHR': BGI.GOTHIC_FONT, 'SCRI.CHR': BGI.SCRIPT_FONT, 'SIMP.CHR': BGI.SIMPLEX_FONT, 'TSCR.CHR': BGI.TRIPLEX_SCR_FONT, 'LCOM.CHR': BGI.COMPLEX_FONT, 'EURO.CHR': BGI.EUROPEAN_FONT, 'BOLD.CHR': BGI.BOLD_FONT }; BGI.fontFileList = [ '', 'TRIP.CHR', 'LITT.CHR', 'SANS.CHR', 'GOTH.CHR', 'SCRI.CHR', 'SIMP.CHR', 'TSCR.CHR', 'LCOM.CHR', 'EURO.CHR', 'BOLD.CHR' ]; // actual scale values to multiply by given size index. BGI.fontScales = [ 1, 0.6, 2/3, 0.75, 1, 4/3, 5/3, 2, 2.5, 3, 4 ]; // used in textheight() BGI.VAR_HEIGHT=0; BGI.MAIN_HEIGHT=1; BGI.DROP_HEIGHT=2; BGI.FULL_HEIGHT=3; BGI.ABOVE_HEIGHT=4; // num pixels between top of cell and top of font when scale = 4. BGI.fontPadAbove = [ 0, 10, 3, 11, 11, 16, 14, 9, 13, 12, 19 ]; // font 0 to 10 // ASCII chars that have descenders. 1 = char goes below baseline. BGI.charDescenders = [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0, 1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1, 0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0 ]; // windows mouse events BGI.WM_MOUSEMOVE = { type:'mousemove', button:-1}; // called whenever the mouse moves BGI.WM_LBUTTONDBLCLK = { type:'dblclick', button:0 }; // left mouse button is double clicked BGI.WM_LBUTTONDOWN = { type:'mousedown', button:0 }; // left mouse button is clicked down BGI.WM_LBUTTONUP = { type:'mouseup', button:0 }; // left mouse button is released up BGI.WM_MBUTTONDBLCLK = { type:'dblclick', button:1 }; // middle mouse button is double clicked BGI.WM_MBUTTONDOWN = { type:'mousedown', button:1 }; // middle mouse button is clicked down BGI.WM_MBUTTONUP = { type:'mouseup', button:1 }; // middle mouse button is released up BGI.WM_RBUTTONDBLCLK = { type:'dblclick', button:2 }; // right mouse button is double clicked BGI.WM_RBUTTONDOWN = { type:'mousedown', button:2 }; // right mouse button is clicked down BGI.WM_RBUTTONUP = { type:'mouseup', button:2 }; // right mouse button is released up ////////////////////////////////////////////////////////////////////////////////