mirror of
https://github.com/milk-net/milk-net.github.io.git
synced 2025-04-18 17:23:40 -05:00
2241 lines
109 KiB
JavaScript
2241 lines
109 KiB
JavaScript
/*
|
|
Teletext rendering engine broken out of editor code in an attempt to make it more general purpose.
|
|
See "public functions and variables" section for general purpose rendering hooks.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
let createCanvas;
|
|
let chardataImage;
|
|
|
|
if (typeof window === 'undefined') {
|
|
// node.js module specific code
|
|
const Canvas = require('canvas');
|
|
createCanvas = Canvas.createCanvas;
|
|
|
|
chardataImage = new Canvas.Image();
|
|
|
|
module.exports = { TeletextRenderer };
|
|
|
|
setTimeout = function(){};
|
|
setInterval = function(){};
|
|
}
|
|
else
|
|
{
|
|
// browser specific code
|
|
createCanvas = function (width, height) {
|
|
return Object.assign(document.createElement('canvas'), { width: width, height: height })
|
|
}
|
|
|
|
chardataImage = new Image();
|
|
}
|
|
|
|
/* teletextCanvas scales up to fill optional screenDiv */
|
|
function TeletextRenderer(teletextCanvas, backgroundCanvas, screenDiv={}){
|
|
/* page data arrays */
|
|
let level1PageArray = []; // packets 0 to 24
|
|
let packetX26TripletArray = []; // array of 18 bit enhancement functions
|
|
|
|
let screenAttributesArray = [];
|
|
// at each screen coordinate (y,x) object has the following properties:
|
|
// switchG0: false = default G0, true = second G0.
|
|
// mosaics: false = alpha, true = mosaics. Only set from spacing attributes!
|
|
// dh: double height, dw: double width, br: bottom row, rh: right half
|
|
// flash: bits 0 and 1 are flash mode, bits 2 and 3 are rate and phase
|
|
// charcode: character code
|
|
// charSet: character set data
|
|
// NOS: National option subset 0 = none
|
|
// foreColour: foreground colour number
|
|
// backColour: background colour number
|
|
// diacritic: G2 diacritic between 0 and 15
|
|
// boxed, conceal, invert
|
|
// separated: separated/underline
|
|
// bold, italic, proportional
|
|
|
|
let CLUT = []; // 32 colour look up table
|
|
|
|
/* default CLUT and CLUT mappings from teletext spec */
|
|
let defaultCLUT = [ 0x000,0xF00,0x0F0,0xFF0,0x00F,0xF0F,0x0FF,0xFFF,
|
|
0x000,0x700,0x070,0x770,0x007,0x707,0x077,0x777,
|
|
0xF05,0xF70,0x0F7,0xFFB,0x0CA,0x500,0x652,0xC77,
|
|
0x333,0xF77,0x7F7,0xFF7,0x77F,0xF7F,0x7FF,0xDDD ];
|
|
let foregroundMapping = [0,0,0,1,1,2,2,2];
|
|
let backgroundMapping = [0,1,2,1,2,1,2,3];
|
|
|
|
let pagePresentationData = {};
|
|
|
|
let pageSettings = {newsflash:false, subtitle:false, suppressHeader:false, inhibitDisplay:false};
|
|
|
|
let leftSidePanel;
|
|
let rightSidePanel;
|
|
let sidePanelStatusFlag;
|
|
let sidePanelColumns;
|
|
let fullScreenColour;
|
|
let fullRowColour = [];
|
|
let blackBackgroundColourSubstitution;
|
|
let colourMapping;
|
|
|
|
let defaultG0Set;
|
|
let defaultNOS;
|
|
let defaultG2Set;
|
|
let secondG0Set;
|
|
let secondNOS;
|
|
let defaultScreenColour;
|
|
let defaultRowColour;
|
|
|
|
let showCursor = false;
|
|
let cursorPosition = [];
|
|
|
|
let displayRow24 = true;
|
|
let displayRow25 = false;
|
|
|
|
let firstRow = 1;
|
|
let firstColumn = 0;
|
|
let lastRow = 24;
|
|
let lastColumn = 39;
|
|
|
|
let visibleControlChars = false;
|
|
let showGrid = false;
|
|
let reveal = false;
|
|
let widescreen = false;
|
|
let mixMode = false;
|
|
let stretchMode = 0;
|
|
let renderLevel = 0; // 0 = L1, 1 = L1.5, 2 = L2.5, 3 = L3.5
|
|
|
|
let newsflash;
|
|
let subtitle;
|
|
let suppressHeader;
|
|
let inhibitDisplay;
|
|
|
|
let hideTeletext;
|
|
let numberEntry;
|
|
let pageSearch;
|
|
|
|
/* charset stuff */
|
|
let charsetCanvas;
|
|
let charsetCanvasContext;
|
|
let controlCodesImageData = [];
|
|
let charsetImageData = [];
|
|
let NOSImageData = [];
|
|
let extraCharsData = []; // ISO 6937 diacritics
|
|
let ISO6937LUT1 = []; // look up table for diacritics
|
|
let ISO6937LUT2 = []; // look up table for diacritics
|
|
let gdrcsArray = []; // array of gdrcs data
|
|
let drcsArray = []; // array of drcs data
|
|
let gpopArray = []; // array of global public objects
|
|
let popArray = []; // array of public objects
|
|
let defaultObjects = []; // array of default (G)POP objects
|
|
// 0-1 are GPOP 1 and 2, 2-3 are POP 1 and 2
|
|
// each has a flag, subpage, and object value.
|
|
|
|
let bufferToImageDataArray = function(buffer,offset,characters){
|
|
let imageDataArray = [];
|
|
for (let character = 0; character < characters; character++){
|
|
let data = [];
|
|
for (let y = 0; y < 10; y++){
|
|
for (let x = 0; x < 12; x++){
|
|
data[(y*12+x)] = buffer[(character * 120) + ((offset + y) * 12) + x] & 1;
|
|
}
|
|
}
|
|
imageDataArray[character] = data;
|
|
}
|
|
return imageDataArray;
|
|
}
|
|
|
|
let initCharsetData = function(){
|
|
/* initialise charsets */
|
|
charsetCanvas = createCanvas(12, 17000);
|
|
charsetCanvasContext = charsetCanvas.getContext("2d");
|
|
|
|
charsetCanvasContext.imageSmoothingEnabled = false;
|
|
charsetCanvasContext.msImageSmoothingEnabled = false;
|
|
charsetCanvasContext.drawImage(chardataImage,0,0);
|
|
|
|
let imageData = charsetCanvasContext.getImageData(0,0,12,17000);
|
|
let buffer = new Uint32Array(imageData.data.buffer);
|
|
|
|
for (let i = 0; i < 14; i++){
|
|
charsetImageData.push(bufferToImageDataArray(buffer, i*960, 0x60));
|
|
/* charsets pushed to array: latinG0, cyrillicG01, cyrillicG02, cyrillicG03, greekG0, arabicG0, hebrewG0, latinG2, cyrillicG2, greekG2, arabicG2, contiguousG1, separatedG1, G3 */
|
|
}
|
|
|
|
controlCodesImageData = bufferToImageDataArray(buffer, 13440, 0x20);
|
|
|
|
/* create an array of image data for the national option subsets */
|
|
for (let i=0; i<13; i++){
|
|
NOSImageData[i] = bufferToImageDataArray(buffer, 13760+(130*i), 13);
|
|
}
|
|
|
|
extraCharsData = bufferToImageDataArray(buffer, 15450, 155);
|
|
|
|
// no diacritic, Grave, Acute, Circumflex, Tilde, Macron, Breve, Dot, Umlaut/diæresis, Bottom dot not in ISO6937, Ring, Cedilla, Underscore not in ISO6937, Double acute, Ogonek, Caron
|
|
ISO6937LUT1 = ["","AEIOUaeiou","ACEILNORSUYZacegilnorsuyz","ACEGHIJOSUWYaceghijosuwy","AINOUainou","AEIOUaeiou","AGUagu","CEGIZcegz","AEIOUYaeiouy","","AUau","CGKLNRSTcklnrst","","OUou","AEIUaeiu","CDELNRSTZcdelnrstz"];
|
|
|
|
// offset to start of diacritic
|
|
ISO6937LUT2 = [0, 0, 10, 35, 59, 69, 79, 85, 94,, 106, 110,, 125, 129, 137];
|
|
}
|
|
|
|
let setCharacterSets = function(defaultG0andG2, secondG0andNOS){
|
|
switch(defaultG0andG2){
|
|
default: // use English if an invalid value is recieved
|
|
case 0x00:
|
|
case 0x10:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 2; // English
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x01:
|
|
case 0x09:
|
|
case 0x11:
|
|
case 0x21:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 5; // German
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x02:
|
|
case 0x0A:
|
|
case 0x12:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 12; // Swedish/Finnish/Hungarian
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x03:
|
|
case 0x0B:
|
|
case 0x13:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 6; // Italian
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x04:
|
|
case 0x0C:
|
|
case 0x14:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 4; // French
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x05:
|
|
case 0x15:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 9; // Portuguese/Spanish
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x06:
|
|
case 0x0E:
|
|
case 0x26:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 1; // Czech/Slovak
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x08:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 8; // Polish
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x16:
|
|
case 0x36:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 13; // Turkish
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x1D:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 11; // Serbian/Croatian/Slovenian
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x1F:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 10; // Rumanian
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x20:
|
|
defaultG0Set = 1; // Serbian/Croatian
|
|
defaultNOS = 0;
|
|
defaultG2Set = 1;
|
|
break;
|
|
case 0x22:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 3; // Estonian
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x23:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 7; // Lettish/Lithuanian
|
|
defaultG2Set = 0;
|
|
break;
|
|
case 0x24:
|
|
defaultG0Set = 2; // Russian/Bulgarian
|
|
defaultNOS = 0;
|
|
defaultG2Set = 1; // Cyrillic G2
|
|
break;
|
|
case 0x25:
|
|
defaultG0Set = 3; // Ukrainian
|
|
defaultNOS = 0;
|
|
defaultG2Set = 1; // Cyrillic G2
|
|
break;
|
|
case 0x37:
|
|
defaultG0Set = 4; // Greek
|
|
defaultNOS = 0;
|
|
defaultG2Set = 2; // Greek G2
|
|
break;
|
|
case 0x40:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 2; // English
|
|
defaultG2Set = 3; // Arabic G2
|
|
break;
|
|
case 0x44:
|
|
defaultG0Set = 0;
|
|
defaultNOS = 4; // French
|
|
defaultG2Set = 3; // Arabic G2
|
|
break;
|
|
case 0x47:
|
|
case 0x57:
|
|
defaultG0Set = 5; // Arabic G0
|
|
defaultNOS = 0;
|
|
defaultG2Set = 3; // Arabic G2
|
|
break;
|
|
case 0x55:
|
|
defaultG0Set = 6; // Hebrew G0
|
|
defaultNOS = 0;
|
|
defaultG2Set = 3; // Arabic G2
|
|
break;
|
|
}
|
|
switch(secondG0andNOS){
|
|
case 0x00:
|
|
case 0x10:
|
|
case 0x40:
|
|
secondG0Set = 0;
|
|
secondNOS = 2; // English
|
|
break;
|
|
case 0x01:
|
|
case 0x09:
|
|
case 0x11:
|
|
case 0x21:
|
|
secondG0Set = 0;
|
|
secondNOS = 5; // German
|
|
break;
|
|
case 0x02:
|
|
case 0x0A:
|
|
case 0x12:
|
|
secondG0Set = 0;
|
|
secondNOS = 12; // Swedish/Finnish/Hungarian
|
|
break;
|
|
case 0x03:
|
|
case 0x0B:
|
|
case 0x13:
|
|
secondG0Set = 0;
|
|
secondNOS = 6; // Italian
|
|
break;
|
|
case 0x04:
|
|
case 0x0C:
|
|
case 0x14:
|
|
case 0x44:
|
|
secondG0Set = 0;
|
|
secondNOS = 4; // French
|
|
break;
|
|
case 0x05:
|
|
case 0x15:
|
|
secondG0Set = 0;
|
|
secondNOS = 9; // Portuguese/Spanish
|
|
break;
|
|
case 0x06:
|
|
case 0x0E:
|
|
case 0x26:
|
|
secondG0Set = 0;
|
|
secondNOS = 1; // Czech/Slovak
|
|
break;
|
|
case 0x08:
|
|
secondG0Set = 0;
|
|
secondNOS = 8; // Polish
|
|
break;
|
|
case 0x16:
|
|
case 0x36:
|
|
secondG0Set = 0;
|
|
secondNOS = 13; // Turkish
|
|
break;
|
|
case 0x1D:
|
|
secondG0Set = 0;
|
|
secondNOS = 11; // Serbian/Croatian/Slovenian
|
|
break;
|
|
case 0x1F:
|
|
secondG0Set = 0;
|
|
secondNOS = 10; // Rumanian
|
|
break;
|
|
case 0x20:
|
|
secondG0Set = 1; // Serbian/Croatian
|
|
secondNOS = 0;
|
|
break;
|
|
case 0x22:
|
|
secondG0Set = 0;
|
|
secondNOS = 3; // Estonian
|
|
break;
|
|
case 0x23:
|
|
secondG0Set = 0;
|
|
secondNOS = 7; // Lettish/Lithuanian
|
|
break;
|
|
case 0x24:
|
|
secondG0Set = 2; // Russian/Bulgarian
|
|
secondNOS = 0;
|
|
break;
|
|
case 0x25:
|
|
secondG0Set = 3; // Ukrainian
|
|
secondNOS = 0;
|
|
break;
|
|
case 0x37:
|
|
secondG0Set = 4; // Greek
|
|
secondNOS = 0;
|
|
break;
|
|
case 0x47:
|
|
case 0x57:
|
|
secondG0Set = 5; // Arabic G0
|
|
secondNOS = 0;
|
|
break;
|
|
case 0x55:
|
|
secondG0Set = 6; // Hebrew G0
|
|
secondNOS = 0;
|
|
break;
|
|
case 0x7F:
|
|
secondG0Set = defaultG0Set;
|
|
secondNOS = defaultNOS;
|
|
}
|
|
}
|
|
|
|
let initializeLevel1PageArray = function(){
|
|
for (let r = 0; r < 26; r++){
|
|
level1PageArray[r] = [];
|
|
for (let c = 0; c < 40; c++){
|
|
level1PageArray[r][c] = 0x20; // fill with spaces
|
|
}
|
|
}
|
|
}
|
|
|
|
let resetDRCSArrays = function(){
|
|
gdrcsArray.length = 0;
|
|
drcsArray.length = 0;
|
|
}
|
|
|
|
let resetPOPArrays = function(){
|
|
gpopArray.length = 0;
|
|
popArray.length = 0;
|
|
for (let i=0; i<4; i++)
|
|
{
|
|
defaultObjects[i] = {flag:0,subpage:0,object:0};
|
|
}
|
|
}
|
|
|
|
let resetPresentationData = function(){
|
|
pagePresentationData.G0andG2 = 0x00;
|
|
pagePresentationData.G0andNOS = 0x7f;
|
|
pagePresentationData.lsp = false;
|
|
pagePresentationData.rsp = false;
|
|
pagePresentationData.spsf = true;
|
|
pagePresentationData.cols = 0;
|
|
pagePresentationData.clut = defaultCLUT.slice();
|
|
pagePresentationData.dsc = 0;
|
|
pagePresentationData.drc = 0;
|
|
pagePresentationData.bbcs = false;
|
|
pagePresentationData.ctm = 0;
|
|
}
|
|
|
|
/* flash stuff */
|
|
let flashEnabled;
|
|
let flashphase = 0x01;
|
|
|
|
function flashinterval(){
|
|
updateFlash();
|
|
|
|
flashphase <<= 1;
|
|
if (flashphase > 0x20)
|
|
flashphase = 0x01;
|
|
}
|
|
|
|
let flashPhasesLUT = [0x38, 0x36, 0x2D, 0x1B]; // slow rate, fast phase 1, fast phase 2, fast phase 3
|
|
let flash;
|
|
|
|
let updateFlash = function(){
|
|
characterArrayCanvasContext.imageSmoothingEnabled = false;
|
|
characterArrayCanvasContext.msImageSmoothingEnabled = false;
|
|
characterArrayCanvasContext.putImageData(steadyPixelData,0,0);
|
|
|
|
flashOnCanvasContext.imageSmoothingEnabled = false;
|
|
flashOnCanvasContext.msImageSmoothingEnabled = false;
|
|
flashOnCanvasContext.putImageData(flashPixelData,0,0); /* update flash canvas */
|
|
|
|
for (let r = 0; r < 26; r++){
|
|
for (let c = 0; c < 72; c++){
|
|
let row = (screenAttributesArray[r][c].br)?r-1:r;
|
|
let col = (screenAttributesArray[row][c].dw && screenAttributesArray[row][c].rh)?c-1:c;
|
|
flash = screenAttributesArray[row][col].flash;
|
|
if (flash & 0x03){
|
|
/* not steady */
|
|
if (flashPhasesLUT[flash >> 2] & flashphase){
|
|
characterArrayCanvasContext.clearRect(c*12,r*10,12,10);
|
|
characterArrayCanvasContext.drawImage(flashOnCanvas,c*12,r*10,12,10,c*12,r*10,12,10);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let moveCursor = function(direction, row, column, cursorRTL, insertMode){
|
|
// map forwards and backwards
|
|
if (cursorRTL){
|
|
if (direction == 4){ // backwards
|
|
direction = 2;
|
|
} else if (direction == 5){ // forwards
|
|
direction = 0;
|
|
}
|
|
} else {
|
|
if (direction == 4){ // backwards
|
|
direction = 0;
|
|
} else if (direction == 5){ // forwards
|
|
direction = 2;
|
|
}
|
|
}
|
|
|
|
let previousCursorX = cursorPosition[0];
|
|
let previousCursorY = cursorPosition[1];
|
|
|
|
switch (direction){
|
|
case 0: // left
|
|
cursorPosition[1]--;
|
|
if (cursorPosition[1] > -1){
|
|
let attrs = screenAttributesArray[cursorPosition[0]][cursorPosition[1]];
|
|
if ((cursorRTL || insertMode) && attrs.dw && attrs.rh)
|
|
cursorPosition[1]--; /* double step cursor in double width text */
|
|
}
|
|
if (cursorPosition[1] < firstColumn){
|
|
if (cursorRTL ){ // right-to-left language
|
|
cursorPosition[1] = lastColumn; // cursor to other end of line
|
|
if (cursorPosition[0] < lastRow){
|
|
cursorPosition[0]++; // cursor down to next row
|
|
} else {
|
|
cursorPosition[0] = firstRow; // loop page
|
|
}
|
|
} else { // left-to-right language
|
|
cursorPosition[1] = lastColumn; // cursor to other end of line
|
|
if (cursorPosition[0] > firstRow) {
|
|
cursorPosition[0]--; // cursor up to previous row
|
|
} else {
|
|
cursorPosition[0] = lastRow; // loop page
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 1: // up
|
|
if (cursorPosition[0] > firstRow){
|
|
cursorPosition[0]--;
|
|
} else {
|
|
cursorPosition[0] = lastRow;
|
|
}
|
|
break;
|
|
case 2: // right
|
|
cursorPosition[1]++;
|
|
let attrs = screenAttributesArray[cursorPosition[0]][cursorPosition[1]];
|
|
if ((!cursorRTL || insertMode) && attrs.dw && attrs.rh)
|
|
cursorPosition[1]++; /* double step cursor in double width text */
|
|
if (cursorPosition[1] > lastColumn){
|
|
if (cursorRTL){ // right-to-left language
|
|
cursorPosition[1] = firstColumn; // cursor to other end of line
|
|
if (cursorPosition[0] > firstRow){
|
|
cursorPosition[0]--; // cursor up to next row
|
|
} else {
|
|
cursorPosition[0] = lastRow;
|
|
}
|
|
} else { // left-to-right language
|
|
cursorPosition[1] = firstColumn; // cursor to other end of line
|
|
if (cursorPosition[0] < lastRow){
|
|
cursorPosition[0]++; // cursor down to previous row
|
|
} else {
|
|
cursorPosition[0] = firstRow;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 3: // down
|
|
if (cursorPosition[0] < lastRow){
|
|
cursorPosition[0]++;
|
|
} else {
|
|
cursorPosition[0] = firstRow;
|
|
}
|
|
break;
|
|
case 6: // home
|
|
if (cursorRTL ){ // right-to-left language
|
|
cursorPosition[1] = lastColumn;
|
|
} else {
|
|
cursorPosition[1] = firstColumn;
|
|
}
|
|
break;
|
|
case 7: // end
|
|
if (cursorRTL ){ // right-to-left language
|
|
cursorPosition[1] = firstColumn;
|
|
} else {
|
|
cursorPosition[1] = lastColumn;
|
|
}
|
|
break;
|
|
case 8: // top
|
|
cursorPosition[0] = firstRow;
|
|
break;
|
|
case 9: // bottom
|
|
cursorPosition[0] = lastRow;
|
|
break;
|
|
case 10: // mouse
|
|
/* move cursor to designated coordinates */
|
|
cursorPosition[0] = row;
|
|
cursorPosition[1] = column;
|
|
break;
|
|
}
|
|
|
|
let prevCellAttributes = screenAttributesArray[previousCursorX][previousCursorY];
|
|
renderCharacter(previousCursorX, previousCursorY); // undraw the old cursor position
|
|
if (prevCellAttributes.dw && !prevCellAttributes.rh){ // left column of double width
|
|
renderCharacter(previousCursorX, previousCursorY+1); // undraw the cursor to the right
|
|
}
|
|
if (prevCellAttributes.dh){ // top of double height
|
|
renderCharacter(previousCursorX+1,previousCursorY); // undraw the new cursor position
|
|
if (prevCellAttributes.dw && !prevCellAttributes.rh){ // left column of double width
|
|
renderCharacter(previousCursorX+1, previousCursorY+1); // undraw the cursor to the right
|
|
}
|
|
}
|
|
|
|
|
|
let cellAttributes = screenAttributesArray[cursorPosition[0]][cursorPosition[1]];
|
|
renderCharacter(cursorPosition[0], cursorPosition[1]); // draw the new cursor position
|
|
if (cellAttributes.dw && !cellAttributes.rh){ // left column of double width
|
|
renderCharacter(cursorPosition[0], cursorPosition[1]+1); // draw the cursor to the right
|
|
}
|
|
if (cellAttributes.dh){ // top of double height
|
|
renderCharacter(cursorPosition[0]+1, cursorPosition[1]); // draw the new cursor position
|
|
if (cellAttributes.dw && !cellAttributes.rh){ // left column of double width
|
|
renderCharacter(cursorPosition[0]+1, cursorPosition[1]+1); // draw the cursor to the right
|
|
}
|
|
}
|
|
}
|
|
|
|
// colour stuff
|
|
let getColour = function(colour, row, boxed, pickerFlag){
|
|
// overload options with pickerFlag to allow colour picker to get colour directly from CLUT
|
|
let RGBA = [];
|
|
let value;
|
|
|
|
if (pickerFlag){
|
|
value = CLUT[colour & 0x1F];
|
|
} else {
|
|
let table = (renderLevel < 3 && colour < 16)?defaultCLUT:CLUT;
|
|
|
|
if ((colour & 0x20) && mixMode){
|
|
RGBA[3] = 0x00; // default background as video in mix mode
|
|
} else {
|
|
if ((colour & 0x1F) != 0x08){ // F/B colour
|
|
if ((newsflash || subtitle) && !boxed){
|
|
value = CLUT[0x08]; // video
|
|
RGBA[3] = 0x00;
|
|
} else {
|
|
value = table[colour & 0x1F]; // F/B colour
|
|
RGBA[3] = 0xFF;
|
|
}
|
|
} else { // CLUT1:0
|
|
if ((!(newsflash || subtitle || boxed)) || ((newsflash || subtitle) && boxed)){
|
|
let rowColour = fullRowColour[row];
|
|
table = (renderLevel < 3 && rowColour < 16)?defaultCLUT:CLUT;
|
|
value = table[rowColour]; // row colour
|
|
if (fullRowColour[row] == 0x08)
|
|
RGBA[3] = 0x00;
|
|
else
|
|
RGBA[3] = 0xFF;
|
|
} else {
|
|
value = CLUT[0x08]; // video
|
|
RGBA[3] = 0x00;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RGBA[0] = ((value >> 8) & 0xF) * 0x11;
|
|
RGBA[1] = ((value >> 4) & 0xF) * 0x11;
|
|
RGBA[2] = (value & 0xF) * 0x11;
|
|
|
|
return RGBA;
|
|
}
|
|
|
|
let getColourHTML = function(colour){
|
|
let table = (renderLevel < 3 && colour < 16)?defaultCLUT:CLUT;
|
|
let paddedvalue = (table[colour] & 0xFFF).toString(16);
|
|
while (paddedvalue.length < 3) paddedvalue = "0" + paddedvalue;
|
|
|
|
return "#"+paddedvalue;
|
|
}
|
|
|
|
let isColourDark = function(R,G,B){
|
|
/* use the "HSP" colour model - http://alienryderflex.com/hsp.html */
|
|
|
|
let P = Math.sqrt((R * R * 0.299) + (G * G * 0.587) + (B * B * 0.114));
|
|
|
|
if (P < 130)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
let parsePacket26Triplets = function(){
|
|
// process contents of packetX26TripletArray
|
|
|
|
// initialise active position to before the first row
|
|
let activePositionX = -1;
|
|
let activePositionY = -1;
|
|
|
|
let originOffsetX = 0;
|
|
let originOffsetY = 0;
|
|
|
|
let G0andG2 = pagePresentationData.G0andG2;
|
|
let modifiedG0andG2 = -1;
|
|
let gdrcsTable = 0;
|
|
let drcsTable = 0;
|
|
let gdrcsLocked = false;
|
|
let drcsLocked = false;
|
|
|
|
let enhancementSizeArray = [];
|
|
for (let r = 0; r < 25; r++){
|
|
enhancementSizeArray[r] = [];
|
|
for (let c = 0; c < 40; c++){
|
|
enhancementSizeArray[r][c] = 0; // no attribute
|
|
}
|
|
}
|
|
|
|
for (let i=0; i<packetX26TripletArray.length; i++){
|
|
let address = packetX26TripletArray[i] & 0x3F;
|
|
let mode = (packetX26TripletArray[i] >> 6) & 0x1F;
|
|
let data = (packetX26TripletArray[i] >> 11) & 0x7F;
|
|
|
|
if (address >= 40){
|
|
// row address group
|
|
let row = address - 40;
|
|
if (row == 0)
|
|
row = 24;
|
|
switch(mode){
|
|
case 0x00: // full screen colour
|
|
if (renderLevel >= 2 && (data & 0x60) == 0 && activePositionX < 0 && activePositionY < 0){
|
|
fullScreenColour = data;
|
|
activePositionY = 0; // move on to first row
|
|
for (let j=0; j<25; j++)
|
|
fullRowColour[j] = data; // all rows to fullScreenColour as per Annex A.5
|
|
}
|
|
break;
|
|
case 0x01: // full row colour
|
|
if (renderLevel >= 2 && activePositionY < row){
|
|
fullRowColour[row] = data & 0x1F;
|
|
if ((data & 0x60) == 0x60){
|
|
for (let j=row+1; j<25; j++){
|
|
fullRowColour[j] = data & 0x1F;
|
|
}
|
|
}
|
|
activePositionY = row;
|
|
activePositionX = 0; // move on to first character of row
|
|
G0andG2 = pagePresentationData.G0andG2;
|
|
}
|
|
break;
|
|
case 0x04: // set active position
|
|
if (activePositionY <= row){
|
|
if (activePositionY < row)
|
|
G0andG2 = pagePresentationData.G0andG2;
|
|
activePositionY = row;
|
|
if (renderLevel >= 2 && data < 40)
|
|
activePositionX = data;
|
|
else
|
|
activePositionX = -1;
|
|
}
|
|
break;
|
|
case 0x07: // address display row 0
|
|
if (address == 0x3F && activePositionX < 0){
|
|
activePositionY = 0;
|
|
activePositionX = 8; // move to 0,8. should be 0,0 but this prevents enhancements being addressed to columns < 8
|
|
G0andG2 = pagePresentationData.G0andG2;
|
|
|
|
if (renderLevel >= 2){
|
|
fullRowColour[0] = data & 0x1F;
|
|
if ((data & 0x60) == 0x60){
|
|
for (let j=1; j<25; j++){
|
|
fullRowColour[j] = data & 0x1F;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 0x10: // origin modifier
|
|
if (renderLevel >= 2 && data < 72){
|
|
originOffsetY = address - 40;
|
|
originOffsetX = data;
|
|
}
|
|
break;
|
|
case 0x11:
|
|
case 0x12:
|
|
case 0x13:
|
|
// object invocation
|
|
originOffsetY = 0;
|
|
originOffsetX = 0;
|
|
let objectSource = ((address>>3)&3);
|
|
let objectType = (mode&3);
|
|
let objectAddress = (data|((address&3)<<7));
|
|
if (objectSource == 0){
|
|
console.log("Illegal object "+address+" "+mode+" "+data);
|
|
break;
|
|
}
|
|
let objectData = [];
|
|
let objectArrays = (objectSource&1)?gpopArray[objectAddress]:popArray[objectAddress];
|
|
if (objectSource == 1){
|
|
// local object
|
|
for (let i=objectAddress;i<packetX26TripletArray.length;i++)
|
|
{
|
|
// copy enhancement data up to terminator
|
|
objectData.push(packetX26TripletArray[i]);
|
|
if ((packetX26TripletArray[i]&0x7ff) == 0x7ff)
|
|
break;
|
|
}
|
|
console.log("Local "+(["active ","adaptive","passive "][objectType-1])+" object invocation from packet "+(objectAddress>>4)+" triplet "+(objectAddress&0xf), objectData);
|
|
}
|
|
else if (objectArrays) // skip if objects aren't loaded
|
|
{
|
|
switch (objectType){
|
|
case 1:
|
|
objectData = objectArrays.active;
|
|
break;
|
|
case 2:
|
|
objectData = objectArrays.adaptive;
|
|
break;
|
|
case 3:
|
|
objectData = objectArrays.passive;
|
|
break;
|
|
}
|
|
console.log(((objectSource&1)?"GPOP ":"POP ")+(["active ","adaptive","passive "][objectType-1])+" object invocation from subpage "+(objectAddress&0xf)+" packet "+(((objectAddress>>7)&3)+1)+" triplet "+((((objectAddress>>5)&3)*3)+objectType)+" bits "+((objectAddress&0x10)?"1 to 9":"10 to 18"), objectData);
|
|
}
|
|
break;
|
|
case 0x15:
|
|
case 0x16:
|
|
case 0x17:
|
|
// object definition
|
|
break;
|
|
case 0x18: // DRCS mode
|
|
if ((renderLevel == 2 && (data & 0x10)) || (renderLevel == 3 && (data & 0x20))){
|
|
if (data & 0x40 && !gdrcsLocked){
|
|
gdrcsTable = data & 0xF;
|
|
if (renderLevel == 2){
|
|
gdrcsLocked = true;
|
|
console.log("Error: tried to designate more than one gdrcs sub-table at level 2.5");
|
|
}
|
|
} else if (!drcsLocked){
|
|
drcsTable = data & 0xF;
|
|
if (renderLevel == 2){
|
|
drcsLocked = true;
|
|
console.log("Error: tried to designate more than one drcs sub-table at level 2.5");
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 0x1F: // termination marker
|
|
if (address == 63){
|
|
i = packetX26TripletArray.length; // break out of loop
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
// column address group
|
|
switch(mode){
|
|
case 0x00: // foreground colour
|
|
if (renderLevel >= 2 && (data & 0x60) == 0 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
for (let j=activePositionX; j<40; j++){
|
|
screenAttributesArray[activePositionY][j].foreColour = data;
|
|
if (!screenAttributesArray[activePositionY][j].br){
|
|
let next = level1PageArray[activePositionY][j]
|
|
if (next < 0x08 || (next >= 0x10 && next < 0x18))
|
|
break; // stop at spacing colour attribute
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 0x01: // G1 mosaic character
|
|
if (renderLevel >= 2 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
if (data >= 0x20){
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = data;
|
|
screenAttributesArray[activePositionY][activePositionX].mosaics = true;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
}
|
|
}
|
|
break;
|
|
case 0x02: // G3 mosaic character at L1.5
|
|
if (activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
if (data >= 0x20){
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = data;
|
|
screenAttributesArray[activePositionY][activePositionX].charSet = 13;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
}
|
|
}
|
|
break;
|
|
case 0x03: // background colour
|
|
if (renderLevel >= 2 && (data & 0x60) == 0 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
for (let j=activePositionX; j<40; j++){
|
|
screenAttributesArray[activePositionY][j].backColour = data;
|
|
if (!screenAttributesArray[activePositionY][j].br){
|
|
let next = level1PageArray[activePositionY][j+1]
|
|
if (next == 0x1C || next == 0x1D)
|
|
break; // stop at spacing background colour attribute
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 0x07: // flash functions
|
|
if (renderLevel >= 2 && (data & 0x60) == 0 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
let phase = 1;
|
|
if (data & 0x10){
|
|
if (data & 8)
|
|
break; // reserved rate and phases
|
|
} else {
|
|
phase = (data & 0xC) >> 2;
|
|
}
|
|
for (let j=activePositionX; j<40; j++){
|
|
screenAttributesArray[activePositionY][j].flash = (data & 3) | (phase << 2);
|
|
if (data & 0x10){
|
|
if (data & 0x4){
|
|
phase--;
|
|
if (phase < 1)
|
|
phase = 3;
|
|
} else {
|
|
phase++;
|
|
if (phase > 3)
|
|
phase = 1;
|
|
}
|
|
}
|
|
// overwrite whole line, erasing any subsequent level 1 flash - I don't know if this is correct!
|
|
}
|
|
}
|
|
break;
|
|
case 0x08: // modified G0 and G2 designation
|
|
if (renderLevel >= 2 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
|
|
if (data == pagePresentationData.G0andG2){
|
|
// selected default charsets
|
|
G0andG2 = data;
|
|
} else if (renderLevel >= 3 || data == modifiedG0andG2 || modifiedG0andG2 < 0){
|
|
// selected another charset
|
|
modifiedG0andG2 = data;
|
|
G0andG2 = data;
|
|
}
|
|
}
|
|
break;
|
|
case 0x09: // G0 character
|
|
if (renderLevel >= 2 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
if (data >= 0x20){
|
|
setCharacterSets(G0andG2,pagePresentationData.G0andNOS);
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = data;
|
|
screenAttributesArray[activePositionY][activePositionX].charSet = defaultG0Set;
|
|
screenAttributesArray[activePositionY][activePositionX].NOS = 0;
|
|
screenAttributesArray[activePositionY][activePositionX].mosaics = false;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
}
|
|
}
|
|
break;
|
|
case 0x0B: // G3 mosaic character at L2.5 and 3.5
|
|
if (renderLevel >= 2 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
if (data >= 0x20){
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = data;
|
|
screenAttributesArray[activePositionY][activePositionX].charSet = 13;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
}
|
|
}
|
|
break;
|
|
case 0x0C: // display attributes
|
|
if (renderLevel >= 2 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
for (let j=activePositionX; j<40; j++){ // boxing/window
|
|
screenAttributesArray[activePositionY][j].boxed = Boolean(data&2);
|
|
if (!screenAttributesArray[activePositionY][j].br){
|
|
if ((data&2 && level1PageArray[activePositionY][j] == 0x0A && level1PageArray[activePositionY][j+1] == 0x0A) || (!(data&2) && level1PageArray[activePositionY][j] == 0x0B && level1PageArray[activePositionY][j+1] == 0x0B))
|
|
break;
|
|
}
|
|
}
|
|
for (let j=activePositionX; j<40; j++){ // conceal
|
|
screenAttributesArray[activePositionY][j].conceal = Boolean(data&4);
|
|
if (!screenAttributesArray[activePositionY][j].br){
|
|
if ((level1PageArray[activePositionY][j] < 0x20) && ((data&4) && (level1PageArray[activePositionY][j]&0xF) < 8) || (!(data&4) && level1PageArray[activePositionY][j+1] == 0x18))
|
|
break;
|
|
}
|
|
}
|
|
for (let j=activePositionX; j<40; j++){ // invert
|
|
screenAttributesArray[activePositionY][j].invert = Boolean(data&0x10);
|
|
}
|
|
for (let j=activePositionX; j<40; j++){ // underline/separated
|
|
screenAttributesArray[activePositionY][j].separated = Boolean(data&0x20);
|
|
screenAttributesArray[activePositionY][j].heldmode = Boolean(data&0x20);
|
|
screenAttributesArray[activePositionY][j].underline = Boolean(data&0x20);
|
|
|
|
if (!screenAttributesArray[activePositionY][j].br){
|
|
if (!(data&0x20) && level1PageArray[activePositionY][j] == 0x1A)
|
|
break; // cancel separated state invoked by a previous spacing attribute
|
|
}
|
|
// pretty sure this is wrong. All these enhancements should probably be written to a shadow attribute array and applied in the same way as the double size stuff below
|
|
}
|
|
let dh = Boolean(data&1);
|
|
let dw = Boolean(data&0x40);
|
|
for (let j=activePositionX; j<40; j++){ // double size
|
|
let sizeAttr = 0;
|
|
if (dw){
|
|
sizeAttr |= dw?1:0;
|
|
} else if (!dw){
|
|
sizeAttr |= 0x10; // normal width
|
|
}
|
|
if (!dh)
|
|
sizeAttr |= 8; // normal height
|
|
else if (dh && activePositionY < 23 && activePositionY > 0)
|
|
sizeAttr |= dh?4:0; // double height
|
|
|
|
enhancementSizeArray[activePositionY][j] = sizeAttr;
|
|
|
|
if (!screenAttributesArray[activePositionY][j].br){
|
|
let next = level1PageArray[activePositionY][j];
|
|
if (j>activePositionX){
|
|
if(next == 0x0C || next == 0x0D || next == 0x0E || next == 0x0F)
|
|
break; // stop at spacing size change
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 0x0D: // DRCS invocation
|
|
if (renderLevel >= 2 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
// TODO
|
|
if ((data & 0x3F) < 48){
|
|
if (!(data & 0x40)){
|
|
if (gdrcsArray[(gdrcsTable*48)+(data & 0x3F)]){
|
|
if (gdrcsArray[(gdrcsTable*48)+(data & 0x3F)].mode > 3){
|
|
console.log("gdrcs error");
|
|
break;
|
|
}
|
|
if (renderLevel == 2){
|
|
gdrcsLocked = true;
|
|
}
|
|
screenAttributesArray[activePositionY][activePositionX].gdrcs = true;
|
|
screenAttributesArray[activePositionY][activePositionX].drcsTable = gdrcsTable;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
} else {
|
|
console.log("gdrcs error");
|
|
break;
|
|
}
|
|
} else {
|
|
if (drcsArray[(drcsTable*48)+(data & 0x3F)]){
|
|
if (drcsArray[(drcsTable*48)+(data & 0x3F)].mode > 3){
|
|
console.log("drcs error");
|
|
break;
|
|
}
|
|
if (renderLevel == 2){
|
|
gdrcsLocked = true;
|
|
}
|
|
screenAttributesArray[activePositionY][activePositionX].drcs = true;
|
|
screenAttributesArray[activePositionY][activePositionX].drcsTable = drcsTable;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
} else {
|
|
console.log("drcs error");
|
|
break;
|
|
}
|
|
}
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = data & 0x3F;
|
|
}
|
|
}
|
|
break;
|
|
case 0x0E: // font style
|
|
if (renderLevel >= 3 && activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
for (let r=0; r<((data >> 4)&7)+1; r++){
|
|
if (activePositionY+r > 24)
|
|
break;
|
|
for (let c=activePositionX; c<40; c++){
|
|
screenAttributesArray[activePositionY+r][c].proportional = Boolean(data&1);
|
|
screenAttributesArray[activePositionY+r][c].bold = Boolean(data&2);
|
|
screenAttributesArray[activePositionY+r][c].italic = Boolean(data&4);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 0x0F: // G2 character
|
|
if (activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
if (data >= 0x20){
|
|
setCharacterSets(G0andG2,pagePresentationData.G0andNOS);
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = data;
|
|
screenAttributesArray[activePositionY][activePositionX].charSet = 7 + defaultG2Set;
|
|
screenAttributesArray[activePositionY][activePositionX].mosaics = false;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
}
|
|
}
|
|
break;
|
|
case 0x10:
|
|
if (data == 0x2A){
|
|
/* special case to handle specification change in EN 300 706 V1.2.1
|
|
The @ symbol replaces the * symbol at position 2/A when the table is accessed via a packet X/26 Column Address triplet with Mode Description = 10000 and Data = 0101010. */
|
|
if (activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = 0x40; // real position of @ symbol
|
|
screenAttributesArray[activePositionY][activePositionX].charSet = 0; // force latin G0
|
|
screenAttributesArray[activePositionY][activePositionX].NOS = 0;
|
|
screenAttributesArray[activePositionY][activePositionX].diacritic = 0;
|
|
screenAttributesArray[activePositionY][activePositionX].mosaics = false;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
}
|
|
break;
|
|
}
|
|
case 0x11:
|
|
case 0x12:
|
|
case 0x13:
|
|
case 0x14:
|
|
case 0x15:
|
|
case 0x16:
|
|
case 0x17:
|
|
case 0x18:
|
|
case 0x19:
|
|
case 0x1A:
|
|
case 0x1B:
|
|
case 0x1C:
|
|
case 0x1D:
|
|
case 0x1E:
|
|
case 0x1F:
|
|
// characters including diacritical marks
|
|
if (activePositionX <= address){
|
|
activePositionY = (activePositionY < 0)?0:activePositionY;
|
|
activePositionX = address;
|
|
if (data >= 0x20){
|
|
setCharacterSets(G0andG2,pagePresentationData.G0andNOS);
|
|
screenAttributesArray[activePositionY][activePositionX].charcode = data;
|
|
screenAttributesArray[activePositionY][activePositionX].charSet = defaultG0Set;
|
|
screenAttributesArray[activePositionY][activePositionX].NOS = 0;
|
|
screenAttributesArray[activePositionY][activePositionX].diacritic = mode & 0xF;
|
|
screenAttributesArray[activePositionY][activePositionX].mosaics = false;
|
|
if (renderLevel >= 2 && activePositionY)
|
|
if (!(screenAttributesArray[activePositionY-1][activePositionX].dh))
|
|
screenAttributesArray[activePositionY][activePositionX].br = false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Attempt to implement double height as per the spec:
|
|
"Where only Double Height or Double Size is invoked anywhere on a row by a non-spacing attribute the row below is not suppressed. All characters and attributes in that row continue to have their normal effect outside the area of the lower parts of the double height characters, including any characters or attributes for that row defined in the Level one page."
|
|
"Where a row contains a mixture of spacing and non-spacing Double Height or Double Size attributes, and a spacing one is encountered before a non-spacing one, the suppression of the lower row that would normally accompany the spacing attribute only applies from the start of the row up to the first non-spacing Double Height or Double Size attribute." */
|
|
|
|
for (let r = 0; r < 25; r++){
|
|
let level1Double = false;
|
|
let noSuppressBottom = false;
|
|
for (let c = 0; c < 40; c++){
|
|
if (!screenAttributesArray[r][c].br){
|
|
let attr = enhancementSizeArray[r][c];
|
|
if (level1PageArray[r][c-1] == 0x0D || level1PageArray[r][c-1] == 0x0F)
|
|
level1Double = true;
|
|
if (level1Double && (attr&0x0C))
|
|
noSuppressBottom = true;
|
|
if (attr&1)
|
|
screenAttributesArray[r][c].dw = true;
|
|
if (attr&4){
|
|
screenAttributesArray[r][c].dh = true; // set double height
|
|
screenAttributesArray[r+1][c].br = true;
|
|
}
|
|
if (attr&8)
|
|
screenAttributesArray[r][c].dh = false; // set normal height
|
|
if (attr&0x10){
|
|
screenAttributesArray[r][c].dw = false; // set normal width
|
|
screenAttributesArray[r][c].rh = false;
|
|
}
|
|
if (!(screenAttributesArray[r][c].dh) && noSuppressBottom){
|
|
screenAttributesArray[r+1][c].br = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// render stuff
|
|
let decodePage = function(){
|
|
/* set page settings */
|
|
newsflash = pageSettings.newsflash;
|
|
subtitle = pageSettings.subtitle;
|
|
suppressHeader = pageSettings.suppressHeader;
|
|
inhibitDisplay = pageSettings.inhibitDisplay;
|
|
|
|
/* set page presentation options here */
|
|
setCharacterSets(pagePresentationData.G0andG2,pagePresentationData.G0andNOS);
|
|
leftSidePanel = pagePresentationData.lsp;
|
|
rightSidePanel = pagePresentationData.rsp;
|
|
sidePanelStatusFlag = pagePresentationData.spsf;
|
|
sidePanelColumns = pagePresentationData.cols;
|
|
defaultScreenColour = pagePresentationData.dsc;
|
|
defaultRowColour = pagePresentationData.drc;
|
|
blackBackgroundColourSubstitution = pagePresentationData.bbcs;
|
|
colourMapping = pagePresentationData.ctm;
|
|
CLUT = pagePresentationData.clut;
|
|
|
|
fullScreenColour = (renderLevel >= 2)?defaultScreenColour:0;
|
|
|
|
for (let i=0; i<25; i++){
|
|
fullRowColour[i] = (renderLevel >= 2)?defaultRowColour:0;
|
|
}
|
|
|
|
/* initialise attributes array */
|
|
for (let r = 0; r < 26; r++){
|
|
screenAttributesArray[r] = [];
|
|
for (let c = 0; c < 72; c++){
|
|
screenAttributesArray[r][c] = {switchG0:false, mosaics:false, dh:false, dw:false, br:false, rh:false, flash:0, charcode:0x20, charSet:defaultG0Set, NOS:defaultNOS, foreColour:0x07, backColour:0x20, diacritic:0, boxed:false, conceal:false, separated:false, underline:false, invert:false, bold:false, italic:false, proportional:false, drcs:false, gdrcs:false, drcsTable:0};
|
|
// backColour 0x20 uses full row background
|
|
}
|
|
}
|
|
|
|
/* read packets from level1PageArray and set screen attributes */
|
|
let bottomrow = false;
|
|
for (let r = 0; r < 26; r++){
|
|
if (r > 0 && inhibitDisplay)
|
|
break; // skip page rows
|
|
if (r == 24 && !displayRow24 && !displayRow25)
|
|
break; // don't display packet 24.
|
|
if (r == 25 && !displayRow25)
|
|
break; // don't display packet 25.
|
|
/* start of row defaults go here */
|
|
let nextforeColour = 0x07;
|
|
let nextbackcolour;
|
|
let backcolour = 0x20; // special black
|
|
let nextconceal = false;
|
|
let nextflash = 0; /* steady */
|
|
let boxed = false;
|
|
let startbox = 0;
|
|
let endbox = 0;
|
|
let nextCharsetToggle = false;
|
|
let nextmosaics = false;
|
|
let nextseparated = false;
|
|
let nextdoubleheight = false;
|
|
let nextdoublewidth = false;
|
|
let nextdoublewidthright = false;
|
|
let nexthold = false;
|
|
let nextheldchar = 0x20;
|
|
let nextheldmode = false;
|
|
let rowhasdoubleheight = false; // reset flag
|
|
for (let c = 0; c < 40; c++){
|
|
let cellAttributes = screenAttributesArray[r][c];
|
|
|
|
if (!bottomrow)
|
|
{
|
|
let G0Set;
|
|
let NOS;
|
|
let foreColour = nextforeColour;
|
|
let flash = nextflash;
|
|
let mosaics = nextmosaics;
|
|
let separated = nextseparated;
|
|
let conceal = nextconceal;
|
|
let doubleheight = nextdoubleheight;
|
|
let doublewidth = nextdoublewidth;
|
|
startbox >>= 1;
|
|
endbox >>= 1;
|
|
let hold = nexthold;
|
|
let heldchar = nextheldchar;
|
|
let heldmode = nextheldmode;
|
|
let charsetToggle = nextCharsetToggle;
|
|
let charcode;
|
|
if (r == 0 && suppressHeader && !(pageSearch || (numberEntry && c < 6))){
|
|
charcode = 0x20;
|
|
} else {
|
|
charcode = level1PageArray[r][c];
|
|
}
|
|
|
|
if (charcode < 0x20){
|
|
switch (charcode){
|
|
case 0x00:
|
|
if (renderLevel < 2)
|
|
break;
|
|
case 0x01:
|
|
case 0x02:
|
|
case 0x03:
|
|
case 0x04:
|
|
case 0x05:
|
|
case 0x06:
|
|
case 0x07:
|
|
/* alphanumeric colour codes */
|
|
nextforeColour = charcode;
|
|
nextmosaics = false; /* switch off mosaics */
|
|
nextheldchar = 0x20; /* clear held mosaic */
|
|
nextheldmode = false;
|
|
nextconceal = false; /* switch off conceal */
|
|
break;
|
|
|
|
case 0x08:
|
|
/* flash */
|
|
nextflash = 1; /* normal flash, slow rate */
|
|
break;
|
|
|
|
case 0x09:
|
|
/* steady */
|
|
nextflash = 0; /* steady */
|
|
flash = nextflash; /* set-at */
|
|
break;
|
|
|
|
case 0x0A:
|
|
/* end box */
|
|
if (endbox == 1)
|
|
boxed = false;
|
|
endbox = 2;
|
|
break;
|
|
|
|
case 0x0B:
|
|
/* start box */
|
|
if (startbox == 1)
|
|
boxed = true;
|
|
startbox = 2;
|
|
break;
|
|
|
|
case 0x0C:
|
|
/* normal size */
|
|
if (doubleheight || doublewidth){ /* size change */
|
|
nextheldchar = 0x20;
|
|
nextheldmode = false;
|
|
heldchar = nextheldchar;
|
|
heldmode = nextheldmode;
|
|
}
|
|
nextdoubleheight = false;
|
|
nextdoublewidth = false;
|
|
break;
|
|
|
|
case 0x0D:
|
|
/* double height */
|
|
if (!doubleheight || (renderLevel >= 2 && doublewidth)){ /* size change */
|
|
nextheldchar = 0x20;
|
|
nextheldmode = false;
|
|
}
|
|
nextdoubleheight = true;
|
|
nextdoublewidth = false;
|
|
rowhasdoubleheight = true; // set flag for row
|
|
break;
|
|
|
|
case 0x0E:
|
|
/* double width */
|
|
if (renderLevel >= 2){
|
|
if (!doublewidth || doubleheight){ /* size change */
|
|
nextheldchar = 0x20;
|
|
nextheldmode = false;
|
|
}
|
|
nextdoublewidth = true;
|
|
nextdoubleheight = false;
|
|
}
|
|
break;
|
|
|
|
case 0x0F:
|
|
/* double size */
|
|
if (renderLevel >= 2){
|
|
if (!doubleheight || !doublewidth){ /* size change */
|
|
nextheldchar = 0x20;
|
|
nextheldmode = false;
|
|
}
|
|
nextdoublewidth = true;
|
|
nextdoubleheight = true;
|
|
rowhasdoubleheight = true; // set flag for row
|
|
}
|
|
break;
|
|
|
|
case 0x10:
|
|
if (renderLevel < 2)
|
|
break;
|
|
case 0x11:
|
|
case 0x12:
|
|
case 0x13:
|
|
case 0x14:
|
|
case 0x15:
|
|
case 0x16:
|
|
case 0x17:
|
|
/* mosaic colour codes */
|
|
nextforeColour = charcode & 0x07;
|
|
nextmosaics = true; /* switch on mosaics */
|
|
nextconceal = false; /* switch off conceal */
|
|
break;
|
|
|
|
case 0x18:
|
|
/* Conceal */
|
|
nextconceal = true;
|
|
conceal = nextconceal; // set-at
|
|
break;
|
|
|
|
case 0x19:
|
|
/* contiguous mosaics */
|
|
nextseparated = false;
|
|
separated = nextseparated; // set-at
|
|
break;
|
|
|
|
case 0x1A:
|
|
/* separated mosaics */
|
|
nextseparated = true;
|
|
separated = nextseparated; // set-at
|
|
break;
|
|
|
|
case 0x1B:
|
|
/* ESC/Switch */
|
|
nextCharsetToggle = !nextCharsetToggle; /* toggle */
|
|
break;
|
|
|
|
case 0x1C:
|
|
/* black background */
|
|
nextbackcolour = 0x20;
|
|
backcolour = 0x20; /* set-at */
|
|
break;
|
|
|
|
case 0x1D:
|
|
/* new background */
|
|
nextbackcolour = foreColour;
|
|
backcolour = nextbackcolour; /* set-at */
|
|
break;
|
|
|
|
case 0x1E:
|
|
/* hold mosaics */
|
|
nexthold = true;
|
|
hold = true; /* set-at */
|
|
break;
|
|
|
|
case 0x1F:
|
|
/* release mosaics */
|
|
nexthold = false;
|
|
break;
|
|
}
|
|
|
|
if (visibleControlChars){
|
|
heldchar = charcode;
|
|
heldmode = false; // don't underline visible control codes.
|
|
} else if (!hold){
|
|
heldchar = 0x20;
|
|
}
|
|
|
|
} else if (mosaics){ /* mosaics active */
|
|
if (charcode & 0x20){ /* bit 6 set */
|
|
nextheldchar = charcode;
|
|
heldchar = nextheldchar;
|
|
nextheldmode = separated; /* save separated-ness */
|
|
heldmode = nextheldmode;
|
|
} else {
|
|
separated = false; // don't underline blast through
|
|
}
|
|
} else {
|
|
separated = false; // don't underline alphanumerics
|
|
}
|
|
|
|
|
|
if (!charsetToggle){
|
|
G0Set = defaultG0Set;
|
|
NOS = defaultNOS;
|
|
} else {
|
|
G0Set = secondG0Set;
|
|
NOS = secondNOS;
|
|
}
|
|
|
|
cellAttributes.charcode = charcode; /* copy charcode into character array */
|
|
cellAttributes.heldchar = heldchar;
|
|
cellAttributes.heldmode = heldmode;
|
|
cellAttributes.foreColour = (renderLevel >= 2)?foreColour+(foregroundMapping[colourMapping]*8):foreColour;
|
|
cellAttributes.backColour = (renderLevel >= 2)?backcolour+(backgroundMapping[colourMapping]*8):backcolour;
|
|
cellAttributes.mosaics = mosaics;
|
|
cellAttributes.switchG0 = charsetToggle;
|
|
cellAttributes.dh = doubleheight;
|
|
cellAttributes.dw = doublewidth;
|
|
cellAttributes.rh = false; /* this is set later when rendering */
|
|
cellAttributes.flash = flash;
|
|
cellAttributes.charSet = G0Set;
|
|
cellAttributes.NOS = NOS;
|
|
cellAttributes.boxed = boxed;
|
|
cellAttributes.conceal = conceal;
|
|
cellAttributes.separated = separated;
|
|
}
|
|
|
|
cellAttributes.br = bottomrow;
|
|
}
|
|
|
|
bottomrow = rowhasdoubleheight; // next row is a bottom row.
|
|
}
|
|
|
|
if (renderLevel >= 1){
|
|
if (packetX26TripletArray.length){
|
|
parsePacket26Triplets();
|
|
}
|
|
else if (defaultObjects[0].flag || defaultObjects[1].flag || defaultObjects[2].flag || defaultObjects[3].flag)
|
|
{
|
|
// TODO: apply default objects if no local enhancements
|
|
console.log("default object present");
|
|
}
|
|
}
|
|
}
|
|
|
|
let redrawScreen = function(){
|
|
for (let row = 0; row < 26; row++){
|
|
renderRow(row);
|
|
}
|
|
}
|
|
|
|
let renderRow = function(row){
|
|
for (let column=0; column<72; column++){
|
|
renderCharacter(row, column);
|
|
}
|
|
}
|
|
|
|
let renderCharacter = function(row, column){
|
|
let charImageData;
|
|
let cellAttributesRowColumn = screenAttributesArray[row][column];
|
|
let cursor;
|
|
let r = row;
|
|
let c = column;
|
|
let italic = false;
|
|
let bold = false;
|
|
let br = cellAttributesRowColumn.br;
|
|
|
|
if (c)
|
|
if (br && screenAttributesArray[r][c].rh && !screenAttributesArray[r][c-1].br){
|
|
screenAttributesArray[r][c].br = false;
|
|
br = false;
|
|
if (screenAttributesArray[r][c-1].dh){
|
|
screenAttributesArray[r][c].dh = true;
|
|
screenAttributesArray[r+1][c].br = true;
|
|
}
|
|
}
|
|
|
|
if (br) /* bottom row of double height */
|
|
r = row - 1;
|
|
|
|
let dh = screenAttributesArray[r][column].dh;
|
|
let dw = screenAttributesArray[r][column].dw;
|
|
let rh = screenAttributesArray[r][column].rh;
|
|
|
|
let boxed = screenAttributesArray[r][column].boxed;
|
|
|
|
if (dw)
|
|
if (rh) /* right column of double width */
|
|
c = column - 1;
|
|
else if (column < 39){
|
|
screenAttributesArray[r][column+1].rh = true;
|
|
screenAttributesArray[r][column+1].dw = true;
|
|
}
|
|
|
|
if (dw && !rh && column == 39){
|
|
dw = false;
|
|
}
|
|
|
|
let cellAttributesRC = screenAttributesArray[r][c];
|
|
let separated = cellAttributesRC.separated;
|
|
let underline = cellAttributesRC.underline;
|
|
let drcs = cellAttributesRC.drcs;
|
|
let gdrcs = cellAttributesRC.gdrcs;
|
|
let drcsTable = cellAttributesRC.drcsTable;
|
|
|
|
let character = cellAttributesRC.charcode;
|
|
let mosaicMode = cellAttributesRC.mosaics;
|
|
|
|
if (gdrcs && gdrcsArray[(drcsTable*48)+character]){
|
|
charImageData = new Uint8Array(gdrcsArray[(drcsTable*48)+character].data); // copy of character data
|
|
} else if (drcs && drcsArray[(drcsTable*48)+character]){
|
|
charImageData = new Uint8Array(drcsArray[(drcsTable*48)+character].data);
|
|
} else {
|
|
if (character < 0x20){
|
|
character = cellAttributesRC.heldchar;
|
|
if (character > 0x20){
|
|
mosaicMode = true;
|
|
separated = cellAttributesRC.heldmode;
|
|
} else {
|
|
mosaicMode = false;
|
|
}
|
|
}
|
|
|
|
if (!reveal && cellAttributesRC.conceal && character > 0x20){
|
|
character = 0x20; // display spaces for concealed characters when reveal mode is not active
|
|
}
|
|
|
|
if (br && !dh){
|
|
character = 0x20;
|
|
underline = false;
|
|
}
|
|
|
|
let chardatay = (character & 0x0f) * 10;
|
|
let chardatax = (character >> 4) * 12;
|
|
let charImage;
|
|
|
|
if (character < 0x20){
|
|
if (rh){
|
|
charImage = charsetImageData[0][0]; // render space
|
|
} else {
|
|
charImage = controlCodesImageData[character];
|
|
dw = false; /* don't be double width */
|
|
}
|
|
underline = false;
|
|
} else {
|
|
chardatax -= 24; /* first 32 characters not present in bitmaps */
|
|
let charSet = cellAttributesRC.charSet;
|
|
let NOS = cellAttributesRC.NOS;
|
|
if (charSet < 13 && mosaicMode && (character < 0x40 || character >= 0x60)){
|
|
if(separated) // if mosaics and separated
|
|
charSet = 12; // use separated mosaics character set
|
|
else
|
|
charSet = 11; // contiguous
|
|
}
|
|
|
|
charImage = charsetImageData[charSet][character-0x20];
|
|
if ((charSet == 0) && NOS){
|
|
switch (character){
|
|
case 0x23:
|
|
chardatax = 0;
|
|
chardatay = 0; // recalculate coordinates
|
|
charImage = NOSImageData[NOS-1][0];
|
|
break;
|
|
case 0x24:
|
|
chardatax = 0;
|
|
chardatay = 10;
|
|
charImage = NOSImageData[NOS-1][1];
|
|
break;
|
|
case 0x40:
|
|
chardatax = 0;
|
|
chardatay = 20;
|
|
charImage = NOSImageData[NOS-1][2];
|
|
break;
|
|
case 0x5B:
|
|
chardatax = 0;
|
|
chardatay = 30;
|
|
charImage = NOSImageData[NOS-1][3];
|
|
break;
|
|
case 0x5C:
|
|
chardatax = 0;
|
|
chardatay = 40;
|
|
charImage = NOSImageData[NOS-1][4];
|
|
break;
|
|
case 0x5D:
|
|
chardatax = 0;
|
|
chardatay = 50;
|
|
charImage = NOSImageData[NOS-1][5];
|
|
break;
|
|
case 0x5E:
|
|
chardatax = 0;
|
|
chardatay = 60;
|
|
charImage = NOSImageData[NOS-1][6];
|
|
break;
|
|
case 0x5F:
|
|
chardatax = 0;
|
|
chardatay = 70;
|
|
charImage = NOSImageData[NOS-1][7];
|
|
break;
|
|
case 0x60:
|
|
chardatax = 0;
|
|
chardatay = 80;
|
|
charImage = NOSImageData[NOS-1][8];
|
|
break;
|
|
case 0x7B:
|
|
chardatax = 0;
|
|
chardatay = 90;
|
|
charImage = NOSImageData[NOS-1][9];
|
|
break;
|
|
case 0x7C:
|
|
chardatax = 0;
|
|
chardatay = 100;
|
|
charImage = NOSImageData[NOS-1][10];
|
|
break;
|
|
case 0x7D:
|
|
chardatax = 0;
|
|
chardatay = 110;
|
|
charImage = NOSImageData[NOS-1][11];
|
|
break;
|
|
case 0x7E:
|
|
chardatax = 0;
|
|
chardatay = 120;
|
|
charImage = NOSImageData[NOS-1][12];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (charSet > 10){
|
|
underline = false; // don't underline G1, G3 or DRCS characters
|
|
} else {
|
|
italic = cellAttributesRC.italic;
|
|
bold = cellAttributesRC.bold;
|
|
}
|
|
|
|
if (cellAttributesRC.diacritic && charSet == 0 && mosaicMode == 0 && defaultG2Set == 0){
|
|
/* look up diacriticImageData */
|
|
let offset = ISO6937LUT1[cellAttributesRC.diacritic].indexOf(String.fromCharCode(character))
|
|
|
|
if (offset > -1){
|
|
charImage = extraCharsData[offset + ISO6937LUT2[cellAttributesRC.diacritic]];
|
|
}
|
|
}
|
|
}
|
|
|
|
charImageData = new Uint8Array(charImage); // copy of character data
|
|
|
|
if (underline){ // extra bit for underline from non spacing attributes
|
|
for (let i = 108; i < 120; i++){
|
|
charImageData[i] = 255; // add underline
|
|
}
|
|
}
|
|
|
|
if (bold){
|
|
let charImageDataTemp = new Uint8Array(charImageData); // copy of character data
|
|
|
|
for (let py = 0; py < 10; py++){
|
|
for (let i = 0; i < 11; i++){
|
|
charImageData[py * 12 + i] = charImageData[py * 12 + i] | charImageDataTemp[py * 12 + i + 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (italic){
|
|
for (let py = 0; py < 3; py++){
|
|
for (let i = 11; i >= 1; i--){
|
|
charImageData[py * 12 + i] = charImageData[py * 12 + i - 1];
|
|
}
|
|
charImageData[py * 12] = 0;
|
|
}
|
|
for (let py = 6; py < 10; py++){
|
|
for (let i = 0; i < 11; i++){
|
|
charImageData[py * 12 + i] = charImageData[py * 12 + i + 1];
|
|
}
|
|
charImageData[py * 12 + 11] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cursorPosition[0] == row && cursorPosition[1] == column && showCursor)
|
|
cursor = true;
|
|
else if (cursorPosition[0] == r && cursorPosition[1] == c && showCursor && (cellAttributesRC.dh || (cellAttributesRC.dw && !br)))
|
|
cursor = true;
|
|
else
|
|
cursor = false;
|
|
|
|
if ((newsflash || subtitle) && row == 0 && (pageSearch || (numberEntry && column < 6))){
|
|
boxed = true;
|
|
}
|
|
|
|
// if bit 5 of back colour is set, this is default background
|
|
if (cellAttributesRowColumn.backColour & 0x20 && !mixMode){
|
|
if (blackBackgroundColourSubstitution || column > 39){
|
|
// if background substitution on, or cell is in side panel, change colour to the fullRowColour
|
|
cellAttributesRowColumn.backColour = fullRowColour[row];
|
|
}
|
|
}
|
|
|
|
let foreColourNumber;
|
|
let backColourNumber;
|
|
|
|
if (cellAttributesRC.invert){ // invert colour
|
|
foreColourNumber = cellAttributesRC.backColour;
|
|
backColourNumber = cellAttributesRC.foreColour;
|
|
} else {
|
|
foreColourNumber = cellAttributesRC.foreColour;
|
|
backColourNumber = cellAttributesRC.backColour;
|
|
}
|
|
|
|
let shiftright = 0;
|
|
let shiftdown = 0;
|
|
let width = 1;
|
|
let height = 1;
|
|
if (dw){
|
|
width = 2;
|
|
if (rh) /* right hand side */
|
|
shiftright = 6;
|
|
}
|
|
if (dh){
|
|
height = 2;
|
|
if (br)
|
|
shiftdown = 5;
|
|
}
|
|
|
|
let foreColour;
|
|
let backColour;
|
|
let flashforeColour;
|
|
let flashBackColour;
|
|
|
|
switch (cellAttributesRC.flash & 3){
|
|
case 0: // steady
|
|
foreColour = getColour(foreColourNumber, row, boxed, false);
|
|
backColour = getColour(backColourNumber, row, boxed, false);
|
|
flashforeColour = foreColour;
|
|
flashBackColour = backColour;
|
|
break;
|
|
case 1: // normal flash to background
|
|
foreColour = getColour(foreColourNumber, row, boxed, false);
|
|
backColour = getColour(backColourNumber, row, boxed, false);
|
|
flashforeColour = backColour;
|
|
flashBackColour = backColour;
|
|
break;
|
|
case 2: // invert phase of flash to background colour
|
|
flashforeColour = getColour(foreColourNumber, row, boxed, false);
|
|
flashBackColour = getColour(backColourNumber, row, boxed, false);
|
|
foreColour = flashBackColour;
|
|
backColour = flashBackColour;
|
|
break;
|
|
case 3: // flash to corresponding colour in adjacent CLUT
|
|
foreColour = getColour(foreColourNumber, row, boxed, false);
|
|
backColour = getColour(backColourNumber, row, boxed, false);
|
|
flashforeColour = getColour(foreColourNumber ^ 8, row, boxed, false);
|
|
flashBackColour = backColour;
|
|
break;
|
|
}
|
|
|
|
if (hideTeletext && (row || column > 5)){
|
|
// force page transparent except number entry
|
|
foreColour = [0,0,0,0];
|
|
backColour = [0,0,0,0];
|
|
flashforeColour = foreColour;
|
|
flashBackColour = backColour;
|
|
}
|
|
|
|
|
|
|
|
let screenaddr = (row * 34560) + (column * 48); // (((row * 10) * 864) *4) + ((column * 12) * 4)
|
|
let pr,pg,pb,pa,fr,fg,fb,fa;
|
|
for (let py=0; py<10; py++){
|
|
for (let px=0; px<12; px++){
|
|
if (charImageData[((Math.floor(py / height)) + shiftdown) * 12 + Math.floor(px / width)+shiftright]){
|
|
pr = foreColour[0];
|
|
pg = foreColour[1];
|
|
pb = foreColour[2];
|
|
pa = foreColour[3];
|
|
fr = flashforeColour[0];
|
|
fg = flashforeColour[1];
|
|
fb = flashforeColour[2];
|
|
fa = flashforeColour[3];
|
|
if (character < 0x20 && visibleControlChars && !gdrcs && !drcs){
|
|
if (isColourDark(backColour[0],backColour[1],backColour[2])){
|
|
pr = 0xFF;
|
|
pb = 0xFF;
|
|
pg = 0xFF;
|
|
} else {
|
|
pr = 0x00;
|
|
pg = 0x00;
|
|
pb = 0x00;
|
|
}
|
|
if (isColourDark(flashBackColour[0],flashBackColour[1],flashBackColour[2])){
|
|
fr = 0xFF;
|
|
fb = 0xFF;
|
|
fg = 0xFF;
|
|
} else {
|
|
fr = 0x00;
|
|
fg = 0x00;
|
|
fb = 0x00;
|
|
}
|
|
pa |= 0xff;
|
|
fa |= 0xff;
|
|
}
|
|
} else {
|
|
pr = backColour[0];
|
|
pg = backColour[1];
|
|
pb = backColour[2];
|
|
pa = backColour[3];
|
|
fr = flashBackColour[0];
|
|
fg = flashBackColour[1];
|
|
fb = flashBackColour[2];
|
|
fa = flashBackColour[3];
|
|
}
|
|
|
|
let renderGrid = showGrid && row >= firstRow && column >= firstColumn && row <= lastRow && column <= lastColumn;
|
|
|
|
if (cursor || (renderGrid && (py == 0 || py == 9 || px == 0 || px == 11))){
|
|
if (isColourDark(pr,pg,pb)){
|
|
pr += 0x40;
|
|
pg += 0x40;
|
|
pb += 0x40;
|
|
} else {
|
|
pr -= 0x40;
|
|
pb -= 0x40;
|
|
pg -= 0x40;
|
|
}
|
|
|
|
if (isColourDark(fr,fg,fb)){
|
|
fr += 0x40;
|
|
fb += 0x40;
|
|
fg += 0x40;
|
|
} else {
|
|
fr -= 0x40;
|
|
fg -= 0x40;
|
|
fb -= 0x40;
|
|
}
|
|
|
|
pa = 0xff;
|
|
fa = 0xff;
|
|
}
|
|
|
|
flashPixelDataData[screenaddr] = fr;
|
|
steadyPixelDataData[screenaddr++] = pr;
|
|
flashPixelDataData[screenaddr] = fg;
|
|
steadyPixelDataData[screenaddr++] = pg;
|
|
flashPixelDataData[screenaddr] = fb;
|
|
steadyPixelDataData[screenaddr++] = pb;
|
|
flashPixelDataData[screenaddr] = fa;
|
|
steadyPixelDataData[screenaddr++] = pa;
|
|
}
|
|
screenaddr += 3408; // ((864-12)*4)
|
|
}
|
|
}
|
|
|
|
let xOffset; /* gets updated by blitScreen() */
|
|
let scaleFactor;
|
|
let yScaleFactor;
|
|
let oldScaleFactor = 0;
|
|
let oldWidth = 0;
|
|
|
|
let backbufferCanvas = createCanvas();
|
|
let backbufferCanvasContext = backbufferCanvas.getContext("2d");
|
|
let teletextOpacity = 1;
|
|
|
|
let renderSidePanels;
|
|
let canvasWidth;
|
|
let rightSidePanelWidth;
|
|
let leftSidePanelWidth
|
|
let leftBorder;
|
|
let rightBorder;
|
|
let rowCounter;
|
|
|
|
let lastFlashTimestamp;
|
|
let lastFrameTimestamp;
|
|
|
|
let start = Date.now();
|
|
let foo = 0;
|
|
|
|
let animate = function(){
|
|
requestAnimationFrame(animate);
|
|
|
|
let now = Date.now();
|
|
|
|
if (flashEnabled){
|
|
let elapsed = (now - lastFlashTimestamp);
|
|
if (elapsed >= (1000/6)){
|
|
lastFlashTimestamp = now - (elapsed % (1000/6));
|
|
flashinterval();
|
|
}
|
|
}
|
|
|
|
if (blitScreenEnabled){
|
|
// limit the framerate to 25fps
|
|
let elapsed = (now - lastFrameTimestamp);
|
|
if (elapsed >= 40){
|
|
lastFrameTimestamp = now - (elapsed % 40);
|
|
|
|
blitScreen();
|
|
}
|
|
}
|
|
}
|
|
|
|
function blitScreen(){
|
|
updateFlash();
|
|
if ((renderLevel < 2) || ((renderLevel < 3) && !sidePanelStatusFlag)){
|
|
renderSidePanels = false;
|
|
} else {
|
|
renderSidePanels = leftSidePanel || rightSidePanel;
|
|
}
|
|
|
|
rightSidePanelWidth = (16 - sidePanelColumns) * 12;
|
|
leftSidePanelWidth = (leftSidePanel && !rightSidePanel && sidePanelColumns == 0 ? 16 : sidePanelColumns) * 12;
|
|
|
|
if (widescreen){
|
|
canvasWidth = (1024 / 1.2);
|
|
} else if ((leftSidePanel|rightSidePanel) && renderSidePanels){
|
|
if ((leftSidePanel ? leftSidePanelWidth : 0) + (rightSidePanel ? rightSidePanelWidth : 0) + 480 > (688 / 1.2)){
|
|
// maintain the caption safe border
|
|
canvasWidth = ((leftSidePanel ? leftSidePanelWidth : 0) + (rightSidePanel ? rightSidePanelWidth : 0) + 480) * 768 / 688;
|
|
} else {
|
|
canvasWidth = (768 / 1.2);
|
|
}
|
|
} else {
|
|
canvasWidth = (768 / 1.2);
|
|
}
|
|
|
|
if (renderSidePanels){
|
|
if (leftSidePanel && rightSidePanel) {
|
|
xOffset = Math.ceil((canvasWidth - (480 + (leftSidePanel ? leftSidePanelWidth : 0) + (rightSidePanel ? rightSidePanelWidth : 0))) / 2 + leftSidePanelWidth);
|
|
} else if (rightSidePanel){
|
|
xOffset = Math.ceil((canvasWidth - (480 + rightSidePanelWidth)) / 2);
|
|
} else {
|
|
xOffset = Math.ceil((canvasWidth - (480 + leftSidePanelWidth)) / 2 + leftSidePanelWidth);
|
|
}
|
|
} else {
|
|
xOffset = Math.ceil((canvasWidth - 480) / 2);
|
|
}
|
|
|
|
leftBorder = xOffset;
|
|
rightBorder = xOffset+480;
|
|
|
|
backbufferCanvas.width = canvasWidth;
|
|
backbufferCanvas.height = 576;
|
|
|
|
backbufferCanvasContext.imageSmoothingEnabled = false; // don't smooth drawing teletext pixels
|
|
backbufferCanvasContext.msImageSmoothingEnabled = false;
|
|
backbufferCanvasContext.globalAlpha = teletextOpacity;
|
|
|
|
if (!(newsflash || subtitle || fullScreenColour == 8 || mixMode || hideTeletext)){
|
|
backbufferCanvasContext.fillStyle = getColourHTML(fullScreenColour);
|
|
backbufferCanvasContext.fillRect(0,0,(1024 / 1.2),38);
|
|
backbufferCanvasContext.fillRect(0,538,(1024 / 1.2),38);
|
|
}
|
|
|
|
if (!hideTeletext){
|
|
for (let rowCounter=0;rowCounter<25;rowCounter++){
|
|
if (!(newsflash || subtitle || mixMode)){
|
|
if (fullRowColour[rowCounter] == 8){
|
|
/* TODO: figure out if this code should be here
|
|
if (fullScreenColour != 8){
|
|
backbufferCanvasContext.fillStyle = getColourHTML(fullScreenColour);
|
|
backbufferCanvasContext.fillRect(0,rowCounter*20 + 38,leftBorder,20);
|
|
backbufferCanvasContext.fillRect(rightBorder,rowCounter*20 + 38,canvasWidth-rightBorder,20);
|
|
}*/
|
|
} else {
|
|
backbufferCanvasContext.fillStyle = getColourHTML(fullRowColour[rowCounter]);
|
|
backbufferCanvasContext.fillRect(0,rowCounter*20 + 38,leftBorder,20);
|
|
backbufferCanvasContext.fillRect(rightBorder,rowCounter*20 + 38,canvasWidth-rightBorder,20);
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (stretchMode){
|
|
case 1:
|
|
backbufferCanvasContext.drawImage(backbufferCanvas,0,38,canvasWidth,240,0,38,canvasWidth,480);
|
|
break;
|
|
case 2:
|
|
backbufferCanvasContext.drawImage(backbufferCanvas,0,278,canvasWidth,240,0,38,canvasWidth,480);
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (stretchMode){
|
|
default:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,0,0,480,240,xOffset,38,480,480);
|
|
break;
|
|
case 1:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,0,0,480,120,xOffset,38,480,480);
|
|
break;
|
|
case 2:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,0,120,480,120,xOffset,38,480,480);
|
|
}
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,0,240,480,10,xOffset,518,480,20);
|
|
if (displayRow25)
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,0,250,480,10,xOffset,538,480,20);
|
|
|
|
if (renderSidePanels){
|
|
if (leftSidePanel && leftSidePanelWidth > 0){
|
|
/* draw left side panel */
|
|
switch (stretchMode){
|
|
default:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,864-leftSidePanelWidth,0,leftSidePanelWidth,240,xOffset-leftSidePanelWidth,38,leftSidePanelWidth,480);
|
|
break;
|
|
case 1:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,864-leftSidePanelWidth,0,leftSidePanelWidth,120,xOffset-leftSidePanelWidth,38,leftSidePanelWidth,480);
|
|
break;
|
|
case 2:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,864-leftSidePanelWidth,120,leftSidePanelWidth,120,xOffset-leftSidePanelWidth,38,leftSidePanelWidth,480);
|
|
}
|
|
|
|
leftBorder -= leftSidePanelWidth;
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,864-leftSidePanelWidth,240,leftSidePanelWidth,10,xOffset-leftSidePanelWidth,518,leftSidePanelWidth,20);
|
|
}
|
|
|
|
if (rightSidePanel && rightSidePanelWidth > 0){
|
|
/* draw right side panel */
|
|
switch (stretchMode){
|
|
default:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,480,0,rightSidePanelWidth,240,xOffset+480,38,rightSidePanelWidth,480);
|
|
break;
|
|
case 1:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,480,0,rightSidePanelWidth,120,xOffset+480,38,rightSidePanelWidth,480);
|
|
break;
|
|
case 2:
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,480,120,rightSidePanelWidth,120,xOffset+480,38,rightSidePanelWidth,480);
|
|
}
|
|
backbufferCanvasContext.drawImage(characterArrayCanvas,480,240,rightSidePanelWidth,10,xOffset+480,518,rightSidePanelWidth,20);
|
|
rightBorder += rightSidePanelWidth;
|
|
}
|
|
}
|
|
|
|
if (screenDiv.clientHeight > 586)
|
|
scaleFactor = Math.ceil(screenDiv.clientHeight / 586);
|
|
else
|
|
scaleFactor = 1;
|
|
yScaleFactor = (widescreen?1024:768) / canvasWidth;
|
|
|
|
if (scaleFactor != oldScaleFactor || oldWidth != canvasWidth){
|
|
// only change canvas size if it is actually required
|
|
teletextCanvas.width = yScaleFactor * scaleFactor * canvasWidth;
|
|
teletextCanvas.height = 576 * scaleFactor;
|
|
teletextCanvasContext.scale(yScaleFactor * scaleFactor, scaleFactor);
|
|
oldWidth = canvasWidth;
|
|
oldScaleFactor = scaleFactor
|
|
}
|
|
|
|
teletextCanvasContext.imageSmoothingEnabled = true; // smooth the final write to the canvas
|
|
teletextCanvasContext.msImageSmoothingEnabled = true;
|
|
teletextCanvasContext.drawImage(backgroundCanvas,0,0,backgroundCanvas.width,backgroundCanvas.height,0,0,canvasWidth,576);
|
|
teletextCanvasContext.drawImage(backbufferCanvas,0,0,canvasWidth,576);
|
|
|
|
if(disableBlitScreen)
|
|
blitScreenEnabled = false;
|
|
}
|
|
|
|
/* ------------------- initialise renderer ----------------------- */
|
|
let teletextCanvasContext = teletextCanvas.getContext("2d");
|
|
teletextCanvasContext.scale(2,2);
|
|
|
|
initCharsetData();
|
|
|
|
initializeLevel1PageArray();
|
|
resetPresentationData();
|
|
resetPOPArrays();
|
|
resetDRCSArrays();
|
|
|
|
cursorPosition[0] = 0;
|
|
cursorPosition[1] = 0;
|
|
|
|
/* create backbuffer canvas */
|
|
let characterArrayCanvas = createCanvas(864,260);
|
|
let characterArrayCanvasContext = characterArrayCanvas.getContext("2d");
|
|
|
|
/* backbuffer for flashing */
|
|
let flashOnCanvas = createCanvas(864,260);
|
|
let flashOnCanvasContext = flashOnCanvas.getContext("2d");
|
|
|
|
let steadyPixelData = characterArrayCanvasContext.createImageData(864, 260);
|
|
let steadyPixelDataData = steadyPixelData.data;
|
|
let flashPixelData = flashOnCanvasContext.createImageData(864, 260);
|
|
let flashPixelDataData = flashPixelData.data;
|
|
|
|
hideTeletext = false;
|
|
numberEntry = false;
|
|
pageSearch = false;
|
|
|
|
decodePage(); // must call decodePage to set the page presentation variables before any functions try to access them!
|
|
redrawScreen();
|
|
|
|
let blitScreenEnabled = true;
|
|
let disableBlitScreen = false;
|
|
|
|
flashEnabled = true;
|
|
|
|
// initialise timers
|
|
lastFrameTimestamp = Date.now();
|
|
lastFlashTimestamp = Date.now();
|
|
|
|
if (typeof requestAnimationFrame !== 'undefined')
|
|
requestAnimationFrame(animate); // start updating display (not in nodejs)
|
|
|
|
/* ---------------------- public functions and variables ------------------ */
|
|
this.level1PageArray = level1PageArray;
|
|
this.packetX26TripletArray = packetX26TripletArray;
|
|
this.pagePresentationData = pagePresentationData;
|
|
this.pageSettings = pageSettings;
|
|
this.gdrcsArray = gdrcsArray;
|
|
this.drcsArray = drcsArray;
|
|
this.gpopArray = gpopArray;
|
|
this.popArray = popArray;
|
|
this.defaultObjects = defaultObjects;
|
|
|
|
this.initializeLevel1PageArray = initializeLevel1PageArray;
|
|
this.resetPresentationData = resetPresentationData;
|
|
this.resetDRCSArrays = resetDRCSArrays;
|
|
this.resetPOPArrays = resetPOPArrays;
|
|
this.decodePage = decodePage;
|
|
this.redrawScreen = redrawScreen;
|
|
|
|
this.enable = function(val){
|
|
if (val === true){
|
|
blitScreenEnabled = true;
|
|
disableBlitScreen = false;
|
|
} else {
|
|
disableBlitScreen = true;
|
|
}
|
|
}
|
|
|
|
this.setWidescreen = function(val){widescreen = val;}
|
|
this.setRenderLevel = function(val){renderLevel = val;}
|
|
this.setReveal = function(val){reveal = val;}
|
|
this.setMixMode = function(val){if (mixMode != val){mixMode = val; decodePage(); redrawScreen();};}
|
|
this.setStretchMode = function(val){stretchMode = val;}
|
|
|
|
/* make 1Hz flash rate have 2:1 ratio instead of 50% duty cycle */
|
|
this.setUnevenFlashRatio = function(val){flashPhasesLUT[0] = (val)?0x30:0x38; };
|
|
|
|
this.setDisplayRow24 = function(val){displayRow24 = val;}
|
|
this.setDisplayRow25 = function(val){displayRow25 = val;}
|
|
|
|
this.getDefaultCLUTEntry = function(i){return defaultCLUT[i];}
|
|
|
|
/* ----------- deep hooks into renderer internals for page editor --------- */
|
|
this.getSwitch = function(r,c){return screenAttributesArray[r][c].switchG0;}
|
|
this.getMosaics = function(r,c){return screenAttributesArray[r][c].mosaics;}
|
|
this.getDoubleSize = function(r,c){let obj = screenAttributesArray[r][c]; return obj.dh?1:0|obj.dw?2:0|obj.br?4:0|obj.rh?8:0; }
|
|
this.getTeletextCanvas = function(){return teletextCanvas;}
|
|
this.getBackbufferCanvas = function(){return backbufferCanvas;}
|
|
this.getCharacterArrayCanvas = function(){return characterArrayCanvas;}
|
|
this.setTeletextOpacity = function(val){teletextOpacity = val};
|
|
this.getYScaleFactor = function(){return yScaleFactor;}
|
|
this.getScaleFactor = function(){return scaleFactor;}
|
|
this.getXOffset = function(){return xOffset;}
|
|
this.getDefaultG0Set = function(){ return defaultG0Set; }
|
|
this.getDefaultNOS = function(){ return defaultNOS; }
|
|
this.getSecondG0Set = function(){ return secondG0Set; }
|
|
this.getSecondNOS = function(){ return secondNOS; }
|
|
this.getColour = getColour;
|
|
this.getColourHTML = getColourHTML;
|
|
this.isColourDark = isColourDark;
|
|
this.getForegroundMapping = function(i){return foregroundMapping[i];}
|
|
this.getBackgroundMapping = function(i){return backgroundMapping[i];}
|
|
this.setBounds = function(a,b,c,d){firstRow = a; firstColumn = b; lastRow = c; lastColumn = d;}
|
|
this.setShowGrid = function(val){showGrid = val;}
|
|
this.setShowCursor = function(val){showCursor = val;}
|
|
this.setVisibleControlChars = function(val){visibleControlChars = val;}
|
|
this.moveCursor = moveCursor;
|
|
this.getCursorX = function(){return cursorPosition[1];}
|
|
this.getCursorY = function(){return cursorPosition[0];}
|
|
this.setCursorX = function(val){cursorPosition[1] = val;}
|
|
this.setCursorY = function(val){cursorPosition[0] = val;}
|
|
this.blitScreen = blitScreen;
|
|
this.updateFlash = updateFlash;
|
|
this.setFlashOn = function(val){flashEnabled = val;}
|
|
this.setFlashphase = function(val){flashphase = val;}
|
|
this.renderRow = renderRow;
|
|
this.immediateWriteSteadyPixelData = function(){characterArrayCanvasContext.putImageData(steadyPixelData,0,0);}
|
|
|
|
/* for teletext viewer/tv emulator */
|
|
this.screenAttributesArray = screenAttributesArray; // raw access to the screen array
|
|
this.enableSidePanels = function(val){leftSidePanel = val; rightSidePanel = val; sidePanelColumns = 8} // enable or disable equal side panels
|
|
this.hideTeletext = function(val){hideTeletext = val; redrawScreen();}
|
|
this.numberEntry = function(val){numberEntry = val;}
|
|
this.pageSearch = function(val){pageSearch = val;}
|
|
this.resetPageSettings = function(){newsflash = false; subtitle = false; suppressHeader = false; inhibitDisplay = false;}
|
|
}
|
|
|
|
/* embed chardata.png data */
|
|
chardataImage.src = "data:image/png;base64,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"
|