From da05ec7a2d026ec87fe4fd29490c2e2ecaf5ffec Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sat, 28 Oct 2017 11:36:55 +0200 Subject: [PATCH] gl renderer: proper block cursor, status screen, special character rendering --- js/term/font_cache.js | 144 +++++++++++++++++++++++++++++++++++++- js/term/webgl_renderer.js | 102 ++++++++++++++++++++++----- 2 files changed, 229 insertions(+), 17 deletions(-) diff --git a/js/term/font_cache.js b/js/term/font_cache.js index e76d2ae..7fa0b17 100644 --- a/js/term/font_cache.js +++ b/js/term/font_cache.js @@ -42,7 +42,7 @@ module.exports = class GLFontCache { ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillStyle = 'white' - ctx.fillText(character, cellSize.width * 1.5, cellSize.height * 1.5) + this.drawCharacter(character) let imageData = ctx.getImageData(0, 0, width, height) @@ -56,4 +56,146 @@ module.exports = class GLFontCache { 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) + } + } } diff --git a/js/term/webgl_renderer.js b/js/term/webgl_renderer.js index be794c4..b9946ef 100644 --- a/js/term/webgl_renderer.js +++ b/js/term/webgl_renderer.js @@ -73,8 +73,10 @@ module.exports = class WebGLRenderer extends EventEmitter { } resetDrawn (width, height) { - this.gl.clearColor(0, 0, 0, 1) - this.gl.viewport(0, 0, width, height) + this.gl.clearColor(...this.getColor(this.defaultBG)) + 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) } @@ -229,10 +231,16 @@ precision mediump float; attribute vec2 position; uniform mat4 projection; uniform vec2 char_pos; +uniform bool clip; varying highp vec2 tex_coord; void main() { - gl_Position = projection * vec4(char_pos - vec2(1.0, 1.0) + 3.0 * position, 0.0, 1.0); - tex_coord = position; + if (clip) { + gl_Position = projection * vec4(char_pos + 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); + tex_coord = position; + } } `, ` precision highp float; @@ -266,7 +274,8 @@ void main() { projection: gl.getUniformLocation(charShader, 'projection'), charPos: gl.getUniformLocation(charShader, 'char_pos'), color: gl.getUniformLocation(charShader, 'color'), - texture: gl.getUniformLocation(charShader, 'texture') + texture: gl.getUniformLocation(charShader, 'texture'), + clip: gl.getUniformLocation(charShader, 'clip') } } @@ -292,7 +301,32 @@ void main() { } draw (reason) { - const { gl, width, height, padding, devicePixelRatio } = this + const { gl, width, height, padding, devicePixelRatio, statusScreen } = this + let { screen, screenFG, screenBG, screenAttrs } = this + + if (statusScreen) { + this.startDrawLoop() + + 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] = '*' + } + } + } if (this.debug && this._debug) this._debug.drawStart(reason) @@ -323,12 +357,15 @@ void main() { this.cursor.y === y && this.cursor.visible - let text = this.screen[cell] - let fg = this.screenFG[cell] | 0 - let bg = this.screenBG[cell] | 0 - let attrs = this.screenAttrs[cell] | 0 + 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 @@ -351,9 +388,6 @@ void main() { bg = -2 } - // TODO: actual cursor - if (isCursor) [fg, bg] = [bg, fg] - gl.uniform2f(this.bgShader.uniforms.charPos, x, y) gl.uniform4f(this.bgShader.uniforms.color, ...this.getColor(bg)) @@ -369,7 +403,7 @@ void main() { this.drawSquare() - if (text.trim()) { + if (text.trim() || isCursor) { let fontIndex = 0 if (attrs & ATTR_BOLD) fontIndex |= 1 if (attrs & ATTR_ITALIC) fontIndex |= 2 @@ -377,7 +411,7 @@ void main() { let type = font + text if (!textCells[type]) textCells[type] = [] - textCells[type].push({ x, y, text, font, fg }) + textCells[type].push({ x, y, text, font, fg, bg, isCursor }) } } @@ -391,18 +425,54 @@ void main() { gl.uniform1i(this.charShader.uniforms.texture, 0) for (let cell of textCells[key]) { - let { x, y, fg } = cell + let { x, y, fg, bg, isCursor } = cell gl.uniform2f(this.charShader.uniforms.charPos, x, y) gl.uniform4f(this.charShader.uniforms.color, ...this.getColor(fg)) 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) + } } } if (this.debug && this._debug) this._debug.drawEnd() } + 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)) + this.draw('draw-loop') + } + static colorToRGBA (color) { color = color.substr(1) if (color.length === 3) {