|
|
|
const EventEmitter = require('events')
|
|
|
|
const FontCache = require('./font_cache')
|
|
|
|
const { themes, getColor } = require('./themes')
|
|
|
|
const {
|
|
|
|
ATTR_FG,
|
|
|
|
ATTR_BG,
|
|
|
|
ATTR_BOLD,
|
|
|
|
ATTR_UNDERLINE,
|
|
|
|
ATTR_INVERSE,
|
|
|
|
ATTR_BLINK,
|
|
|
|
ATTR_ITALIC,
|
|
|
|
ATTR_STRIKE,
|
|
|
|
ATTR_OVERLINE,
|
|
|
|
ATTR_FAINT,
|
|
|
|
ATTR_FRAKTUR
|
|
|
|
} = require('./screen_attr_bits')
|
|
|
|
|
|
|
|
// Some non-bold Fraktur symbols are outside the contiguous block
|
|
|
|
const frakturExceptions = {
|
|
|
|
'C': '\u212d',
|
|
|
|
'H': '\u210c',
|
|
|
|
'I': '\u2111',
|
|
|
|
'R': '\u211c',
|
|
|
|
'Z': '\u2128'
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = class WebGLRenderer extends EventEmitter {
|
|
|
|
constructor (canvas) {
|
|
|
|
super()
|
|
|
|
|
|
|
|
this.canvas = canvas
|
|
|
|
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')
|
|
|
|
if (!this.gl) throw new Error('No WebGL context')
|
|
|
|
|
|
|
|
this.fontCache = new FontCache(this.gl)
|
|
|
|
|
|
|
|
this._palette = null
|
|
|
|
this.defaultBG = 0
|
|
|
|
this.defaultFG = 7
|
|
|
|
|
|
|
|
this._debug = null
|
|
|
|
|
|
|
|
// screen data, considered immutable
|
|
|
|
this.width = 0
|
|
|
|
this.height = 0
|
|
|
|
this.padding = 0
|
|
|
|
this.charSize = { width: 0, height: 0 }
|
|
|
|
this.cellSize = { width: 0, height: 0 }
|
|
|
|
this.fonts = ['', '', '', ''] // normal, bold, italic, bold-italic
|
|
|
|
this.screen = []
|
|
|
|
this.screenFG = []
|
|
|
|
this.screenBG = []
|
|
|
|
this.screenAttrs = []
|
|
|
|
this.screenSelection = []
|
|
|
|
this.cursor = {}
|
|
|
|
this.reverseVideo = false
|
|
|
|
this.hasBlinkingCells = false
|
|
|
|
this.statusScreen = null
|
|
|
|
this.backgroundImage = null
|
|
|
|
|
|
|
|
this.blinkStyleOn = false
|
|
|
|
this.blinkInterval = null
|
|
|
|
this.cursorBlinkOn = false
|
|
|
|
this.cursorBlinkInterval = null
|
|
|
|
|
|
|
|
this.redrawLoop = false
|
|
|
|
this.resetDrawn(100, 100)
|
|
|
|
this.initTime = Date.now()
|
|
|
|
|
|
|
|
this.init()
|
|
|
|
|
|
|
|
// start loops and timers
|
|
|
|
this.resetBlink()
|
|
|
|
this.resetCursorBlink()
|
|
|
|
this.startDrawLoop()
|
|
|
|
}
|
|
|
|
|
|
|
|
render (reason, data) {
|
|
|
|
if ('hasBlinkingCells' in data && data.hasBlinkingCells !== this.hasBlinkingCells) {
|
|
|
|
if (data.hasBlinkingCells) this.resetBlink()
|
|
|
|
else clearInterval(this.blinkInterval)
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.assign(this, data)
|
|
|
|
this.scheduleDraw(reason)
|
|
|
|
}
|
|
|
|
|
|
|
|
resetDrawn (width, height) {
|
|
|
|
if (this.backgroundImage) {
|
|
|
|
this.gl.clearColor(0, 0, 0, 0)
|
|
|
|
this.canvas.style.backgroundColor = getColor(this.defaultBG, this.palette)
|
|
|
|
} else {
|
|
|
|
this.gl.clearColor(...this.getColor(this.defaultBG))
|
|
|
|
this.canvas.style.backgroundColor = null
|
|
|
|
}
|
|
|
|
if (width && height) {
|
|
|
|
this.gl.viewport(0, 0, width, height)
|
|
|
|
}
|
|
|
|
this.gl.enable(this.gl.BLEND)
|
|
|
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The color palette. Should define 16 colors in an array.
|
|
|
|
* @type {string[]}
|
|
|
|
*/
|
|
|
|
get palette () {
|
|
|
|
return this._palette || themes[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @type {string[]} */
|
|
|
|
set palette (palette) {
|
|
|
|
if (this._palette !== palette) {
|
|
|
|
this._palette = palette
|
|
|
|
this.emit('palette-update', palette)
|
|
|
|
this.scheduleDraw('palette')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getCharWidthFor (font) {
|
|
|
|
this.fontCache.ctx.font = font
|
|
|
|
return Math.floor(this.fontCache.ctx.measureText(' ').width)
|
|
|
|
}
|
|
|
|
|
|
|
|
loadTheme (i) {
|
|
|
|
if (i in themes) this.palette = themes[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
setDefaultColors (fg, bg) {
|
|
|
|
if (fg !== this.defaultFG || bg !== this.defaultBG) {
|
|
|
|
this.defaultFG = fg
|
|
|
|
this.defaultBG = bg
|
|
|
|
this.scheduleDraw('default-colors')
|
|
|
|
|
|
|
|
// full bg with default color (goes behind the image)
|
|
|
|
this.canvas.style.backgroundColor = this.getColor(bg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedule a draw in the next millisecond
|
|
|
|
* @param {string} why - the reason why the draw occured (for debugging)
|
|
|
|
* @param {number} [aggregateTime] - time to wait for more scheduleDraw calls
|
|
|
|
* to occur. 1 ms by default.
|
|
|
|
*/
|
|
|
|
scheduleDraw (why, aggregateTime = 1) {
|
|
|
|
clearTimeout(this._scheduledDraw)
|
|
|
|
this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the specified color. If `i` is in the palette, it will return the
|
|
|
|
* palette color. If `i` is between 16 and 255, it will return the 256color
|
|
|
|
* value. If `i` is larger than 255, it will return an RGB color value. If `i`
|
|
|
|
* is -1 (foreground) or -2 (background), it will return the selection colors.
|
|
|
|
* @param {number} i - the color
|
|
|
|
* @returns {string} the CSS color
|
|
|
|
*/
|
|
|
|
getColor (i) {
|
|
|
|
return WebGLRenderer.colorToRGBA(getColor(i, this.palette))
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resets the cursor blink to on and restarts the timer
|
|
|
|
*/
|
|
|
|
resetCursorBlink () {
|
|
|
|
this.cursorBlinkOn = true
|
|
|
|
clearInterval(this.cursorBlinkInterval)
|
|
|
|
this.cursorBlinkInterval = setInterval(() => {
|
|
|
|
this.cursorBlinkOn = this.cursor.blinking ? !this.cursorBlinkOn : true
|
|
|
|
if (this.cursor.blinking) this.scheduleDraw('cursor-blink')
|
|
|
|
}, 500)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resets the blink style to on and restarts the timer
|
|
|
|
*/
|
|
|
|
resetBlink () {
|
|
|
|
this.blinkStyleOn = true
|
|
|
|
clearInterval(this.blinkInterval)
|
|
|
|
let intervals = 0
|
|
|
|
this.blinkInterval = setInterval(() => {
|
|
|
|
if (this.blinkingCellCount <= 0) return
|
|
|
|
|
|
|
|
intervals++
|
|
|
|
if (intervals >= 4 && this.blinkStyleOn) {
|
|
|
|
this.blinkStyleOn = false
|
|
|
|
intervals = 0
|
|
|
|
this.scheduleDraw('blink-style')
|
|
|
|
} else if (intervals >= 1 && !this.blinkStyleOn) {
|
|
|
|
this.blinkStyleOn = true
|
|
|
|
intervals = 0
|
|
|
|
this.scheduleDraw('blink-style')
|
|
|
|
}
|
|
|
|
}, 200)
|
|
|
|
}
|
|
|
|
|
|
|
|
compileShader (vertex, fragment) {
|
|
|
|
const { gl } = this
|
|
|
|
let vert = gl.createShader(gl.VERTEX_SHADER)
|
|
|
|
gl.shaderSource(vert, vertex)
|
|
|
|
gl.compileShader(vert)
|
|
|
|
let frag = gl.createShader(gl.FRAGMENT_SHADER)
|
|
|
|
gl.shaderSource(frag, fragment)
|
|
|
|
gl.compileShader(frag)
|
|
|
|
if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS) || !gl.getShaderParameter(frag, gl.COMPILE_STATUS)) {
|
|
|
|
console.error(gl.getShaderInfoLog(vert), gl.getShaderInfoLog(frag))
|
|
|
|
gl.deleteShader(vert)
|
|
|
|
gl.deleteShader(frag)
|
|
|
|
throw new Error('Shader compile error')
|
|
|
|
}
|
|
|
|
|
|
|
|
let shader = gl.createProgram()
|
|
|
|
gl.attachShader(shader, vert)
|
|
|
|
gl.attachShader(shader, frag)
|
|
|
|
gl.linkProgram(shader)
|
|
|
|
|
|
|
|
if (!gl.getProgramParameter(shader, gl.LINK_STATUS)) {
|
|
|
|
console.error(gl.getProgramInfoLog(shader))
|
|
|
|
throw new Error('Shader link error')
|
|
|
|
}
|
|
|
|
|
|
|
|
return shader
|
|
|
|
}
|
|
|
|
|
|
|
|
init () {
|
|
|
|
const { gl } = this
|
|
|
|
|
|
|
|
let bgShader = this.compileShader(`
|
|
|
|
precision mediump float;
|
|
|
|
attribute vec2 position;
|
|
|
|
uniform mat4 projection;
|
|
|
|
uniform vec2 char_pos;
|
|
|
|
uniform vec2 extend;
|
|
|
|
void main() {
|
|
|
|
vec2 scale = vec2(1.0 + abs(extend.x), 1.0 + abs(extend.y));
|
|
|
|
vec2 offset = min(vec2(0.0, 0.0), extend);
|
|
|
|
gl_Position = projection * vec4(char_pos + offset + scale * position, 0.0, 1.0);
|
|
|
|
}
|
|
|
|
`, `
|
|
|
|
precision highp float;
|
|
|
|
uniform vec4 color;
|
|
|
|
void main() {
|
|
|
|
gl_FragColor = color;
|
|
|
|
}
|
|
|
|
`)
|
|
|
|
|
|
|
|
let charShader = this.compileShader(`
|
|
|
|
precision mediump float;
|
|
|
|
attribute vec2 position;
|
|
|
|
uniform mat4 projection;
|
|
|
|
uniform vec2 char_pos;
|
|
|
|
uniform bool clip;
|
|
|
|
varying highp vec2 tex_coord;
|
|
|
|
varying vec4 screen_pos;
|
|
|
|
void main() {
|
|
|
|
if (clip) {
|
|
|
|
gl_Position = projection * vec4(char_pos + position, 0.0, 1.0);
|
|
|
|
screen_pos = vec4(position, 0.0, 1.0);
|
|
|
|
tex_coord = position / 3.0 + vec2(1.0 / 3.0, 1.0 / 3.0);
|
|
|
|
} else {
|
|
|
|
gl_Position = projection * vec4(char_pos - vec2(1.0, 1.0) + 3.0 * position, 0.0, 1.0);
|
|
|
|
screen_pos = vec4(3.0 * position - vec2(1.0, 1.0), 0.0, 1.0);
|
|
|
|
tex_coord = position;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`, `
|
|
|
|
precision highp float;
|
|
|
|
uniform vec4 color;
|
|
|
|
uniform sampler2D texture;
|
|
|
|
uniform bool faint;
|
|
|
|
uniform bool overline;
|
|
|
|
uniform bool strike;
|
|
|
|
uniform bool underline;
|
|
|
|
varying highp vec2 tex_coord;
|
|
|
|
varying vec4 screen_pos;
|
|
|
|
void main() {
|
|
|
|
gl_FragColor = texture2D(texture, tex_coord) * color;
|
|
|
|
if (screen_pos.x >= 0.0 && screen_pos.x <= 1.0) {
|
|
|
|
if (faint) {
|
|
|
|
gl_FragColor.a /= 2.0;
|
|
|
|
}
|
|
|
|
if (overline) {
|
|
|
|
if (screen_pos.y >= 0.0 && screen_pos.y <= 0.05) gl_FragColor = color;
|
|
|
|
}
|
|
|
|
if (strike) {
|
|
|
|
if (screen_pos.y >= 0.475 && screen_pos.y <= 0.525) gl_FragColor = color;
|
|
|
|
}
|
|
|
|
if (underline) {
|
|
|
|
if (screen_pos.y >= 0.95 && screen_pos.y <= 1.0) gl_FragColor = color;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`)
|
|
|
|
|
|
|
|
let fboShader = this.compileShader(`
|
|
|
|
precision mediump float;
|
|
|
|
attribute vec2 position;
|
|
|
|
uniform mat4 projection;
|
|
|
|
varying highp vec2 tex_coord;
|
|
|
|
void main() {
|
|
|
|
gl_Position = projection * vec4(position, 0.0, 1.0);
|
|
|
|
tex_coord = position;
|
|
|
|
}
|
|
|
|
`, `
|
|
|
|
precision highp float;
|
|
|
|
uniform sampler2D texture;
|
|
|
|
uniform vec2 pixel_scale;
|
|
|
|
uniform float time;
|
|
|
|
varying highp vec2 tex_coord;
|
|
|
|
void main() {
|
|
|
|
gl_FragColor = texture2D(texture, tex_coord);
|
|
|
|
/* uncomment for CRT-ish effect
|
|
|
|
vec4 sum = vec4(0);
|
|
|
|
|
|
|
|
for (int i = -4; i <= 4; i++) {
|
|
|
|
for (int j = -4; j <= 4; j++) {
|
|
|
|
sum += texture2D(texture, tex_coord + vec2(j, i) * pixel_scale) * 0.07;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
float factor = 0.05 + 0.02 * sin(time * 5.0);
|
|
|
|
if (mod(tex_coord.y / pixel_scale.y, 10.0) < 5.0) {
|
|
|
|
factor += 0.1;
|
|
|
|
}
|
|
|
|
float beam_y = (mod(-time, 9.0) - 1.5) / 6.0;
|
|
|
|
if (abs(tex_coord.y - beam_y) < 0.05) {
|
|
|
|
factor += 0.2 * cos((tex_coord.y - beam_y) / 0.05 * 1.57);
|
|
|
|
}
|
|
|
|
gl_FragColor = sum * sum * factor + texture2D(texture, tex_coord); */
|
|
|
|
}
|
|
|
|
`)
|
|
|
|
|
|
|
|
this.bgShader = {
|
|
|
|
shader: bgShader,
|
|
|
|
attributes: {
|
|
|
|
position: gl.getAttribLocation(bgShader, 'position')
|
|
|
|
},
|
|
|
|
uniforms: {
|
|
|
|
projection: gl.getUniformLocation(bgShader, 'projection'),
|
|
|
|
charPos: gl.getUniformLocation(bgShader, 'char_pos'),
|
|
|
|
extend: gl.getUniformLocation(bgShader, 'extend'),
|
|
|
|
color: gl.getUniformLocation(bgShader, 'color')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.charShader = {
|
|
|
|
shader: charShader,
|
|
|
|
attributes: {
|
|
|
|
position: gl.getAttribLocation(charShader, 'position')
|
|
|
|
},
|
|
|
|
uniforms: {
|
|
|
|
projection: gl.getUniformLocation(charShader, 'projection'),
|
|
|
|
charPos: gl.getUniformLocation(charShader, 'char_pos'),
|
|
|
|
color: gl.getUniformLocation(charShader, 'color'),
|
|
|
|
texture: gl.getUniformLocation(charShader, 'texture'),
|
|
|
|
clip: gl.getUniformLocation(charShader, 'clip'),
|
|
|
|
faint: gl.getUniformLocation(charShader, 'faint'),
|
|
|
|
overline: gl.getUniformLocation(charShader, 'overline'),
|
|
|
|
strike: gl.getUniformLocation(charShader, 'strike'),
|
|
|
|
underline: gl.getUniformLocation(charShader, 'underline')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.fboShader = {
|
|
|
|
shader: fboShader,
|
|
|
|
attributes: {
|
|
|
|
position: gl.getAttribLocation(fboShader, 'position')
|
|
|
|
},
|
|
|
|
uniforms: {
|
|
|
|
projection: gl.getUniformLocation(fboShader, 'projection'),
|
|
|
|
pixelScale: gl.getUniformLocation(fboShader, 'pixel_scale'),
|
|
|
|
time: gl.getUniformLocation(fboShader, 'time'),
|
|
|
|
texture: gl.getUniformLocation(fboShader, 'texture')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let buffer = gl.createBuffer()
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
|
|
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
|
|
1, 1,
|
|
|
|
0, 1,
|
|
|
|
1, 0,
|
|
|
|
0, 0
|
|
|
|
]), gl.STATIC_DRAW)
|
|
|
|
this.squareBuffer = buffer
|
|
|
|
this.useShader = (shader, projection) => {
|
|
|
|
gl.vertexAttribPointer(shader.attributes.position, 2, gl.FLOAT, false, 0, 0)
|
|
|
|
gl.enableVertexAttribArray(shader.attributes.position)
|
|
|
|
gl.useProgram(shader.shader)
|
|
|
|
|
|
|
|
gl.uniformMatrix4fv(shader.uniforms.projection, false, projection)
|
|
|
|
}
|
|
|
|
this.drawSquare = () => {
|
|
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
|
|
|
}
|
|
|
|
|
|
|
|
// frame buffers
|
|
|
|
|
|
|
|
let maxBuffers = gl.getParameter(gl.getExtension('WEBGL_draw_buffers').MAX_COLOR_ATTACHMENTS_WEBGL)
|
|
|
|
let createBuffer = i => {
|
|
|
|
let buffer = gl.createFramebuffer()
|
|
|
|
let texture = gl.createTexture()
|
|
|
|
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, buffer)
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, texture)
|
|
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
|
|
|
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, texture, 0)
|
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
|
|
|
|
|
|
|
return { buffer, texture }
|
|
|
|
}
|
|
|
|
|
|
|
|
if (maxBuffers >= 2) {
|
|
|
|
this.buffers = {
|
|
|
|
drawing: createBuffer(0),
|
|
|
|
display: createBuffer(1)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let buffer = createBuffer(0)
|
|
|
|
this.buffers = { drawing: buffer, display: buffer }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
draw (reason) {
|
|
|
|
const { gl, width, height, padding, devicePixelRatio, statusScreen } = this
|
|
|
|
let { screen, screenFG, screenBG, screenAttrs } = this
|
|
|
|
|
|
|
|
// ;[this.buffers.drawing, this.buffers.display] = [this.buffers.display, this.buffers.drawing]
|
|
|
|
|
|
|
|
let drawingBuffer = this.buffers.drawing
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, drawingBuffer.buffer)
|
|
|
|
gl.activeTexture(gl.TEXTURE0)
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, drawingBuffer.texture)
|
|
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
|
|
|
|
|
|
|
|
if (statusScreen) {
|
|
|
|
this.redrawLoop = true
|
|
|
|
|
|
|
|
screen = new Array(width * height).fill(' ')
|
|
|
|
screenFG = new Array(width * height).fill(this.defaultFG)
|
|
|
|
screenBG = new Array(width * height).fill(this.defaultBG)
|
|
|
|
screenAttrs = new Array(width * height).fill(ATTR_FG | ATTR_BG)
|
|
|
|
|
|
|
|
let text = statusScreen.title
|
|
|
|
for (let i = 0; i < Math.min(width * height, text.length); i++) {
|
|
|
|
screen[i] = text[i]
|
|
|
|
}
|
|
|
|
if (statusScreen.loading) {
|
|
|
|
let t = Date.now() / 1000
|
|
|
|
|
|
|
|
for (let i = width; i < Math.min(width * height, width + 8); i++) {
|
|
|
|
let offset = ((t * 12) - i) % 12
|
|
|
|
let value = Math.max(0.2, 1 - offset / 3) * 255
|
|
|
|
screenFG[i] = 256 + value + (value << 8) + (value << 16)
|
|
|
|
screen[i] = '*'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else this.redrawLoop = false
|
|
|
|
|
|
|
|
if (this.debug && this._debug) this._debug.drawStart(reason)
|
|
|
|
|
|
|
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
|
|
|
this.fontCache.cellSize = this.cellSize
|
|
|
|
this.fontCache.dp = devicePixelRatio
|
|
|
|
|
|
|
|
let paddingX = padding / this.cellSize.width
|
|
|
|
let paddingY = padding / this.cellSize.height
|
|
|
|
|
|
|
|
let projection = new Float32Array([
|
|
|
|
2 / (width + 2 * paddingX), 0, 0, 0,
|
|
|
|
0, -2 / (height + 2 * paddingY), 0, 0,
|
|
|
|
0, 0, 1, 0,
|
|
|
|
-1 + 2 * paddingX / width, 1 - 2 * paddingY / height, 0, 1
|
|
|
|
])
|
|
|
|
|
|
|
|
// draw background
|
|
|
|
this.useShader(this.bgShader, projection)
|
|
|
|
|
|
|
|
let textCells = {}
|
|
|
|
|
|
|
|
for (let cell = 0; cell < width * height; cell++) {
|
|
|
|
let x = cell % width
|
|
|
|
let y = Math.floor(cell / width)
|
|
|
|
let isCursor = this.cursorBlinkOn &&
|
|
|
|
this.cursor.x === x &&
|
|
|
|
this.cursor.y === y &&
|
|
|
|
this.cursor.visible
|
|
|
|
|
|
|
|
let text = screen[cell]
|
|
|
|
let fg = screenFG[cell] | 0
|
|
|
|
let bg = screenBG[cell] | 0
|
|
|
|
let attrs = screenAttrs[cell] | 0
|
|
|
|
let inSelection = this.screenSelection[cell]
|
|
|
|
|
|
|
|
if (!(cell in screen)) continue
|
|
|
|
if (statusScreen) isCursor = false
|
|
|
|
|
|
|
|
let isDefaultBG = false
|
|
|
|
|
|
|
|
if (!(attrs & ATTR_FG)) fg = this.defaultFG
|
|
|
|
if (!(attrs & ATTR_BG)) {
|
|
|
|
bg = this.defaultBG
|
|
|
|
isDefaultBG = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attrs & ATTR_INVERSE) [fg, bg] = [bg, fg] // swap - reversed character colors
|
|
|
|
if (this.reverseVideo) [fg, bg] = [bg, fg] // swap - reversed all screen
|
|
|
|
|
|
|
|
if (attrs & ATTR_BLINK && !this.blinkStyleOn) {
|
|
|
|
// blinking is enabled and blink style is off
|
|
|
|
// set text to nothing so drawCharacter only draws decoration
|
|
|
|
text = ' '
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inSelection) {
|
|
|
|
fg = -1
|
|
|
|
bg = -2
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.backgroundImage || !isDefaultBG) {
|
|
|
|
gl.uniform2f(this.bgShader.uniforms.charPos, x, y)
|
|
|
|
gl.uniform4f(this.bgShader.uniforms.color, ...this.getColor(bg))
|
|
|
|
|
|
|
|
let extendX = 0
|
|
|
|
let extendY = 0
|
|
|
|
|
|
|
|
if (x === 0) extendX = -1
|
|
|
|
if (x === width - 1) extendX = 1
|
|
|
|
if (y === 0) extendY = -1
|
|
|
|
if (y === height - 1) extendY = 1
|
|
|
|
|
|
|
|
gl.uniform2f(this.bgShader.uniforms.extend, extendX, extendY)
|
|
|
|
|
|
|
|
this.drawSquare()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (text.trim() || isCursor || attrs) {
|
|
|
|
let fontIndex = 0
|
|
|
|
if (attrs & ATTR_BOLD) fontIndex |= 1
|
|
|
|
if (attrs & ATTR_ITALIC) fontIndex |= 2
|
|
|
|
let font = this.fonts[fontIndex]
|
|
|
|
if (attrs & ATTR_FRAKTUR) text = WebGLRenderer.alphaToFraktur(text)
|
|
|
|
let type = font + text
|
|
|
|
|
|
|
|
if (!textCells[type]) textCells[type] = []
|
|
|
|
textCells[type].push({ x, y, text, font, fg, bg, attrs, isCursor })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.useShader(this.charShader, projection)
|
|
|
|
gl.activeTexture(gl.TEXTURE1)
|
|
|
|
|
|
|
|
for (let key in textCells) {
|
|
|
|
let { font, text } = textCells[key][0]
|
|
|
|
let texture = this.fontCache.getChar(font, text)
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, texture)
|
|
|
|
gl.uniform1i(this.charShader.uniforms.texture, 1)
|
|
|
|
|
|
|
|
for (let cell of textCells[key]) {
|
|
|
|
let { x, y, fg, bg, attrs, isCursor } = cell
|
|
|
|
|
|
|
|
gl.uniform2f(this.charShader.uniforms.charPos, x, y)
|
|
|
|
gl.uniform4f(this.charShader.uniforms.color, ...this.getColor(fg))
|
|
|
|
|
|
|
|
gl.uniform1i(this.charShader.uniforms.faint, (attrs & ATTR_FAINT) > 0)
|
|
|
|
gl.uniform1i(this.charShader.uniforms.overline, (attrs & ATTR_OVERLINE) > 0)
|
|
|
|
gl.uniform1i(this.charShader.uniforms.strike, (attrs & ATTR_STRIKE) > 0)
|
|
|
|
gl.uniform1i(this.charShader.uniforms.underline, (attrs & ATTR_UNDERLINE) > 0)
|
|
|
|
|
|
|
|
this.drawSquare()
|
|
|
|
|
|
|
|
if (isCursor) {
|
|
|
|
if (fg === bg) {
|
|
|
|
fg = 7
|
|
|
|
bg = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
this.useShader(this.bgShader, projection)
|
|
|
|
gl.uniform2f(this.bgShader.uniforms.extend, 0, 0)
|
|
|
|
gl.uniform2f(this.bgShader.uniforms.charPos, x, y)
|
|
|
|
gl.uniform4f(this.bgShader.uniforms.color, ...this.getColor(fg))
|
|
|
|
this.drawSquare()
|
|
|
|
|
|
|
|
this.useShader(this.charShader, projection)
|
|
|
|
gl.uniform4f(this.charShader.uniforms.color, ...this.getColor(bg))
|
|
|
|
gl.uniform1i(this.charShader.uniforms.clip, true)
|
|
|
|
this.drawSquare()
|
|
|
|
gl.uniform1i(this.charShader.uniforms.clip, false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
|
|
|
|
this.drawFrame()
|
|
|
|
|
|
|
|
if (this.debug && this._debug) this._debug.drawEnd()
|
|
|
|
}
|
|
|
|
|
|
|
|
drawFrame () {
|
|
|
|
const { gl } = this
|
|
|
|
let drawingBuffer = this.buffers.drawing
|
|
|
|
|
|
|
|
gl.clear(gl.COLOR_BUFFER_BIT)
|
|
|
|
gl.activeTexture(gl.TEXTURE0)
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, drawingBuffer.texture)
|
|
|
|
this.useShader(this.fboShader, [
|
|
|
|
2, 0, 0, 0,
|
|
|
|
0, 2, 0, 0,
|
|
|
|
0, 0, 1, 0,
|
|
|
|
-1, -1, 0, 1
|
|
|
|
])
|
|
|
|
gl.uniform2f(this.fboShader.uniforms.pixelScale, 1 / gl.drawingBufferWidth, 1 / gl.drawingBufferHeight)
|
|
|
|
gl.uniform1i(this.fboShader.uniforms.texture, 0)
|
|
|
|
gl.uniform1f(this.fboShader.uniforms.time, ((Date.now() - this.initTime) / 1000) % 86400)
|
|
|
|
this.drawSquare()
|
|
|
|
}
|
|
|
|
|
|
|
|
startDrawLoop () {
|
|
|
|
if (this._drawTimerThread) return
|
|
|
|
let threadID = Math.random().toString(36)
|
|
|
|
this._drawTimerThread = threadID
|
|
|
|
this.drawTimerLoop(threadID)
|
|
|
|
}
|
|
|
|
|
|
|
|
stopDrawLoop () {
|
|
|
|
this._drawTimerThread = null
|
|
|
|
}
|
|
|
|
|
|
|
|
drawTimerLoop (threadID) {
|
|
|
|
if (!threadID || threadID !== this._drawTimerThread) return
|
|
|
|
window.requestAnimationFrame(() => this.drawTimerLoop(threadID))
|
|
|
|
if (this.redrawLoop) this.draw('draw-loop')
|
|
|
|
// uncomment for an update every frame (GPU-intensive)
|
|
|
|
// (also, lots of errors. TODO: investigate)
|
|
|
|
// this.drawFrame()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts an alphabetic character to its fraktur variant.
|
|
|
|
* @param {string} character - the character
|
|
|
|
* @returns {string} the converted character
|
|
|
|
*/
|
|
|
|
static alphaToFraktur (character) {
|
|
|
|
if (character >= 'a' && character <= 'z') {
|
|
|
|
character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0))
|
|
|
|
} else if (character >= 'A' && character <= 'Z') {
|
|
|
|
character = frakturExceptions[character] || String.fromCodePoint(0x1d504 - 0x41 + character.charCodeAt(0))
|
|
|
|
}
|
|
|
|
return character
|
|
|
|
}
|
|
|
|
|
|
|
|
static colorToRGBA (color) {
|
|
|
|
color = color.substr(1)
|
|
|
|
if (color.length === 3) {
|
|
|
|
return [
|
|
|
|
parseInt(color[0], 16) * 0x11 / 0xff,
|
|
|
|
parseInt(color[1], 16) * 0x11 / 0xff,
|
|
|
|
parseInt(color[2], 16) * 0x11 / 0xff,
|
|
|
|
1
|
|
|
|
]
|
|
|
|
} else {
|
|
|
|
return [
|
|
|
|
parseInt(color.substr(0, 2), 16) / 0xff,
|
|
|
|
parseInt(color.substr(2, 2), 16) / 0xff,
|
|
|
|
parseInt(color.substr(4, 2), 16) / 0xff,
|
|
|
|
1
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|