Compare commits

..

7 Commits

  1. 4
      _debug_replacements.php
  2. 145
      js/term/canvas_renderer.js
  3. 5
      js/term/debug.js
  4. 8
      js/term/demo.js
  5. 201
      js/term/font_cache.js
  6. 39
      js/term/screen.js
  7. 17
      js/term/screen_layout.js
  8. 30
      js/term/screen_parser.js
  9. 727
      js/term/webgl_renderer.js
  10. 15
      lang/cs.php
  11. 28
      lang/de.php
  12. 10
      lang/en.php
  13. 28
      lang/hu.php
  14. 4
      package.json
  15. 4
      pages/about.php
  16. 40
      pages/cfg_system.php
  17. 1
      pages/help.php
  18. 102
      pages/help/cmd_screen.php
  19. 137
      pages/help/cmd_system.php
  20. 39
      pages/help/iocontrol.php
  21. 1925
      yarn.lock

@ -87,10 +87,6 @@ return [
'sta_mac' => '5c:cf:7f:02:74:51',
'ap_mac' => '5e:cf:7f:02:74:51',
'gpio2_conf' => '0',
'gpio4_conf' => '1',
'gpio5_conf' => '1',
'width' => '80',
'height' => '25',

@ -60,7 +60,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
this.screenBG = []
this.screenAttrs = []
this.screenSelection = []
this.screenLines = []
this.cursor = {}
this.reverseVideo = false
this.hasBlinkingCells = false
@ -97,7 +96,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
this.drawnScreenFG = []
this.drawnScreenBG = []
this.drawnScreenAttrs = []
this.drawnScreenLines = []
this.drawnCursor = [-1, -1, '', false]
}
@ -211,9 +209,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
drawBackground ({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) {
const { ctx, width, height, padding } = this
// is a double-width/double-height line
if (this.screenLines[y] & 0b001) cellWidth *= 2
ctx.fillStyle = this.getColor(bg)
let screenX = x * cellWidth + padding
let screenY = y * cellHeight + padding
@ -281,39 +276,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
let screenX = x * cellWidth + padding
let screenY = y * cellHeight + padding
const dblWidth = this.screenLines[y] & 0b001
const dblHeightTop = this.screenLines[y] & 0b010
const dblHeightBot = this.screenLines[y] & 0b100
if (this.screenLines[y]) {
// is a double-width/double-height line
if (dblWidth) cellWidth *= 2
ctx.save()
ctx.translate(padding, screenY + 0.5 * cellHeight)
if (dblWidth) ctx.scale(2, 1)
if (dblHeightTop) {
// top half
ctx.scale(1, 2)
ctx.translate(0, cellHeight / 4)
} else if (dblHeightBot) {
// bottom half
ctx.scale(1, 2)
ctx.translate(0, -cellHeight / 4)
}
ctx.translate(-padding, -screenY - 0.5 * cellHeight)
if (dblWidth) ctx.translate(-cellWidth / 4, 0)
if (dblHeightBot || dblHeightTop) {
// characters overflow -- needs clipping
// TODO: clipping is really expensive
ctx.beginPath()
if (dblHeightTop) ctx.rect(screenX, screenY, cellWidth, cellHeight / 2)
else ctx.rect(screenX, screenY + cellHeight / 2, cellWidth, cellHeight / 2)
ctx.clip()
}
}
let codePoint = text.codePointAt(0)
if (codePoint >= 0x2580 && codePoint <= 0x259F) {
// block elements
@ -488,8 +450,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
ctx.stroke()
}
if (this.screenLines[y]) ctx.restore()
ctx.globalAlpha = 1
}
@ -598,7 +558,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text
bg !== this.drawnScreenBG[cell] || // background updated
attrs !== this.drawnScreenAttrs[cell] || // attributes updated
this.screenLines[y] !== this.drawnScreenLines[y] || // line updated
// TODO: fix artifacts or keep this hack:
isCursor || wasCursor || // cursor blink/position updated
(isCursor && this.cursor.style !== this.drawnCursor[2]) || // cursor style updated
@ -611,28 +570,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
updateMap.set(cell, didUpdate)
}
// set drawn screen lines
this.drawnScreenLines = this.screenLines.slice()
let debugFilledUpdates = []
if (this.graphics >= 1) {
// fancy graphics gets really slow when there's a lot of masks
// so here's an algorithm that fills in holes in the update map
for (let cell of updateMap.keys()) {
if (updateMap.get(cell)) continue
let previous = updateMap.get(cell - 1) || false
let next = updateMap.get(cell + 1) || false
if (previous && next) {
// set cell to true of horizontally adjacent updated
updateMap.set(cell, true)
if (this.debug && this._debug) debugFilledUpdates.push(cell)
}
}
}
// Map of (cell index) -> boolean, whether or not a cell should be redrawn
const redrawMap = new Map()
const maskedCells = new Map()
@ -657,10 +594,7 @@ module.exports = class CanvasRenderer extends EventEmitter {
// update this cell if:
// - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are)
// - the adjacent cell updated and this cell or the adjacent cell is wide
// - this or the adjacent cell is not double-sized
if (updateMap.get(adjacentCell) &&
(this.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell])) &&
(!this.screenLines[Math.floor(cell / this.width)] && !this.screenLines[Math.floor(adjacentCell / this.width)])) {
if (updateMap.get(adjacentCell) && (this.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell]))) {
adjacentDidUpdate = true
if (this.getAdjacentCells(cell, 1).includes(adjacentCell)) {
@ -687,54 +621,11 @@ module.exports = class CanvasRenderer extends EventEmitter {
// TODO: include padding in border cells
const padding = this.padding
let regions = []
for (let y = 0; y < height; y++) {
let regionStart = null
for (let x = 0; x < width; x++) {
let cell = y * width + x
let masked = maskedCells.get(cell)
if (masked && regionStart === null) regionStart = x
if (!masked && regionStart !== null) {
regions.push([regionStart, y, x, y + 1])
regionStart = null
}
}
if (regionStart !== null) {
regions.push([regionStart, y, width, y + 1])
}
}
// join regions if possible (O(n^2-1), sorry)
let i = 0
while (i < regions.length) {
let region = regions[i]
let j = 0
while (j < regions.length) {
let other = regions[j]
if (other === region) {
j++
continue
}
if (other[0] === region[0] && other[2] === region[2] && other[3] === region[1]) {
region[1] = other[1]
regions.splice(j, 1)
if (i > j) i--
j--
}
j++
}
i++
}
ctx.save()
ctx.beginPath()
for (let region of regions) {
let [regionStart, y, endX, endY] = region
let clipRegion = (regionStart, y, endX) => {
let rectX = padding + regionStart * cellWidth
let rectY = padding + y * cellHeight
let rectWidth = (endX - regionStart) * cellWidth
let rectHeight = (endY - y) * cellHeight
let rectHeight = cellHeight
// compensate for padding
if (regionStart === 0) {
@ -750,6 +641,24 @@ module.exports = class CanvasRenderer extends EventEmitter {
ctx.rect(rectX, rectY, rectWidth, rectHeight)
}
ctx.save()
ctx.beginPath()
for (let y = 0; y < height; y++) {
let regionStart = null
for (let x = 0; x < width; x++) {
let cell = y * width + x
let masked = maskedCells.get(cell)
if (masked && regionStart === null) regionStart = x
if (!masked && regionStart !== null) {
clipRegion(regionStart, y, x)
regionStart = null
}
}
if (regionStart !== null) {
clipRegion(regionStart, y, width)
}
}
ctx.clip()
}
@ -767,7 +676,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
flags |= (+updateMap.get(cell)) << 1
flags |= (+maskedCells.get(cell)) << 2
flags |= (+isTextWide(text)) << 3
flags |= (+debugFilledUpdates.includes(cell)) << 4
this._debug.setCell(cell, flags)
}
}
@ -808,22 +716,17 @@ module.exports = class CanvasRenderer extends EventEmitter {
let cursorX = x
let cursorY = y
let cursorWidth = cellWidth // JS doesn't allow same-name assignment
if (this.cursor.hanging) {
// draw hanging cursor in the margin
cursorX += 1
}
// double-width lines
if (this.screenLines[cursorY] & 0b001) cursorWidth *= 2
let screenX = cursorX * cursorWidth + this.padding
let screenX = cursorX * cellWidth + this.padding
let screenY = cursorY * cellHeight + this.padding
if (this.cursor.style === 'block') {
// block
ctx.rect(screenX, screenY, cursorWidth, cellHeight)
ctx.rect(screenX, screenY, cellWidth, cellHeight)
} else if (this.cursor.style === 'bar') {
// vertical bar
let barWidth = 2
@ -831,7 +734,7 @@ module.exports = class CanvasRenderer extends EventEmitter {
} else if (this.cursor.style === 'line') {
// underline
let lineHeight = 2
ctx.rect(screenX, screenY + charSize.height, cursorWidth, lineHeight)
ctx.rect(screenX, screenY + charSize.height, cellWidth, lineHeight)
}
ctx.clip()

@ -203,11 +203,6 @@ module.exports = function attachDebugger (screen, connection) {
ctx.fillStyle = '#0ff'
}
if (flags & 16) {
// was filled to speed up rendering
ctx.globalAlpha /= 2
}
ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
if (flags & 8) {

@ -276,8 +276,10 @@ class ScrollingTerminal {
data += encodeAsCodePoint(25)
data += encodeAsCodePoint(80)
data += encodeAsCodePoint(this.theme)
data += this.encodeColor(this.defaultFG)
data += this.encodeColor(this.defaultBG)
data += encodeAsCodePoint(this.defaultFG & 0xFFFF)
data += encodeAsCodePoint(this.defaultFG >> 16)
data += encodeAsCodePoint(this.defaultBG & 0xFFFF)
data += encodeAsCodePoint(this.defaultBG >> 16)
let attributes = +this.cursor.visible
attributes |= (3 << 5) * +this.trackMouse // track mouse controls both
attributes |= 3 << 7 // buttons/links always visible
@ -288,7 +290,7 @@ class ScrollingTerminal {
getButtons () {
let data = 'B'
data += encodeAsCodePoint(this.buttonLabels.length)
data += this.buttonLabels.map(x => `\x01${x}\x01`).join('')
data += this.buttonLabels.map(x => x + '\x01').join('')
return data
}
getTitle () {

@ -0,0 +1,201 @@
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)
}
}
}

@ -90,14 +90,6 @@ module.exports = class TermScreen extends EventEmitter {
this.screenFG = []
this.screenBG = []
this.screenAttrs = []
this.screenLines = []
// For testing TODO remove
// this.screenLines[0] = 0b001
// this.screenLines[1] = 0b010
// this.screenLines[2] = 0b100
// this.screenLines[3] = 0b011
// this.screenLines[4] = 0b101
let selecting = false
@ -157,11 +149,6 @@ module.exports = class TermScreen extends EventEmitter {
touchPosition = getTouchPositionOffset(e.touches[0])
touchDidMove = false
touchDownTime = Date.now()
if (this.mouseMode.clicks) {
this.emit('mousedown', ...this.layout.screenToGrid(...touchPosition), 1)
e.preventDefault()
}
})
this.layout.on('touchmove', e => {
@ -174,9 +161,6 @@ module.exports = class TermScreen extends EventEmitter {
} else if (selecting) {
e.preventDefault()
selectMove(...touchPosition)
} else if (this.mouseMode.movement && !selecting) {
this.emit('mousemove', ...this.layout.screenToGrid(...touchPosition))
e.preventDefault()
}
touchDidMove = true
@ -199,9 +183,6 @@ module.exports = class TermScreen extends EventEmitter {
)
this.emit('show-touch-select-menu', selectionPos[0], selectionPos[1])
} else if (this.mouseMode.clicks) {
this.emit('mouseup', ...this.layout.screenToGrid(...touchPosition), 1)
e.preventDefault()
}
if (!touchDidMove && !this.mouseMode.clicks) {
@ -209,7 +190,7 @@ module.exports = class TermScreen extends EventEmitter {
x: touchPosition[0],
y: touchPosition[1]
}))
} else if (!touchDidMove) this.resetSelection()
}
touchPosition = null
})
@ -218,7 +199,10 @@ module.exports = class TermScreen extends EventEmitter {
if (this.selection.start[0] !== this.selection.end[0] ||
this.selection.start[1] !== this.selection.end[1]) {
// selection is not empty
this.resetSelection()
// reset selection
this.selection.start = this.selection.end = [0, 0]
this.emit('hide-touch-select-menu')
this.renderScreen('select-reset')
} else {
e.preventDefault()
this.emit('open-soft-keyboard')
@ -274,7 +258,6 @@ module.exports = class TermScreen extends EventEmitter {
this.screen.screenFG = new Array(width * height).fill(0)
this.screen.screenBG = new Array(width * height).fill(0)
this.screen.screenAttrs = new Array(width * height).fill(0)
this.screen.screenLines = new Array(height).fill(0)
}
updateLayout () {
@ -297,7 +280,6 @@ module.exports = class TermScreen extends EventEmitter {
screenBG: this.screenBG,
screenSelection: selection,
screenAttrs: this.screenAttrs,
screenLines: this.screenLines,
cursor: this.cursor,
statusScreen: this.window.statusScreen,
reverseVideo: this.reverseVideo,
@ -305,12 +287,6 @@ module.exports = class TermScreen extends EventEmitter {
})
}
resetSelection () {
this.selection.start = this.selection.end = [0, 0]
this.emit('hide-touch-select-menu')
this.renderScreen('select-reset')
}
/**
* Returns a normalized version of the current selection, such that `start`
* is always before `end`.
@ -500,11 +476,6 @@ module.exports = class TermScreen extends EventEmitter {
this.emit('opts-update')
break
case 'double-lines':
this.screenLines = update.lines
this.renderScreen('double-lines')
break
case 'static-opts':
this.layout.window.fontFamily = update.fontStack || null
this.layout.window.fontSize = update.fontSize

@ -1,5 +1,6 @@
const EventEmitter = require('events')
const CanvasRenderer = require('./screen_renderer')
const CanvasRenderer = require('./canvas_renderer')
const WebGLRenderer = require('./webgl_renderer')
const DEFAULT_FONT = '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace'
@ -11,7 +12,12 @@ module.exports = class ScreenLayout extends EventEmitter {
super()
this.canvas = document.createElement('canvas')
this.renderer = new CanvasRenderer(this.canvas)
try {
this.renderer = new WebGLRenderer(this.canvas)
} catch (err) {
console.error(err)
this.renderer = new CanvasRenderer(this.canvas)
}
this._window = {
width: 0,
@ -135,9 +141,8 @@ module.exports = class ScreenLayout extends EventEmitter {
x = x / this._windowScale - this._padding
y = y / this._windowScale - this._padding
y = Math.floor(y / cellSize.height)
if (this.renderer.drawnScreenLines[y]) x /= 2 // double size
x = Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width)
y = Math.floor(y / cellSize.height)
x = Math.max(0, Math.min(this.window.width - 1, x))
y = Math.max(0, Math.min(this.window.height - 1, y))
@ -154,8 +159,6 @@ module.exports = class ScreenLayout extends EventEmitter {
gridToScreen (x, y, withScale = false) {
let cellSize = this.getCellSize()
if (this.renderer.drawnScreenLines[y]) x *= 2 // double size
return [x * cellSize.width, y * cellSize.height].map(v => this._padding + (withScale ? v * this._windowScale : v))
}
@ -251,7 +254,7 @@ module.exports = class ScreenLayout extends EventEmitter {
this.canvas.style.height = `${realHeight}px`
// the screen has been cleared (by changing canvas width)
this.renderer.resetDrawn()
this.renderer.resetDrawn(this.canvas.width, this.canvas.height)
this.renderer.render('update-size', this.serializeRenderData())

@ -27,17 +27,15 @@ function du (str) {
}
/* eslint-disable no-multi-spaces */
// mnemonic
const TOPIC_SCREEN_OPTS = 'O' // O-ptions
const TOPIC_STATIC_OPTS = 'P' // P-arams
const TOPIC_CONTENT = 'S' // S-creen
const TOPIC_TITLE = 'T' // T-itle
const TOPIC_BUTTONS = 'B' // B-uttons
const TOPIC_CURSOR = 'C' // C-ursor
const TOPIC_INTERNAL = 'D' // D-ebug
const TOPIC_BELL = '!' // !!!
const TOPIC_BACKDROP = 'W' // W-allpaper
const TOPIC_DOUBLE_LINES = 'H' // H-uge
const TOPIC_SCREEN_OPTS = 'O'
const TOPIC_STATIC_OPTS = 'P'
const TOPIC_CONTENT = 'S'
const TOPIC_TITLE = 'T'
const TOPIC_BUTTONS = 'B'
const TOPIC_CURSOR = 'C'
const TOPIC_INTERNAL = 'D'
const TOPIC_BELL = '!'
const TOPIC_BACKDROP = 'W'
const OPT_CURSOR_VISIBLE = (1 << 0)
const OPT_DEBUGBAR = (1 << 1)
@ -190,16 +188,6 @@ module.exports = class ScreenParser {
fontSize
})
} else if (topic === TOPIC_DOUBLE_LINES) {
let lines = []
const count = du(strArray[ci++])
for (let i = 0; i < count; i++) {
// format: INDEX<<3 | (dbl-h-bot : dbl-h-top : dbl-w)
let n = du(strArray[ci++])
lines[n >> 3] = n & 0b111
}
updates.push({ topic: 'double-lines', lines: lines })
} else if (topic === TOPIC_TITLE) {
updates.push({ topic: 'title', title: collectOneTerminatedString() })

@ -0,0 +1,727 @@
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
]
}
}
}

