Compare commits
24 Commits
webgl-rend
...
master
Author | SHA1 | Date |
---|---|---|
Ondřej Hruška | f729d91651 | 6 years ago |
Ondřej Hruška | ae38110816 | 6 years ago |
Ondřej Hruška | 2e014f4312 | 6 years ago |
Ondřej Hruška | ef05565155 | 6 years ago |
cpsdqs | 86abce86aa | 6 years ago |
Ondřej Hruška | e25523de11 | 7 years ago |
Ondřej Hruška | 1a05b70b6c | 7 years ago |
schneemaier | c55ceb1298 | 7 years ago |
Ondřej Hruška | 8b43b1d171 | 7 years ago |
Ondřej Hruška | 36f0f60dbd | 7 years ago |
Ondřej Hruška | a0412c0e9e | 7 years ago |
Ondřej Hruška | fed47ffda9 | 7 years ago |
Ondřej Hruška | 68a467c33e | 7 years ago |
cpsdqs | 681685fa3b | 7 years ago |
cpsdqs | 915113a628 | 7 years ago |
Ondřej Hruška | 0893b0a268 | 7 years ago |
Ondřej Hruška | d489e99194 | 7 years ago |
cpsdqs | f61d861883 | 7 years ago |
cpsdqs | 4e9970300b | 7 years ago |
cpsdqs | 45766fccce | 7 years ago |
cpsdqs | 244ee72531 | 7 years ago |
cpsdqs | f7f5658685 | 7 years ago |
cpsdqs | f590741d97 | 7 years ago |
Ondřej Hruška | 9cab25ed4d | 7 years ago |
@ -1,201 +0,0 @@ |
|||||||
module.exports = class GLFontCache { |
|
||||||
constructor (gl) { |
|
||||||
this.gl = gl |
|
||||||
|
|
||||||
// cache: string => WebGLTexture
|
|
||||||
this.cache = new Map() |
|
||||||
this.dp = 1 |
|
||||||
this.cellSize = { width: 0, height: 0 } |
|
||||||
|
|
||||||
this.canvas = document.createElement('canvas') |
|
||||||
this.ctx = this.canvas.getContext('2d') |
|
||||||
} |
|
||||||
|
|
||||||
clearCache () { |
|
||||||
for (let texture of this.cache.values()) this.gl.deleteTexture(texture) |
|
||||||
this.cache = new Map() |
|
||||||
} |
|
||||||
|
|
||||||
getChar (font, character) { |
|
||||||
let name = `${font}@${this.dp}x:${character}` |
|
||||||
|
|
||||||
if (!this.cache.has(name)) { |
|
||||||
this.cache.set(name, this.render(font, character)) |
|
||||||
} |
|
||||||
return this.cache.get(name) |
|
||||||
} |
|
||||||
|
|
||||||
render (font, character) { |
|
||||||
const { gl, ctx, dp, cellSize } = this |
|
||||||
|
|
||||||
let width = dp * cellSize.width * 3 |
|
||||||
let height = dp * cellSize.height * 3 |
|
||||||
if (this.canvas.width !== width) this.canvas.width = width |
|
||||||
if (this.canvas.height !== height) this.canvas.height = height |
|
||||||
|
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0) |
|
||||||
ctx.clearRect(0, 0, width, height) |
|
||||||
ctx.scale(dp, dp) |
|
||||||
|
|
||||||
if (ctx.font !== font) ctx.font = font |
|
||||||
|
|
||||||
ctx.textAlign = 'center' |
|
||||||
ctx.textBaseline = 'middle' |
|
||||||
ctx.fillStyle = 'white' |
|
||||||
this.drawCharacter(character) |
|
||||||
|
|
||||||
let imageData = ctx.getImageData(0, 0, width, height) |
|
||||||
|
|
||||||
let texture = gl.createTexture() |
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture) |
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageData) |
|
||||||
|
|
||||||
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 texture |
|
||||||
} |
|
||||||
|
|
||||||
drawCharacter (character) { |
|
||||||
const { ctx, cellSize } = this |
|
||||||
|
|
||||||
let screenX = cellSize.width |
|
||||||
let screenY = cellSize.height |
|
||||||
|
|
||||||
let codePoint = character.codePointAt(0) |
|
||||||
if (codePoint >= 0x2580 && codePoint <= 0x259F) { |
|
||||||
// block elements
|
|
||||||
ctx.beginPath() |
|
||||||
const left = screenX |
|
||||||
const top = screenY |
|
||||||
const cw = cellSize.width |
|
||||||
const ch = cellSize.height |
|
||||||
const c2w = cellSize.width / 2 |
|
||||||
const c2h = cellSize.height / 2 |
|
||||||
|
|
||||||
// http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm
|
|
||||||
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
|
|
||||||
// 0x2580 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏
|
|
||||||
// 0x2590 ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟
|
|
||||||
|
|
||||||
if (codePoint === 0x2580) { |
|
||||||
// upper half block >▀<
|
|
||||||
ctx.rect(left, top, cw, c2h) |
|
||||||
} else if (codePoint <= 0x2588) { |
|
||||||
// lower n eighth block (increasing) >▁< to >█<
|
|
||||||
let offset = (1 - (codePoint - 0x2580) / 8) * ch |
|
||||||
ctx.rect(left, top + offset, cw, ch - offset) |
|
||||||
} else if (codePoint <= 0x258F) { |
|
||||||
// left n eighth block (decreasing) >▉< to >▏<
|
|
||||||
let offset = (codePoint - 0x2588) / 8 * cw |
|
||||||
ctx.rect(left, top, cw - offset, ch) |
|
||||||
} else if (codePoint === 0x2590) { |
|
||||||
// right half block >▐<
|
|
||||||
ctx.rect(left + c2w, top, c2w, ch) |
|
||||||
} else if (codePoint <= 0x2593) { |
|
||||||
// shading >░< >▒< >▓<
|
|
||||||
|
|
||||||
// dot spacing by dividing cell size by a constant. This could be
|
|
||||||
// reworked to always return a whole number, but that would require
|
|
||||||
// prime factorization, and doing that without a loop would let you
|
|
||||||
// take over the world, which is not within the scope of this project.
|
|
||||||
let dotSpacingX, dotSpacingY, dotSize |
|
||||||
if (codePoint === 0x2591) { |
|
||||||
dotSpacingX = cw / 4 |
|
||||||
dotSpacingY = ch / 10 |
|
||||||
dotSize = 1 |
|
||||||
} else if (codePoint === 0x2592) { |
|
||||||
dotSpacingX = cw / 6 |
|
||||||
dotSpacingY = cw / 10 |
|
||||||
dotSize = 1 |
|
||||||
} else if (codePoint === 0x2593) { |
|
||||||
dotSpacingX = cw / 4 |
|
||||||
dotSpacingY = cw / 7 |
|
||||||
dotSize = 2 |
|
||||||
} |
|
||||||
|
|
||||||
let alignRight = false |
|
||||||
for (let dy = 0; dy < ch; dy += dotSpacingY) { |
|
||||||
for (let dx = 0; dx < cw; dx += dotSpacingX) { |
|
||||||
// prevent overflow
|
|
||||||
let dotSizeY = Math.min(dotSize, ch - dy) |
|
||||||
ctx.rect(left + (alignRight ? cw - dx - dotSize : dx), top + dy, dotSize, dotSizeY) |
|
||||||
} |
|
||||||
alignRight = !alignRight |
|
||||||
} |
|
||||||
} else if (codePoint === 0x2594) { |
|
||||||
// upper one eighth block >▔<
|
|
||||||
ctx.rect(left, top, cw, ch / 8) |
|
||||||
} else if (codePoint === 0x2595) { |
|
||||||
// right one eighth block >▕<
|
|
||||||
ctx.rect(left + (7 / 8) * cw, top, cw / 8, ch) |
|
||||||
} else if (codePoint === 0x2596) { |
|
||||||
// left bottom quadrant >▖<
|
|
||||||
ctx.rect(left, top + c2h, c2w, c2h) |
|
||||||
} else if (codePoint === 0x2597) { |
|
||||||
// right bottom quadrant >▗<
|
|
||||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
|
||||||
} else if (codePoint === 0x2598) { |
|
||||||
// left top quadrant >▘<
|
|
||||||
ctx.rect(left, top, c2w, c2h) |
|
||||||
} else if (codePoint === 0x2599) { |
|
||||||
// left chair >▙<
|
|
||||||
ctx.rect(left, top, c2w, ch) |
|
||||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
|
||||||
} else if (codePoint === 0x259A) { |
|
||||||
// quadrants lt rb >▚<
|
|
||||||
ctx.rect(left, top, c2w, c2h) |
|
||||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
|
||||||
} else if (codePoint === 0x259B) { |
|
||||||
// left chair upside down >▛<
|
|
||||||
ctx.rect(left, top, c2w, ch) |
|
||||||
ctx.rect(left + c2w, top, c2w, c2h) |
|
||||||
} else if (codePoint === 0x259C) { |
|
||||||
// right chair upside down >▜<
|
|
||||||
ctx.rect(left, top, cw, c2h) |
|
||||||
ctx.rect(left + c2w, top + c2h, c2w, c2h) |
|
||||||
} else if (codePoint === 0x259D) { |
|
||||||
// right top quadrant >▝<
|
|
||||||
ctx.rect(left + c2w, top, c2w, c2h) |
|
||||||
} else if (codePoint === 0x259E) { |
|
||||||
// quadrants lb rt >▞<
|
|
||||||
ctx.rect(left, top + c2h, c2w, c2h) |
|
||||||
ctx.rect(left + c2w, top, c2w, c2h) |
|
||||||
} else if (codePoint === 0x259F) { |
|
||||||
// right chair upside down >▟<
|
|
||||||
ctx.rect(left, top + c2h, c2w, c2h) |
|
||||||
ctx.rect(left + c2w, top, c2w, ch) |
|
||||||
} |
|
||||||
|
|
||||||
ctx.fill() |
|
||||||
} else if (codePoint >= 0xE0B0 && codePoint <= 0xE0B3) { |
|
||||||
// powerline symbols, except branch, line, and lock. Basically, just the triangles
|
|
||||||
ctx.beginPath() |
|
||||||
|
|
||||||
if (codePoint === 0xE0B0 || codePoint === 0xE0B1) { |
|
||||||
// right-pointing triangle
|
|
||||||
ctx.moveTo(screenX, screenY) |
|
||||||
ctx.lineTo(screenX + cellSize.width, screenY + cellSize.height / 2) |
|
||||||
ctx.lineTo(screenX, screenY + cellSize.height) |
|
||||||
} else if (codePoint === 0xE0B2 || codePoint === 0xE0B3) { |
|
||||||
// left-pointing triangle
|
|
||||||
ctx.moveTo(screenX + cellSize.width, screenY) |
|
||||||
ctx.lineTo(screenX, screenY + cellSize.height / 2) |
|
||||||
ctx.lineTo(screenX + cellSize.width, screenY + cellSize.height) |
|
||||||
} |
|
||||||
|
|
||||||
if (codePoint % 2 === 0) { |
|
||||||
// triangle
|
|
||||||
ctx.fill() |
|
||||||
} else { |
|
||||||
// chevron
|
|
||||||
ctx.strokeStyle = ctx.fillStyle |
|
||||||
ctx.stroke() |
|
||||||
} |
|
||||||
} else { |
|
||||||
// Draw other characters using the text renderer
|
|
||||||
ctx.fillText(character, cellSize.width * 1.5, cellSize.height * 1.5) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,727 +0,0 @@ |
|||||||
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; |
|
||||||
float hue_to_rgb (float p, float q, float t) { |
|
||||||
if (t < 0.0) t += 1.0; |
|
||||||
if (t > 1.0) t -= 1.0; |
|
||||||
if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t; |
|
||||||
if (t < 0.5) return q; |
|
||||||
if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0; |
|
||||||
return p; |
|
||||||
} |
|
||||||
vec4 hsl_to_rgb (vec4 hsl) { |
|
||||||
vec4 rgb = vec4(0); |
|
||||||
rgb.a = hsl.a; |
|
||||||
hsl.x = mod(hsl.x, 1.0); |
|
||||||
if (hsl.y == 0.0) { |
|
||||||
rgb.r = hsl.z; |
|
||||||
rgb.g = hsl.z; |
|
||||||
rgb.b = hsl.z; |
|
||||||
} else { |
|
||||||
float q = hsl.z < 0.5 ? hsl.z * (1.0 + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y; |
|
||||||
float p = 2.0 * hsl.z - q; |
|
||||||
rgb.r = hue_to_rgb(p, q, hsl.x + 1.0 / 3.0); |
|
||||||
rgb.g = hue_to_rgb(p, q, hsl.x); |
|
||||||
rgb.b = hue_to_rgb(p, q, hsl.x - 1.0 / 3.0); |
|
||||||
} |
|
||||||
return rgb; |
|
||||||
} |
|
||||||
vec4 rgb_to_hsl (vec4 rgb) { |
|
||||||
float max_rgb = max(rgb.r, max(rgb.g, rgb.b)); |
|
||||||
float min_rgb = min(rgb.r, min(rgb.g, rgb.b)); |
|
||||||
float lightness = (max_rgb + min_rgb) / 2.0; |
|
||||||
float hue = 0.0, saturation = 0.0; |
|
||||||
if (max_rgb != min_rgb) { |
|
||||||
float vd = max_rgb - min_rgb; |
|
||||||
saturation = lightness > 0.5 ? vd / (2.0 - max_rgb - min_rgb) : vd / (max_rgb + min_rgb); |
|
||||||
if (max_rgb == rgb.r) hue = (rgb.g - rgb.b) / vd + (rgb.g < rgb.b ? 6.0 : 0.0); |
|
||||||
else if (max_rgb == rgb.g) hue = (rgb.b - rgb.r) / vd + 2.0; |
|
||||||
else if (max_rgb == rgb.b) hue = (rgb.r - rgb.g) / vd + 4.0; |
|
||||||
hue /= 6.0; |
|
||||||
} |
|
||||||
return vec4(hue, saturation, lightness, rgb.a); |
|
||||||
} |
|
||||||
vec2 bulge (vec2 v) { |
|
||||||
vec2 norm = v * 2.0 - 1.0; |
|
||||||
float hypot = length(norm); |
|
||||||
return ((norm * (hypot / 4.0 + 1.0) / 1.25) + 1.0) / 2.0; |
|
||||||
} |
|
||||||
void main() { |
|
||||||
// gl_FragColor = texture2D(texture, tex_coord);
|
|
||||||
|
|
||||||
// bulge, lines, bloom
|
|
||||||
vec4 sum = vec4(0); |
|
||||||
for (int i = -2; i <= 2; i++) { |
|
||||||
for (int j = -2; j <= 2; j++) { |
|
||||||
sum += texture2D(texture, bulge(tex_coord + vec2(i, j) * pixel_scale)) * 0.07; |
|
||||||
} |
|
||||||
} |
|
||||||
gl_FragColor = sum * sum + texture2D(texture, bulge(tex_coord)) * (0.5 * sin(bulge(tex_coord).y / pixel_scale.y) + 0.5); |
|
||||||
|
|
||||||
/* CRT-ish effect (requires draw on every animation frame!) |
|
||||||
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 in Chrome. 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 |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,39 @@ |
|||||||
|
<div class="Box fold"> |
||||||
|
<h2>Remote GPIO Control</h2> |
||||||
|
|
||||||
|
<div class="Row v"> |
||||||
|
<p> |
||||||
|
ESPTerm provides a simple API to remotely control and read GPIO pins GPIO2, GPIO4, and GPIO5. |
||||||
|
The main use of this API is to remotely reset a device that communicates with ESPTerm |
||||||
|
through the UART. |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
GPIO2 is normally used for debug UART, so when used as GPIO, debug logging is disabled. You |
||||||
|
can configure the pin functions in <a href="<?= url('cfg_system') ?>">System Settings</a>.
|
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
The GPIO control endpoint is `/api/v1/gpio`, with optional GET arguments: |
||||||
|
</p> |
||||||
|
|
||||||
|
<ul> |
||||||
|
<li>`do2=<i>x</i>` - set GPIO2 level. <i>x</i> can be `0`, `1`, or `t` to toggle the pin. |
||||||
|
<li>`do4=<i>x</i>` - set GPIO4 level |
||||||
|
<li>`do5=<i>x</i>` - set GPIO5 level |
||||||
|
<li>`pulse=<i>ms</i>` - the command starts a pulse. After the given amount of time |
||||||
|
(milliseconds) has elapsed, the pins are set to the opposite levels than what was specified |
||||||
|
(in the case of toggle, the original pin state) |
||||||
|
</ul> |
||||||
|
|
||||||
|
<p> |
||||||
|
A quick example: <a href="/api/v1/gpio?do4=1&pulse=500">`/api/v1/gpio?do4=1&pulse=500`</a> |
||||||
|
sends a 500ms long positive pulse on GPIO4. |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
The GPIO endpoint always returns a JSON object like this: `{"io2":0,"io4":1,"io5":0}`, showing |
||||||
|
the current input levels. Input reading works always, regardless of the GPIO settings. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
Loading…
Reference in new issue