UNPKG

gw-canvas

Version:

Library for rendering colorized bitmap fonts. Very fast, suitable for applications where the whole canvas needs frequent redrawing.

324 lines (323 loc) 11.6 kB
// Based on: https://github.com/ondras/fastiles/blob/master/ts/scene.ts (v2.1.0) import * as shaders from "./shaders"; import { Glyphs } from "./glyphs"; import { Layer } from "./layer"; import * as Color from "./color"; export const VERTICES_PER_TILE = 6; export class NotSupportedError extends Error { constructor(...params) { // Pass remaining arguments (including vendor specific ones) to parent constructor super(...params); // Maintains proper stack trace for where our error was thrown (only available on V8) // @ts-ignore if (Error.captureStackTrace) { // @ts-ignore Error.captureStackTrace(this, NotSupportedError); } this.name = "NotSupportedError"; } } export class Canvas { constructor(options) { this._renderRequested = false; this._autoRender = true; this._width = 50; this._height = 25; this._layers = []; if (!options.glyphs) throw new Error("You must supply glyphs for the canvas."); this._node = this._createNode(); this._createContext(); this._configure(options); } get node() { return this._node; } get width() { return this._width; } get height() { return this._height; } get tileWidth() { return this._glyphs.tileWidth; } get tileHeight() { return this._glyphs.tileHeight; } get pxWidth() { return this.node.clientWidth; } get pxHeight() { return this.node.clientHeight; } get glyphs() { return this._glyphs; } set glyphs(glyphs) { this._setGlyphs(glyphs); } layer(depth = 0) { let layer = this._layers.find((l) => l.depth === depth); if (layer) return layer; layer = new Layer(this, depth); this._layers.push(layer); this._layers.sort((a, b) => a.depth - b.depth); return layer; } clearLayer(depth = 0) { const layer = this._layers.find((l) => l.depth === depth); if (layer) layer.clear(); } removeLayer(depth = 0) { const index = this._layers.findIndex((l) => l.depth === depth); if (index > -1) { this._layers.splice(index, 1); } } _createNode() { return document.createElement("canvas"); } _configure(options) { this._width = options.width || this._width; this._height = options.height || this._height; this._autoRender = options.render !== false; this._setGlyphs(options.glyphs); this.bg = Color.from(options.bg || Color.BLACK); if (options.div) { let el; if (typeof options.div === "string") { el = document.getElementById(options.div); if (!el) { console.warn("Failed to find parent element by ID: " + options.div); } } else { el = options.div; } if (el && el.appendChild) { el.appendChild(this.node); } } } _setGlyphs(glyphs) { if (glyphs === this._glyphs) return false; this._glyphs = glyphs; this.resize(this._width, this._height); const gl = this._gl; const uniforms = this._uniforms; gl.uniform2uiv(uniforms["tileSize"], [this.tileWidth, this.tileHeight]); this._uploadGlyphs(); return true; } resize(width, height) { this._width = width; this._height = height; const node = this.node; node.width = this._width * this.tileWidth; node.height = this._height * this.tileHeight; const gl = this._gl; // const uniforms = this._uniforms; gl.viewport(0, 0, this.node.width, this.node.height); // gl.uniform2ui(uniforms["viewportSize"], this.node.width, this.node.height); this._createGeometry(); this._createData(); } _requestRender() { if (this._renderRequested) return; this._renderRequested = true; if (!this._autoRender) return; requestAnimationFrame(() => this.render()); } hasXY(x, y) { return x >= 0 && y >= 0 && x < this.width && y < this.height; } toX(x) { return Math.floor((this.width * x) / this.node.clientWidth); } toY(y) { return Math.floor((this.height * y) / this.node.clientHeight); } _createContext() { let gl = this.node.getContext("webgl2"); if (!gl) { throw new NotSupportedError("WebGL 2 not supported"); } this._gl = gl; this._buffers = {}; this._attribs = {}; this._uniforms = {}; const p = createProgram(gl, shaders.VS, shaders.FS); gl.useProgram(p); const attributeCount = gl.getProgramParameter(p, gl.ACTIVE_ATTRIBUTES); for (let i = 0; i < attributeCount; i++) { gl.enableVertexAttribArray(i); let info = gl.getActiveAttrib(p, i); this._attribs[info.name] = i; } const uniformCount = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); for (let i = 0; i < uniformCount; i++) { let info = gl.getActiveUniform(p, i); this._uniforms[info.name] = gl.getUniformLocation(p, info.name); } gl.uniform1i(this._uniforms["font"], 0); this._texture = createTexture(gl); } _createGeometry() { const gl = this._gl; this._buffers.position && gl.deleteBuffer(this._buffers.position); this._buffers.uv && gl.deleteBuffer(this._buffers.uv); let buffers = createGeometry(gl, this._attribs, this.width, this.height); Object.assign(this._buffers, buffers); } _createData() { const gl = this._gl; const attribs = this._attribs; this._buffers.fg && gl.deleteBuffer(this._buffers.fg); this._buffers.bg && gl.deleteBuffer(this._buffers.bg); this._buffers.glyph && gl.deleteBuffer(this._buffers.glyph); if (this._layers.length) { this._layers.forEach((l) => l.detach()); this._layers.length = 0; } const fg = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, fg); gl.vertexAttribIPointer(attribs["fg"], 1, gl.UNSIGNED_SHORT, 0, 0); const bg = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, bg); gl.vertexAttribIPointer(attribs["bg"], 1, gl.UNSIGNED_SHORT, 0, 0); const glyph = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, glyph); gl.vertexAttribIPointer(attribs["glyph"], 1, gl.UNSIGNED_BYTE, 0, 0); Object.assign(this._buffers, { fg, bg, glyph }); } _uploadGlyphs() { if (!this._glyphs.needsUpdate) return; const gl = this._gl; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this._texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._glyphs.node); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); this._requestRender(); this._glyphs.needsUpdate = false; } draw(x, y, glyph, fg, bg) { this.layer(0).draw(x, y, glyph, fg, bg); } render() { const gl = this._gl; if (this._glyphs.needsUpdate) { // auto keep glyphs up to date this._uploadGlyphs(); } else if (!this._renderRequested) { return; } this._renderRequested = false; // clear to bg color? gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.clearColor(this.bg.r / 100, this.bg.g / 100, this.bg.b / 100, this.bg.a / 100); gl.clear(gl.COLOR_BUFFER_BIT); // sort layers? this._layers.forEach((layer) => { if (layer.empty) return; // set depth gl.uniform1i(this._uniforms["depth"], layer.depth); gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.fg); gl.bufferData(gl.ARRAY_BUFFER, layer.fg, gl.DYNAMIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.bg); gl.bufferData(gl.ARRAY_BUFFER, layer.bg, gl.DYNAMIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.glyph); gl.bufferData(gl.ARRAY_BUFFER, layer.glyph, gl.DYNAMIC_DRAW); gl.drawArrays(gl.TRIANGLES, 0, this._width * this._height * VERTICES_PER_TILE); }); } } export function withImage(image) { let opts = {}; if (typeof image === "string") { opts.glyphs = Glyphs.fromImage(image); } else if (image instanceof HTMLImageElement) { opts.glyphs = Glyphs.fromImage(image); } else { if (!image.image) throw new Error("You must supply the image."); Object.assign(opts, image); opts.glyphs = Glyphs.fromImage(image.image); } return new Canvas(opts); } export function withFont(src) { if (typeof src === "string") { src = { font: src }; } src.glyphs = Glyphs.fromFont(src); return new Canvas(src); } // Copy of: https://github.com/ondras/fastiles/blob/master/ts/utils.ts (v2.1.0) export function createProgram(gl, ...sources) { const p = gl.createProgram(); [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, index) => { const shader = gl.createShader(type); gl.shaderSource(shader, sources[index]); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { throw new Error(gl.getShaderInfoLog(shader)); } gl.attachShader(p, shader); }); gl.linkProgram(p); if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { throw new Error(gl.getProgramInfoLog(p)); } return p; } function createTexture(gl) { let t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); return t; } // x, y offsets for 6 verticies (2 triangles) in square export const QUAD = [0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]; function createGeometry(gl, attribs, width, height) { let tileCount = width * height; let positionData = new Float32Array(tileCount * QUAD.length); let offsetData = new Uint8Array(tileCount * QUAD.length); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (x + y * width) * QUAD.length; positionData.set(QUAD.map((v, i) => { if (i % 2) { // y return 1 - (2 * (y + v)) / height; } else { return (2 * (x + v)) / width - 1; } }), index); offsetData.set(QUAD, index); } } const position = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, position); gl.vertexAttribPointer(attribs["position"], 2, gl.FLOAT, false, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW); const uv = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, uv); gl.vertexAttribIPointer(attribs["offset"], 2, gl.UNSIGNED_BYTE, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, offsetData, gl.STATIC_DRAW); return { position, uv }; }