Rewrite term_screen with canvas
This commit is contained in:
+383
-318
@@ -1,346 +1,417 @@
|
|||||||
var Screen = (function () {
|
// Some non-bold Fraktur symbols are outside the contiguous block
|
||||||
var W = 0, H = 0; // dimensions
|
const frakturExceptions = {
|
||||||
var inited = false;
|
|
||||||
|
|
||||||
var cursor = {
|
|
||||||
a: false, // active (blink state)
|
|
||||||
x: 0, // 0-based coordinates
|
|
||||||
y: 0,
|
|
||||||
fg: 7, // colors 0-15
|
|
||||||
bg: 0,
|
|
||||||
attrs: 0,
|
|
||||||
suppress: false, // do not turn on in blink interval (for safe moving)
|
|
||||||
forceOn: false, // force on unless hanging: used to keep cursor visible during move
|
|
||||||
hidden: false, // do not show (DEC opt)
|
|
||||||
hanging: false, // cursor at column "W+1" - not visible
|
|
||||||
};
|
|
||||||
|
|
||||||
var screen = [];
|
|
||||||
var blinkIval;
|
|
||||||
var cursorFlashStartIval;
|
|
||||||
|
|
||||||
// Some non-bold Fraktur symbols are outside the contiguous block
|
|
||||||
var frakturExceptions = {
|
|
||||||
'C': '\u212d',
|
'C': '\u212d',
|
||||||
'H': '\u210c',
|
'H': '\u210c',
|
||||||
'I': '\u2111',
|
'I': '\u2111',
|
||||||
'R': '\u211c',
|
'R': '\u211c',
|
||||||
'Z': '\u2128',
|
'Z': '\u2128'
|
||||||
};
|
}
|
||||||
|
|
||||||
// for BEL
|
// constants for decoding the update blob
|
||||||
var audioCtx = null;
|
const SEQ_SET_COLOR_ATTR = 1
|
||||||
try {
|
const SEQ_REPEAT = 2
|
||||||
audioCtx = new (window.AudioContext || window.audioContext || window.webkitAudioContext)();
|
const SEQ_SET_COLOR = 3
|
||||||
} catch (er) {
|
const SEQ_SET_ATTR = 4
|
||||||
console.error("No AudioContext!", er);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get cell under cursor */
|
const themes = [
|
||||||
function _curCell() {
|
[
|
||||||
return screen[cursor.y*W + cursor.x];
|
'#111213',
|
||||||
}
|
'#CC0000',
|
||||||
|
'#4E9A06',
|
||||||
|
'#C4A000',
|
||||||
|
'#3465A4',
|
||||||
|
'#75507B',
|
||||||
|
'#06989A',
|
||||||
|
'#D3D7CF',
|
||||||
|
'#555753',
|
||||||
|
'#EF2929',
|
||||||
|
'#8AE234',
|
||||||
|
'#FCE94F',
|
||||||
|
'#729FCF',
|
||||||
|
'#AD7FA8',
|
||||||
|
'#34E2E2',
|
||||||
|
'#EEEEEC'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
/** Safely move cursor */
|
class TermScreen {
|
||||||
function cursorSet(y, x) {
|
constructor () {
|
||||||
// Hide and prevent from showing up during the move
|
this.canvas = document.createElement('canvas')
|
||||||
cursor.suppress = true;
|
this.ctx = this.canvas.getContext('2d')
|
||||||
_draw(_curCell(), false);
|
|
||||||
cursor.x = x;
|
|
||||||
cursor.y = y;
|
|
||||||
// Show again
|
|
||||||
cursor.suppress = false;
|
|
||||||
_draw(_curCell());
|
|
||||||
}
|
|
||||||
|
|
||||||
function alpha2fraktur(t) {
|
if ('AudioContext' in window || 'webkitAudioContext' in window) {
|
||||||
// perform substitution
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
||||||
if (t >= 'a' && t <= 'z') {
|
|
||||||
t = String.fromCodePoint(0x1d51e - 97 + t.charCodeAt(0));
|
|
||||||
}
|
|
||||||
else if (t >= 'A' && t <= 'Z') {
|
|
||||||
// this set is incomplete, some exceptions are needed
|
|
||||||
if (frakturExceptions.hasOwnProperty(t)) {
|
|
||||||
t = frakturExceptions[t];
|
|
||||||
} else {
|
} else {
|
||||||
t = String.fromCodePoint(0x1d504 - 65 + t.charCodeAt(0));
|
console.warn('No AudioContext!')
|
||||||
}
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update cell on display. inv = invert (for cursor) */
|
this.cursor = {
|
||||||
function _draw(cell, inv) {
|
x: 0,
|
||||||
if (!cell) return;
|
y: 0,
|
||||||
if (typeof inv == 'undefined') {
|
|
||||||
inv = cursor.a && cursor.x == cell.x && cursor.y == cell.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fg, bg, cn, t;
|
|
||||||
|
|
||||||
fg = inv ? cell.bg : cell.fg;
|
|
||||||
bg = inv ? cell.fg : cell.bg;
|
|
||||||
|
|
||||||
t = cell.t;
|
|
||||||
if (!t.length) t = ' ';
|
|
||||||
|
|
||||||
cn = 'fg' + fg + ' bg' + bg;
|
|
||||||
if (cell.attrs & (1<<0)) cn += ' bold';
|
|
||||||
if (cell.attrs & (1<<1)) cn += ' faint';
|
|
||||||
if (cell.attrs & (1<<2)) cn += ' italic';
|
|
||||||
if (cell.attrs & (1<<3)) cn += ' under';
|
|
||||||
if (cell.attrs & (1<<4)) cn += ' blink';
|
|
||||||
if (cell.attrs & (1<<5)) {
|
|
||||||
cn += ' fraktur';
|
|
||||||
t = alpha2fraktur(t);
|
|
||||||
}
|
|
||||||
if (cell.attrs & (1<<6)) cn += ' strike';
|
|
||||||
|
|
||||||
cell.slot.textContent = t;
|
|
||||||
cell.elem.className = cn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show entire screen */
|
|
||||||
function _drawAll() {
|
|
||||||
for (var i = W*H-1; i>=0; i--) {
|
|
||||||
_draw(screen[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _rebuild(rows, cols) {
|
|
||||||
W = cols;
|
|
||||||
H = rows;
|
|
||||||
|
|
||||||
/* Build screen & show */
|
|
||||||
var cOuter, cInner, cell, screenDiv = qs('#screen');
|
|
||||||
|
|
||||||
// Empty the screen node
|
|
||||||
while (screenDiv.firstChild) screenDiv.removeChild(screenDiv.firstChild);
|
|
||||||
|
|
||||||
screen = [];
|
|
||||||
|
|
||||||
for(var i = 0; i < W*H; i++) {
|
|
||||||
cOuter = mk('span');
|
|
||||||
cInner = mk('span');
|
|
||||||
|
|
||||||
/* Mouse tracking */
|
|
||||||
(function() {
|
|
||||||
var x = i % W;
|
|
||||||
var y = Math.floor(i / W);
|
|
||||||
cOuter.addEventListener('mouseenter', function (evt) {
|
|
||||||
Input.onMouseMove(x, y);
|
|
||||||
});
|
|
||||||
cOuter.addEventListener('mousedown', function (evt) {
|
|
||||||
Input.onMouseDown(x, y, evt.button+1);
|
|
||||||
});
|
|
||||||
cOuter.addEventListener('mouseup', function (evt) {
|
|
||||||
Input.onMouseUp(x, y, evt.button+1);
|
|
||||||
});
|
|
||||||
cOuter.addEventListener('contextmenu', function (evt) {
|
|
||||||
if (Input.mouseTracksClicks()) {
|
|
||||||
evt.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cOuter.addEventListener('mousewheel', function (evt) {
|
|
||||||
Input.onMouseWheel(x, y, evt.deltaY>0?1:-1);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* End of line */
|
|
||||||
if ((i > 0) && (i % W == 0)) {
|
|
||||||
screenDiv.appendChild(mk('br'));
|
|
||||||
}
|
|
||||||
/* The cell */
|
|
||||||
cOuter.appendChild(cInner);
|
|
||||||
screenDiv.appendChild(cOuter);
|
|
||||||
|
|
||||||
cell = {
|
|
||||||
t: ' ',
|
|
||||||
fg: 7,
|
fg: 7,
|
||||||
bg: 0, // the colors will be replaced immediately as we receive data (user won't see this)
|
bg: 0,
|
||||||
attrs: 0,
|
attrs: 0,
|
||||||
elem: cOuter,
|
blinkOn: false,
|
||||||
slot: cInner,
|
visible: true,
|
||||||
x: i % W,
|
hanging: false,
|
||||||
y: Math.floor(i / W),
|
blinkInterval: null
|
||||||
};
|
}
|
||||||
screen.push(cell);
|
|
||||||
_draw(cell);
|
this._colors = themes[0]
|
||||||
|
|
||||||
|
this._window = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
devicePixelRatio: 1,
|
||||||
|
fontFamily: '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", ' +
|
||||||
|
'monospace',
|
||||||
|
fontSize: 20,
|
||||||
|
gridScaleX: 1.0,
|
||||||
|
gridScaleY: 1.2,
|
||||||
|
blinkStyleOn: true,
|
||||||
|
blinkInterval: null
|
||||||
|
}
|
||||||
|
this.windowState = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
devicePixelRatio: 0,
|
||||||
|
gridScaleX: 0,
|
||||||
|
gridScaleY: 0,
|
||||||
|
fontFamily: '',
|
||||||
|
fontSize: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this
|
||||||
|
this.window = new Proxy(this._window, {
|
||||||
|
set (target, key, value, receiver) {
|
||||||
|
target[key] = value
|
||||||
|
self.updateSize()
|
||||||
|
self.scheduleDraw()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.screen = []
|
||||||
|
this.screenFG = []
|
||||||
|
this.screenBG = []
|
||||||
|
this.screenAttrs = []
|
||||||
|
|
||||||
|
this.resetBlink()
|
||||||
|
this.resetCursorBlink()
|
||||||
|
}
|
||||||
|
|
||||||
|
get colors () { return this._colors }
|
||||||
|
set colors (theme) {
|
||||||
|
this._colors = theme
|
||||||
|
this.scheduleDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// schedule a draw in the next tick
|
||||||
|
scheduleDraw () {
|
||||||
|
clearTimeout(this._scheduledDraw)
|
||||||
|
this._scheduledDraw = setTimeout(() => this.draw(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFont (modifiers = {}) {
|
||||||
|
let fontStyle = modifiers.style || 'normal'
|
||||||
|
let fontWeight = modifiers.weight || 'normal'
|
||||||
|
return `${fontStyle} normal ${fontWeight} ${
|
||||||
|
this.window.fontSize}px ${this.window.fontFamily}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getCharSize () {
|
||||||
|
this.ctx.font = this.getFont()
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: this.ctx.measureText(' ').width,
|
||||||
|
height: this.window.fontSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Init the terminal */
|
updateSize () {
|
||||||
function _init() {
|
this._window.devicePixelRatio = window.devicePixelRatio || 1
|
||||||
/* Cursor blinking */
|
|
||||||
clearInterval(blinkIval);
|
let didChange = false
|
||||||
blinkIval = setInterval(function () {
|
for (let key in this.windowState) {
|
||||||
cursor.a = !cursor.a;
|
if (this.windowState[key] !== this.window[key]) {
|
||||||
if (cursor.hidden || cursor.hanging) {
|
didChange = true
|
||||||
cursor.a = false;
|
this.windowState[key] = this.window[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cursor.suppress) {
|
if (didChange) {
|
||||||
_draw(_curCell(), cursor.forceOn || cursor.a);
|
const {
|
||||||
|
width, height, devicePixelRatio, gridScaleX, gridScaleY, fontSize
|
||||||
|
} = this.window
|
||||||
|
const charSize = this.getCharSize()
|
||||||
|
|
||||||
|
this.canvas.width = width * devicePixelRatio * charSize.width * gridScaleX
|
||||||
|
this.canvas.style.width = `${width * charSize.width * gridScaleX}px`
|
||||||
|
this.canvas.height = height * devicePixelRatio * charSize.height *
|
||||||
|
gridScaleY
|
||||||
|
this.canvas.style.height = `${height * charSize.height * gridScaleY}px`
|
||||||
}
|
}
|
||||||
}, 500);
|
|
||||||
|
|
||||||
/* blink attribute animation */
|
|
||||||
setInterval(function () {
|
|
||||||
$('#screen').removeClass('blink-hide');
|
|
||||||
setTimeout(function () {
|
|
||||||
$('#screen').addClass('blink-hide');
|
|
||||||
}, 800); // 200 ms ON
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
inited = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// constants for decoding the update blob
|
resetCursorBlink () {
|
||||||
var SEQ_SET_COLOR_ATTR = 1;
|
clearInterval(this.cursor.blinkInterval)
|
||||||
var SEQ_REPEAT = 2;
|
this.cursor.blinkInterval = setInterval(() => {
|
||||||
var SEQ_SET_COLOR = 3;
|
this.cursor.blinkOn = !this.cursor.blinkOn
|
||||||
var SEQ_SET_ATTR = 4;
|
this.scheduleDraw()
|
||||||
|
}, 500)
|
||||||
/** Parse received screen update object (leading S removed already) */
|
|
||||||
function _load_content(str) {
|
|
||||||
var i = 0, ci = 0, j, jc, num, num2, t = ' ', fg, bg, attrs, cell;
|
|
||||||
|
|
||||||
if (!inited) _init();
|
|
||||||
|
|
||||||
var cursorMoved;
|
|
||||||
|
|
||||||
// Set size
|
|
||||||
num = parse2B(str, i); i += 2; // height
|
|
||||||
num2 = parse2B(str, i); i += 2; // width
|
|
||||||
if (num != H || num2 != W) {
|
|
||||||
_rebuild(num, num2);
|
|
||||||
}
|
}
|
||||||
// console.log("Size ",num, num2);
|
|
||||||
|
|
||||||
// Cursor position
|
resetBlink () {
|
||||||
num = parse2B(str, i); i += 2; // row
|
clearInterval(this.window.blinkInterval)
|
||||||
num2 = parse2B(str, i); i += 2; // col
|
let intervals = 0
|
||||||
cursorMoved = (cursor.x != num2 || cursor.y != num);
|
this.window.blinkInterval = setInterval(() => {
|
||||||
cursorSet(num, num2);
|
intervals++
|
||||||
// console.log("Cursor at ",num, num2);
|
if (intervals >= 4 && this.window.blinkStyleOn) {
|
||||||
|
this.window.blinkStyleOn = false
|
||||||
|
intervals = 0
|
||||||
|
} else {
|
||||||
|
this.window.blinkStyleOn = true
|
||||||
|
intervals = 0
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
// Attributes
|
drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }) {
|
||||||
num = parse2B(str, i); i += 2; // fg bg attribs
|
const ctx = this.ctx
|
||||||
cursor.hidden = !(num & (1<<0)); // DEC opt "visible"
|
ctx.fillStyle = this.colors[bg]
|
||||||
cursor.hanging = !!(num & (1<<1));
|
ctx.globalCompositeOperation = 'destination-over'
|
||||||
// console.log("Attributes word ",num.toString(16)+'h');
|
ctx.fillRect(x * cellWidth, y * cellHeight,
|
||||||
|
Math.ceil(cellWidth), Math.ceil(cellHeight))
|
||||||
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
|
|
||||||
|
let fontModifiers = {}
|
||||||
|
let underline = false
|
||||||
|
let blink = false
|
||||||
|
let strike = false
|
||||||
|
if (attrs & 1) fontModifiers.weight = 'bold'
|
||||||
|
if (attrs & 1 << 1) ctx.globalAlpha = 0.5
|
||||||
|
if (attrs & 1 << 2) fontModifiers.style = 'italic'
|
||||||
|
if (attrs & 1 << 3) underline = true
|
||||||
|
if (attrs & 1 << 4) blink = true
|
||||||
|
if (attrs & 1 << 5) text = TermScreen.alphaToFraktur(text)
|
||||||
|
if (attrs & 1 << 6) strike = true
|
||||||
|
|
||||||
|
if (!blink || this.window.blinkStyleOn) {
|
||||||
|
ctx.font = this.getFont(fontModifiers)
|
||||||
|
ctx.fillStyle = this.colors[fg]
|
||||||
|
ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight)
|
||||||
|
|
||||||
|
if (underline || strike) {
|
||||||
|
let lineY = underline
|
||||||
|
? y * cellHeight * charSize.height
|
||||||
|
: (y + 0.5) * cellHeight
|
||||||
|
|
||||||
|
ctx.strokeStyle = this.colors[fg]
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.moveTo(x * cellWidth, lineY)
|
||||||
|
ctx.lineTo((x + 1) * cellWidth, lineY)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
draw () {
|
||||||
|
const ctx = this.ctx
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
devicePixelRatio,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
gridScaleX,
|
||||||
|
gridScaleY
|
||||||
|
} = this.window
|
||||||
|
|
||||||
|
const charSize = this.getCharSize()
|
||||||
|
const cellWidth = charSize.width * gridScaleX
|
||||||
|
const cellHeight = charSize.height * gridScaleY
|
||||||
|
const screenWidth = width * cellWidth
|
||||||
|
const screenHeight = height * cellHeight
|
||||||
|
const screenLength = width * height
|
||||||
|
|
||||||
|
ctx.setTransform(this.window.devicePixelRatio, 0, 0,
|
||||||
|
this.window.devicePixelRatio, 0, 0)
|
||||||
|
ctx.clearRect(0, 0, screenWidth, screenHeight)
|
||||||
|
|
||||||
|
ctx.font = this.getFont()
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
|
for (let cell = 0; cell < screenLength; cell++) {
|
||||||
|
let x = cell % width
|
||||||
|
let y = Math.floor(cell / width)
|
||||||
|
let isCursor = this.cursor.x === x && this.cursor.y === y
|
||||||
|
|
||||||
|
let text = this.screen[cell]
|
||||||
|
let fg = isCursor ? this.screenBG[cell] : this.screenFG[cell]
|
||||||
|
let bg = isCursor ? this.screenFG[cell] : this.screenBG[cell]
|
||||||
|
let attrs = this.screenAttrs[cell]
|
||||||
|
|
||||||
|
if (isCursor && fg === bg) bg = fg === 0 ? 7 : 0
|
||||||
|
|
||||||
|
this.drawCell({
|
||||||
|
x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadContent (str) {
|
||||||
|
// current index
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
// window size
|
||||||
|
this.window.height = parse2B(str, i)
|
||||||
|
this.window.width = parse2B(str, i + 2)
|
||||||
|
this.updateSize()
|
||||||
|
i += 4
|
||||||
|
|
||||||
|
// cursor position
|
||||||
|
let [cursorY, cursorX] = [parse2B(str, i), parse2B(str, i + 2)]
|
||||||
|
i += 4
|
||||||
|
let cursorMoved = (cursorX !== this.cursor.x || cursorY !== this.cursor.y)
|
||||||
|
this.cursor.x = cursorX
|
||||||
|
this.cursor.y = cursorY
|
||||||
|
|
||||||
|
if (cursorMoved) this.resetCursorBlink()
|
||||||
|
|
||||||
|
// attributes
|
||||||
|
let attributes = parse2B(str, i)
|
||||||
|
i += 2
|
||||||
|
|
||||||
|
this.cursor.visible = !!(attributes & 1)
|
||||||
|
this.cursor.hanging = !!(attributes & 1 << 0)
|
||||||
|
|
||||||
Input.setAlts(
|
Input.setAlts(
|
||||||
!!(num & (1<<2)), // cursors alt
|
!!(attributes & 1 << 2), // cursors alt
|
||||||
!!(num & (1<<3)), // numpad alt
|
!!(attributes & 1 << 3), // numpad alt
|
||||||
!!(num & (1<<4)) // fn keys alt
|
!!(attributes & 1 << 4) // fn keys alt
|
||||||
);
|
)
|
||||||
|
|
||||||
var mt_click = !!(num & (1<<5));
|
let trackMouseClicks = !!(attributes & 1 << 5)
|
||||||
var mt_move = !!(num & (1<<6));
|
let trackMouseMovement = !!(attributes & 1 << 6)
|
||||||
Input.setMouseMode(
|
|
||||||
mt_click,
|
|
||||||
mt_move
|
|
||||||
);
|
|
||||||
$('#screen').toggleClass('noselect', mt_move);
|
|
||||||
|
|
||||||
var show_buttons = !!(num & (1<<7));
|
Input.setMouseMode(trackMouseClicks, trackMouseMovement)
|
||||||
var show_config_links = !!(num & (1<<8));
|
|
||||||
$('.x-term-conf-btn').toggleClass('hidden', !show_config_links);
|
|
||||||
$('#action-buttons').toggleClass('hidden', !show_buttons);
|
|
||||||
|
|
||||||
fg = 7;
|
let showButtons = !!(attributes & 1 << 7)
|
||||||
bg = 0;
|
let showConfigLinks = !!(attributes & 1 << 8)
|
||||||
attrs = 0;
|
|
||||||
|
|
||||||
// Here come the content
|
$('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks)
|
||||||
while(i < str.length && ci<W*H) {
|
$('#action-buttons').toggleClass('hidden', !showButtons)
|
||||||
|
|
||||||
j = str[i++];
|
// content
|
||||||
jc = j.charCodeAt(0);
|
let fg = 7
|
||||||
if (jc == SEQ_SET_COLOR_ATTR) {
|
let bg = 0
|
||||||
num = parse3B(str, i); i += 3;
|
let attrs = 0
|
||||||
fg = num & 0x0F;
|
let cell = 0 // cell index
|
||||||
bg = (num & 0xF0) >> 4;
|
let text = ' '
|
||||||
attrs = (num & 0xFF00)>>8;
|
let screenLength = this.window.width * this.window.height
|
||||||
|
|
||||||
|
this.screen = new Array(screenLength).fill(' ')
|
||||||
|
this.screenFG = new Array(screenLength).fill(' ')
|
||||||
|
this.screenBG = new Array(screenLength).fill(' ')
|
||||||
|
this.screenAttrs = new Array(screenLength).fill(' ')
|
||||||
|
|
||||||
|
while (i < str.length && cell < screenLength) {
|
||||||
|
let character = str[i++]
|
||||||
|
let charCode = character.charCodeAt(0)
|
||||||
|
|
||||||
|
if (charCode === SEQ_SET_COLOR_ATTR) {
|
||||||
|
let data = parse3B(str, i)
|
||||||
|
i += 3
|
||||||
|
fg = data & 0xF
|
||||||
|
bg = data >> 4 & 0xF
|
||||||
|
attrs = data >> 8 & 0xFF
|
||||||
|
} else if (charCode == SEQ_SET_COLOR) {
|
||||||
|
let data = parse2B(str, i)
|
||||||
|
i += 2
|
||||||
|
fg = data & 0xF
|
||||||
|
bg = data >> 4 & 0xF
|
||||||
|
} else if (charCode === SEQ_SET_ATTR) {
|
||||||
|
let data = parse2B(str, i)
|
||||||
|
i += 2
|
||||||
|
attrs = data & 0xFF
|
||||||
|
} else if (charCode === SEQ_REPEAT) {
|
||||||
|
let count = parse2B(str, i)
|
||||||
|
i += 2
|
||||||
|
for (let j = 0; j < count; j++) {
|
||||||
|
this.screen[cell] = text
|
||||||
|
this.screenFG[cell] = fg
|
||||||
|
this.screenBG[cell] = bg
|
||||||
|
this.screenAttrs[cell] = attrs
|
||||||
|
|
||||||
|
if (++cell > screenLength) break
|
||||||
}
|
}
|
||||||
else if (jc == SEQ_SET_COLOR) {
|
} else {
|
||||||
num = parse2B(str, i); i += 2;
|
// unique cell character
|
||||||
fg = num & 0x0F;
|
this.screen[cell] = text = character
|
||||||
bg = (num & 0xF0) >> 4;
|
this.screenFG[cell] = fg
|
||||||
}
|
this.screenBG[cell] = bg
|
||||||
else if (jc == SEQ_SET_ATTR) {
|
this.screenAttrs[cell] = attrs
|
||||||
num = parse2B(str, i); i += 2;
|
cell++
|
||||||
attrs = num & 0xFF;
|
|
||||||
}
|
|
||||||
else if (jc == SEQ_REPEAT) {
|
|
||||||
num = parse2B(str, i); i += 2;
|
|
||||||
// console.log("Repeat x ",num);
|
|
||||||
for (; num>0 && ci<W*H; num--) {
|
|
||||||
cell = screen[ci++];
|
|
||||||
cell.fg = fg;
|
|
||||||
cell.bg = bg;
|
|
||||||
cell.t = t;
|
|
||||||
cell.attrs = attrs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cell = screen[ci++];
|
|
||||||
// Unique cell character
|
|
||||||
t = cell.t = j;
|
|
||||||
cell.fg = fg;
|
|
||||||
cell.bg = bg;
|
|
||||||
cell.attrs = attrs;
|
|
||||||
// console.log("Symbol ", j);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_drawAll();
|
this.scheduleDraw()
|
||||||
|
if (this.onload) this.onload()
|
||||||
// if (!cursor.hidden || cursor.hanging || !cursor.suppress) {
|
|
||||||
// // hide cursor asap
|
|
||||||
// _draw(_curCell(), false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (cursorMoved) {
|
|
||||||
cursor.forceOn = true;
|
|
||||||
cursorFlashStartIval = setTimeout(function() {
|
|
||||||
cursor.forceOn = false;
|
|
||||||
}, 1200);
|
|
||||||
_draw(_curCell(), true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply labels to buttons and screen title (leading T removed already) */
|
/** Apply labels to buttons and screen title (leading T removed already) */
|
||||||
function _load_labels(str) {
|
loadLabels (str) {
|
||||||
var pieces = str.split('\x01');
|
let pieces = str.split('\x01')
|
||||||
qs('h1').textContent = pieces[0];
|
qs('h1').textContent = pieces[0]
|
||||||
$('#action-buttons button').forEach(function(x, i) {
|
$('#action-buttons button').forEach((button, i) => {
|
||||||
var s = pieces[i+1].trim();
|
var label = pieces[i + 1].trim()
|
||||||
// if empty string, use the "dim" effect and put nbsp instead to stretch the btn vertically
|
// if empty string, use the "dim" effect and put nbsp instead to
|
||||||
x.innerHTML = s.length > 0 ? e(s) : " ";
|
// stretch the button vertically
|
||||||
x.style.opacity = s.length > 0 ? 1 : 0.2;
|
button.innerHTML = label ? e(label) : ' ';
|
||||||
|
button.style.opacity = label ? 1 : 0.2;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Audible beep for ASCII 7 */
|
load (str) {
|
||||||
function _beep() {
|
const content = str.substr(1)
|
||||||
var osc, gain;
|
|
||||||
if (!audioCtx) return;
|
|
||||||
|
|
||||||
// Main beep
|
switch (str[0]) {
|
||||||
osc = audioCtx.createOscillator();
|
case 'S':
|
||||||
gain = audioCtx.createGain();
|
this.loadContent(content)
|
||||||
osc.connect(gain);
|
break
|
||||||
|
case 'T':
|
||||||
|
this.loadLabels(content)
|
||||||
|
break
|
||||||
|
case 'B':
|
||||||
|
this.beep()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn(`Bad data message type; ignoring.\n${
|
||||||
|
JSON.stringify(content)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beep () {
|
||||||
|
const audioCtx = this.audioCtx
|
||||||
|
if (!audioCtx) return
|
||||||
|
|
||||||
|
let osc, gain
|
||||||
|
|
||||||
|
// main beep
|
||||||
|
osc = audioCtx.createOscillator()
|
||||||
|
gain = audioCtx.createGain()
|
||||||
|
osc.connect(gain)
|
||||||
gain.connect(audioCtx.destination);
|
gain.connect(audioCtx.destination);
|
||||||
gain.gain.value = 0.5;
|
gain.gain.value = 0.5;
|
||||||
osc.frequency.value = 750;
|
osc.frequency.value = 750;
|
||||||
osc.type = 'sine';
|
osc.type = 'sine';
|
||||||
osc.start();
|
osc.start();
|
||||||
osc.stop(audioCtx.currentTime+0.05);
|
osc.stop(audioCtx.currentTime + 0.05);
|
||||||
|
|
||||||
// Surrogate beep (making it sound like 'oops')
|
// surrogate beep (making it sound like 'oops')
|
||||||
osc = audioCtx.createOscillator();
|
osc = audioCtx.createOscillator();
|
||||||
gain = audioCtx.createGain();
|
gain = audioCtx.createGain();
|
||||||
osc.connect(gain);
|
osc.connect(gain);
|
||||||
@@ -348,31 +419,25 @@ var Screen = (function () {
|
|||||||
gain.gain.value = 0.2;
|
gain.gain.value = 0.2;
|
||||||
osc.frequency.value = 400;
|
osc.frequency.value = 400;
|
||||||
osc.type = 'sine';
|
osc.type = 'sine';
|
||||||
osc.start(audioCtx.currentTime+0.05);
|
osc.start(audioCtx.currentTime + 0.05);
|
||||||
osc.stop(audioCtx.currentTime+0.08);
|
osc.stop(audioCtx.currentTime + 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load screen content from a binary sequence (new) */
|
static alphaToFraktur (character) {
|
||||||
function load(str) {
|
if ('a' <= character && character <= 'z') {
|
||||||
//console.log(JSON.stringify(str));
|
character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0))
|
||||||
var content = str.substr(1);
|
} else if ('A' <= character && character <= 'Z') {
|
||||||
switch(str.charAt(0)) {
|
character = frakturExceptions[character] || String.fromCodePoint(
|
||||||
case 'S':
|
0x1d504 - 0x41 + character.charCodeAt(0))
|
||||||
_load_content(content);
|
|
||||||
break;
|
|
||||||
case 'T':
|
|
||||||
_load_labels(content);
|
|
||||||
break;
|
|
||||||
case 'B':
|
|
||||||
_beep();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn("Bad data message type, ignoring.");
|
|
||||||
console.log(str);
|
|
||||||
}
|
}
|
||||||
|
return character
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const Screen = new TermScreen()
|
||||||
load: load, // full load (string)
|
let didAddScreen = false
|
||||||
};
|
Screen.onload = function () {
|
||||||
})();
|
if (didAddScreen) return
|
||||||
|
didAddScreen = true
|
||||||
|
qs('#screen').appendChild(Screen.canvas)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user