@ -235,11 +235,6 @@ return [
'persist.restore_hard_explain' =>
'(Tímto vymažete nastavení WiFi! Záloha a systémové heslo zůstanou beze změny.)',
'backup.title' => 'Záloha do souboru',
'backup.explain' => 'Všechna nastavení kromě systémového hesla je možné uložit do a obnovit z INI souboru.',
'backup.export' => 'Zálohovat do souboru',
'backup.import' => 'Nahrát soubor!',
// UART settings form
'uart.title' => 'Sériový port',
@ -265,16 +260,6 @@ return [
obrazovky a stránky se budou načítat rychleji. Nevýhodou je vyšší spotřeba a citlivost k~rušení.
',
'hwtuning.overclock' => 'Přetaktovat na 160~MHz',
'gpio2_config' => 'Funkce GPIO2',
'gpio4_config' => 'Funkce GPIO4',
'gpio5_config' => 'Funkce GPIO5',
'gpio_config.off' => 'Vypnuto',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Výstup (výchozí stav 0)',
'gpio_config.out_initial1' => 'Výstup (výchozí stav 1)',
'gpio_config.in_pull' => 'Vstup (s pull-upem)',
'gpio_config.in_nopull' => 'Vstup (plovoucí)',
// Generic button / dialog labels

@ -80,10 +80,6 @@ return [
'term.debugbar' => 'Debug-Leiste anzeigen',
'term.ascii_debug' => 'Kontrollcodes anzeigen',
'term.backdrop' => 'Hintergrundbild-URL',
'term.button_count' => 'Tastenanzahl',
'term.button_colors' => 'Tastenfarben',
'term.font_stack' => 'Schriftstapel',
'term.font_size' => 'Schriftgröße',
'cursor.block_blink' => 'Block, blinkend',
'cursor.block_steady' => 'Block, ruhig',
@ -184,7 +180,7 @@ return [
'pwlock.title' => 'Zugriffsbeschränkungen',
'pwlock.explain' => '
Manche, oder alle Teile des Web-Interface können mit einem Passwort geschützt werden.
Lass die Passwortfelder leer wenn du es nicht verändern möchtest.<br>
Lass die Passwortfelder leer wenn du es sie verändern möchtest.<br>
Das voreingestellte Passwort ist "%def_access_pw%".',
'pwlock.region' => 'Geschützte Seiten',
'pwlock.region.none' => 'Keine, alles offen',
@ -233,11 +229,6 @@ return [
(Dies löscht die WLAN-Konfiguration! Beeinflusst die gespeicherten Voreinstellungen
oder das Systempasswort nicht.)',
'backup.title' => 'Konfigurationsdatei sichern',
'backup.explain' => 'Die ganze Konfiguration außer dem Systempasswort können mit einer INI-Datei gesichert und wiederhergestellt werden.',
'backup.export' => 'Datei exportieren',
'backup.import' => 'Importieren!',
// UART settings form
'uart.title' => 'Serieller Port Parameter',
@ -259,22 +250,11 @@ return [
'hwtuning.title' => 'Hardware-Tuning',
'hwtuning.explain' => '
Der ESP8266 kann von 80&nbsp;MHz auf 160&nbsp;MHz übertaktet werden.
ESP8266 kann übertaktet werden von 80&nbsp;MHz auf 160&nbsp;MHz.
Alles wird etwas schneller sein, aber mit höherem Stromverbrauch,
und eventuell auch mit mehr Interferenz.
Mit Sorgfalt benutzen.
und eventuell auch mit höherer Interferenz. Mit Sorgfalt benutzen.
',
'hwtuning.overclock' => 'Auf 160MHz übertakten',
'gpio2_config' => 'GPIO2 Funktion',
'gpio4_config' => 'GPIO4 Funktion',
'gpio5_config' => 'GPIO5 Funktion',
'gpio_config.off' => 'Deaktiviert',
'gpio_config.off_2' => 'UART Tx Debuggen',
'gpio_config.out_initial0' => 'Output (Anfangslevel 0)',
'gpio_config.out_initial1' => 'Output (Anfangslevel 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
'hwtuning.overclock' => 'Übertakten',
// Generic button / dialog labels

@ -266,16 +266,6 @@ return [
',
'hwtuning.overclock' => 'Overclock to 160MHz',
'gpio2_config' => 'GPIO2 function',
'gpio4_config' => 'GPIO4 function',
'gpio5_config' => 'GPIO5 function',
'gpio_config.off' => 'Disabled',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Output (initial 0)',
'gpio_config.out_initial1' => 'Output (initial 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels
'apply' => 'Apply!',

@ -23,7 +23,7 @@ return [
'term_nav.paste' => 'Beillesztés',
'term_nav.upload' => 'Feltöltés',
'term_nav.keybd' => 'Billentyűzet',
'term_nav.paste_prompt' => 'Szöveg beillesztése és küldése:',
'term_nav.paste_prompt' => 'Szöveg beillesztése és küldés:',
'term_conn.connecting' => 'Csatlakozás',
'term_conn.waiting_content' => 'Várakozás a csatlakozásra',
@ -80,10 +80,6 @@ return [
'term.debugbar' => 'Belső állapot hibakeresés',
'term.ascii_debug' => 'Kontroll kódok mutatása',
'term.backdrop' => 'Háttérkép URL.je',
'term.button_count' => 'Gomb szám',
'term.button_colors' => 'Gomb színek',
'term.font_stack' => 'Betű típus',
'term.font_size' => 'Betű méret',
'cursor.block_blink' => 'Blokk, villog',
'cursor.block_steady' => 'Blokk, fix',
@ -233,12 +229,6 @@ return [
'persist.restore_hard_explain' =>
'(Ez törli a Wifi beállításokat, de nincs hatása az admin jelszóra.)',
'backup.title' => 'Configurációs fájl biztonsági másolat készítés',
'backup.explain' => 'Minden beállítás menthető és visszaállítható az admin jelszó kivételévelAll config except the admin password can be backed up and restored using egy .INI fájllal.',
'backup.export' => 'Fáljbe exportálás',
'backup.import' => 'Importálás!',
// UART settings form
'uart.title' => 'Soros port paraméterek',
@ -251,7 +241,7 @@ return [
'uart.parity.none' => 'Egyiksem',
'uart.parity.odd' => 'Páratlan',
'uart.parity.even' => 'Páros',
'uart.stop_bits' => 'Stop-bit',
'uart.stop_bits' => 'Stop-bite',
'uart.stop_bits.one' => 'Egy',
'uart.stop_bits.one_and_half' => 'Másfél',
'uart.stop_bits.two' => 'Kettő',
@ -267,23 +257,13 @@ return [
',
'hwtuning.overclock' => 'Órajel emelése 160MHz-re',
'gpio2_config' => 'GPIO2 function', // TODO translate
'gpio4_config' => 'GPIO4 function',
'gpio5_config' => 'GPIO5 function',
'gpio_config.off' => 'Disabled',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Output (initial 0)',
'gpio_config.out_initial1' => 'Output (initial 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels
'apply' => 'Alkalmaz',
'start' => 'Start',
'cancel' => 'Mégse',
'enabled' => 'Engedélyezve',
'disabled' => 'Letiltva',
'enabled' => 'Engedélyez',
'disabled' => 'Letilt',
'yes' => 'Igen',
'no' => 'Nem',
'confirm' => 'OK',

@ -8,10 +8,10 @@
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"babel-preset-minify": "^0.2.0",
"html-minifier": "^3.5.5",
"node-sass": "^4.5.3",
"standard": "^10.0.3",
"webpack": "^3.6.0"
"webpack": "^3.6.0",
"html-minifier": "^3.5.5"
},
"scripts": {
"webpack": "webpack --display-modules $@",

@ -10,8 +10,8 @@
</p>
<p>
Vyvinuto na <a href="http://measure.feld.cvut.cz/" target="blank">Katedře měření, FEL ČVUT</a><br>
Developed at the Department of Measurement, FEE CTU in Prague
<a href="http://measure.feld.cvut.cz/" target="blank">Katedra měření, FEL ČVUT</a><br>
Department of Measurement, FEE CTU
</p>
</div>

@ -8,7 +8,7 @@
<div class="Row buttons2">
<a class="button icn-restore"
onclick="return confirm('<?= e(tr('persist.confirm_restore')) ?>');"
onclick="return confirm('<?= tr('persist.confirm_restore') ?>');"
href="<?= e(url('restore_defaults')) ?>">
<?= tr('persist.restore_defaults') ?>
</a>
@ -19,7 +19,7 @@
</div>
<div class="Row buttons2">
<a onclick="return confirm('<?= e(tr('persist.confirm_restore_hard')) ?>');"
<a onclick="return confirm('<?= tr('persist.confirm_restore_hard') ?>');"
href="<?= e(url('restore_hard')) ?>">
<?= tr('persist.restore_hard') ?>
</a><br>
@ -63,39 +63,6 @@
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" id="overclock" name="overclock" value="%overclock%">
</div>
<div class="Row">
<label for="gpio2_conf"><?= tr("gpio2_config") ?></label>
<select name="gpio2_conf" id="gpio2_conf">
<option value="0"><?= tr("gpio_config.off_2") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row">
<label for="gpio4_conf"><?= tr("gpio4_config") ?></label>
<select name="gpio4_conf" id="gpio4_conf">
<option value="0"><?= tr("gpio_config.off") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row">
<label for="gpio5_conf"><?= tr("gpio5_config") ?></label>
<select name="gpio5_conf" id="gpio5_conf">
<option value="0"><?= tr("gpio_config.off") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-hw').submit()"><?= tr('apply') ?></a>
@ -187,7 +154,4 @@ $NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:t
}
$('#pwlock').val(%pwlock%);
$('#gpio2_conf').val(%gpio2_conf%);
$('#gpio4_conf').val(%gpio4_conf%);
$('#gpio5_conf').val(%gpio5_conf%);
</script>

@ -18,7 +18,6 @@
<?php require __DIR__ . "/help/cmd_screen.php"; ?>
<?php require __DIR__ . "/help/cmd_d2d.php"; ?>
<?php require __DIR__ . "/help/cmd_system.php"; ?>
<?php require __DIR__ . "/help/iocontrol.php"; ?>
<script>
function hpfold(yes) {

@ -8,124 +8,60 @@
If an argument is left out, it's treated as 0 or 1, depending on what makes sense for the command.
</p>
<h3>Erasing &amp; Inserting</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>`\e[<i>m</i>J`</td>
<td>
Clear part of screen. _m_: 0 - from cursor, 1 - to cursor, 2 - all
`\e[<i>m</i>J`
</td>
</tr>
<tr>
<td>`\e[<i>m</i>K`</td>
<td>
Erase part of line. _m_: 0 - from cursor, 1 - to cursor, 2 - all
</td>
</tr>
<tr>
<td>`\e[<i>n</i>X`</td>
<td>
Erase _n_ characters in line.
Clear part of screen. _m_: 0 - from cursor, 1 - to cursor, 2 - all
</td>
</tr>
<tr>
<td><code>
\e[<i>n</i>L \\
\e[<i>n</i>M
</code></td>
<td>
Insert (`L`) or delete (`M`) _n_ lines. Following lines are pulled up or pushed down.
`\e[<i>m</i>K`
</td>
</tr>
<tr>
<td><code>
\e[<i>n</i>@ \\
\e[<i>n</i>P
</code></td>
<td>
Insert (`@`) or delete (`P`) _n_ characters. The rest of the line is pulled left or pushed right.
Characters going past the end of line are lost.
Erase part of line. _m_: 0 - from cursor, 1 - to cursor, 2 - all
</td>
</tr>
</tbody>
</table>
<h3>Supersized lines</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>`\e#1`, `\e#2`</td>
<td>
Make the current line part of a double-height line.
Use `1` for the top, `2` for the bottom half.
</td>
</tr>
<tr>
<td>`\e#3`, `\e#4`</td>
`\e[<i>n</i>X`</td>
<td>
Make the current line part of a double-width, double-height line.
Use `3` for the top, `4` for the bottom half.
Erase _n_ characters in line.
</td>
</tr>
<tr>
<td>`\e#6`</td>
<td>
Make the current line double-width.
</td>
</tr>
<tr>
<td>`\e#5`</td>
`\e[<i>n</i>b`</td>
<td>
Reset the current line to normal size.
Repeat last printed characters _n_ times (moving cursor and using the current style).
</td>
</tr>
</tbody>
</table>
<h3>Other</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>`\ec`</td>
<td>
Clear screen, reset attributes and cursor. This command also restores the default
screen size, title, button labels and messages and the background URL.
<code>
\e[<i>n</i>L \\
\e[<i>n</i>M
</code>
</td>
</tr>
<tr>
<td><code>
\e[?1049h \\
\e[?1049l
</code></td>
<td>
Switch to (`h`) or from (`l`) an alternate screen.
ESPTerm can't implement this fully, so the original screen content is not saved,
but it will remember the cursor, screen size, terminal title, button labels and messages.
Insert (`L`) or delete (`M`) _n_ lines. Following lines are pulled up or pushed down.
</td>
</tr>
<tr>
<td>`\e[8;<i>r</i>;<i>c</i>t`</td>
<td>Set screen size to _r_ rows and _c_ columns (this is a command borrowed from Xterm)</td>
</tr>
<tr>
<td>
`\e[<i>n</i>b`</td>
<td>
Repeat last printed characters _n_ times (moving cursor and using the current style).
<code>
\e[<i>n</i>@ \\
\e[<i>n</i>P
</code>
</td>
</tr>
<tr>
<td>`\e#8`</td>
<td>
Reset all screen attributes to default and fill the screen with the letter "E". This was
historically used for aligning CRT displays, now can be useful e.g. for testing erasing commands.
Insert (`@`) or delete (`P`) _n_ characters. The rest of the line is pulled left or pushed right.
Characters going past the end of line are lost.
</td>
</tr>
</tbody>

@ -8,8 +8,6 @@
Those changes are not retained after restart.
</p>
<h3>Single-byte commands &amp; queries</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
@ -30,6 +28,17 @@
This message contains the curretn version, unique ID, and the IP address if in Client mode.
</td>
</tr>
<tr>
<td>`\ec`</td>
<td>
Clear screen, reset attributes and cursor. This command also restores the default
screen size, title, button labels and messages and the background URL.
</td>
</tr>
<tr>
<td>`\e[8;<i>r</i>;<i>c</i>t`</td>
<td>Set screen size to _r_ rows and _c_ columns (this is a command borrowed from Xterm)</td>
</tr>
<tr>
<td>`\e[5n`</td>
<td>
@ -37,14 +46,6 @@
Can be used to check if the terminal has booted up and is ready to receive commands.
</td>
</tr>
</tbody>
</table>
<h3>Setting parameters</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td>`\e[<i>n</i> q`</td>
<td>
@ -76,18 +77,22 @@
</td>
</tr>
<tr>
<td><code>
\e]28;<i>x</i>;<i>t</i>\a
</code></td>
<td>
<code>
\e]28;<i>x</i>;<i>t</i>\a
</code>
</td>
<td>
Set label for button _x_ (1-5) to _t_ - e.g.`\e]28;1;Yes\a`
sets the first button text to "Yes".
</td>
</tr>
<tr>
<td><code>
\e]29;<i>x</i>;<i>m</i>\a
</code></td>
<td>
<code>
\e]29;<i>x</i>;<i>m</i>\a
</code>
</td>
<td>
Set message for button _x_ (1-5) to _m_ - e.g.`\e]29;3;+\a`
sets the 3rd button to send "+" when pressed. The message can be up to
@ -95,39 +100,58 @@
</td>
</tr>
<tr>
<td><code>
\e]30;<i>x</i>;<i>c</i>\a
</code></td>
<td>
Set button _x_ (1-5) color to _c_ - e.g.`\e]30;2;#00FF00\a`
makes the 2nd button green. Supported are SGR colors 1-255
and TrueColor in the format `#RRGGBB`. Use 0 to
reset to the default color.
<code>
\e]9;<i>t</i>\a
</code>
</td>
<td>
Show a notification with text _t_. This will be either a desktop notification
or a pop-up balloon.
</td>
</tr>
<tr>
<td><code>
\e[?800h \\
\e[?800l
</code></td>
<td>
<code>
\e[?<i>n</i>s \\
\e[?<i>n</i>r
</code>
</td>
<td>
Save (`s`) and restore (`r`) any option set using `CSI ? <i>n</i> h`.
This is used by some applications to back up the original state before
making changes.
</td>
</tr>
<tr>
<td>
<code>
\e[?800h \\
\e[?800l
</code>
</td>
<td>
Show (`h`) or hide (`l`) the action buttons (the blue buttons under the screen).
</td>
</tr>
<tr>
<td><code>
\e[?801h \\
\e[?801l
</code></td>
<td>
<code>
\e[?801h \\
\e[?801l
</code>
</td>
<td>
Show (`h`) or hide (`l`) menu/help links under the screen.
</td>
</tr>
<tr>
<td><code>
\e[?2004h \\
\e[?2004l
</code></td>
<td>
<code>
\e[?2004h \\
\e[?2004l
</code>
</td>
<td>
Enable (`h`) or disable (`l`) Bracketed Paste mode.
This mode makes any text sent using the Upload Tool be preceded by `\e[200\~`
@ -136,41 +160,28 @@
</td>
</tr>
<tr>
<td><code>
\e[12h \\
\e[12l
</code></td>
<td>
Enable (`h`) or disable (`l`) Send-Receive Mode (SRM).
SRM is the opposite of Local Echo, meaning `\e[12h` disables and `\e[12l` enables Local Echo.
<code>
\e[?1049h \\
\e[?1049l
</code>
</td>
</tr>
</tbody>
</table>
<h3>Other</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td><code>
\e]9;<i>t</i>\a
</code></td>
<td>
Show a notification with text _t_. This will be either a desktop notification
or a pop-up balloon.
Switch to (`h`) or from (`l`) an alternate screen.
ESPTerm can't implement this fully, so the original screen content is not saved,
but it will remember the cursor, screen size, terminal title, button labels and messages.
</td>
</tr>
<tr>
<td><code>
\e[?<i>n</i>s \\
\e[?<i>n</i>r
</code></td>
<td>
Save (`s`) and restore (`r`) any option set using `CSI ? <i>n</i> h`.
This is used by some applications to back up the original state before
making changes.
<code>
\e[12h \\
\e[12l
</code>
</td>
<td>
Enable (`h`) or disable (`l`) Send-Receive Mode (SRM).
SRM is the opposite of Local Echo, meaning `\e[12h` disables and `\e[12l` enables Local Echo.
</td>
</tr>
</tbody>

@ -1,39 +0,0 @@
<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&amp;pulse=500">`/api/v1/gpio?do4=1&amp;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>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save