UNPKG

rot-js

Version:

A roguelike toolkit in JavaScript

280 lines (262 loc) 9.83 kB
import Backend from "./backend.js"; import * as Color from "../color.js"; /** * @class Tile backend * @private */ export default class TileGL extends Backend { constructor() { super(); this._uniforms = {}; try { this._gl = this._initWebGL(); } catch (e) { if (typeof e === "string") { alert(e); } else if (e instanceof Error) { alert(e.message); } } } static isSupported() { return !!document.createElement("canvas").getContext("webgl2", { preserveDrawingBuffer: true }); } schedule(cb) { requestAnimationFrame(cb); } getContainer() { return this._gl.canvas; } setOptions(opts) { super.setOptions(opts); this._updateSize(); let tileSet = this._options.tileSet; if (tileSet && "complete" in tileSet && !tileSet.complete) { tileSet.addEventListener("load", () => this._updateTexture(tileSet)); } else { this._updateTexture(tileSet); } } draw(data, clearBefore) { const gl = this._gl; const opts = this._options; let [x, y, ch, fg, bg] = data; let scissorY = gl.canvas.height - (y + 1) * opts.tileHeight; gl.scissor(x * opts.tileWidth, scissorY, opts.tileWidth, opts.tileHeight); if (clearBefore) { if (opts.tileColorize) { gl.clearColor(0, 0, 0, 0); } else { gl.clearColor(...parseColor(bg)); } gl.clear(gl.COLOR_BUFFER_BIT); } if (!ch) { return; } let chars = [].concat(ch); let bgs = [].concat(bg); let fgs = [].concat(fg); gl.uniform2fv(this._uniforms["targetPosRel"], [x, y]); for (let i = 0; i < chars.length; i++) { let tile = this._options.tileMap[chars[i]]; if (!tile) { throw new Error(`Char "${chars[i]}" not found in tileMap`); } gl.uniform1f(this._uniforms["colorize"], opts.tileColorize ? 1 : 0); gl.uniform2fv(this._uniforms["tilesetPosAbs"], tile); if (opts.tileColorize) { gl.uniform4fv(this._uniforms["tint"], parseColor(fgs[i])); gl.uniform4fv(this._uniforms["bg"], parseColor(bgs[i])); } gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } /* for (let i=0;i<chars.length;i++) { if (this._options.tileColorize) { // apply colorization let canvas = this._colorCanvas; let context = canvas.getContext("2d") as CanvasRenderingContext2D; context.globalCompositeOperation = "source-over"; context.clearRect(0, 0, tileWidth, tileHeight); let fg = fgs[i]; let bg = bgs[i]; context.drawImage( this._options.tileSet!, tile[0], tile[1], tileWidth, tileHeight, 0, 0, tileWidth, tileHeight ); if (fg != "transparent") { context.fillStyle = fg; context.globalCompositeOperation = "source-atop"; context.fillRect(0, 0, tileWidth, tileHeight); } if (bg != "transparent") { context.fillStyle = bg; context.globalCompositeOperation = "destination-over"; context.fillRect(0, 0, tileWidth, tileHeight); } this._ctx.drawImage(canvas, x*tileWidth, y*tileHeight, tileWidth, tileHeight); } else { // no colorizing, easy this._ctx.drawImage( this._options.tileSet!, tile[0], tile[1], tileWidth, tileHeight, x*tileWidth, y*tileHeight, tileWidth, tileHeight ); } } */ } clear() { const gl = this._gl; gl.clearColor(...parseColor(this._options.bg)); gl.scissor(0, 0, gl.canvas.width, gl.canvas.height); gl.clear(gl.COLOR_BUFFER_BIT); } computeSize(availWidth, availHeight) { let width = Math.floor(availWidth / this._options.tileWidth); let height = Math.floor(availHeight / this._options.tileHeight); return [width, height]; } computeFontSize() { throw new Error("Tile backend does not understand font size"); } eventToPosition(x, y) { let canvas = this._gl.canvas; let rect = canvas.getBoundingClientRect(); x -= rect.left; y -= rect.top; x *= canvas.width / rect.width; y *= canvas.height / rect.height; if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) { return [-1, -1]; } return this._normalizedEventToPosition(x, y); } _initWebGL() { let gl = document.createElement("canvas").getContext("webgl2", { preserveDrawingBuffer: true }); window.gl = gl; let program = createProgram(gl, VS, FS); gl.useProgram(program); createQuad(gl); UNIFORMS.forEach(name => this._uniforms[name] = gl.getUniformLocation(program, name)); this._program = program; gl.enable(gl.BLEND); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.SCISSOR_TEST); return gl; } _normalizedEventToPosition(x, y) { return [Math.floor(x / this._options.tileWidth), Math.floor(y / this._options.tileHeight)]; } _updateSize() { const gl = this._gl; const opts = this._options; const canvasSize = [opts.width * opts.tileWidth, opts.height * opts.tileHeight]; gl.canvas.width = canvasSize[0]; gl.canvas.height = canvasSize[1]; gl.viewport(0, 0, canvasSize[0], canvasSize[1]); gl.uniform2fv(this._uniforms["tileSize"], [opts.tileWidth, opts.tileHeight]); gl.uniform2fv(this._uniforms["targetSize"], canvasSize); } _updateTexture(tileSet) { createTexture(this._gl, tileSet); } } const UNIFORMS = ["targetPosRel", "tilesetPosAbs", "tileSize", "targetSize", "colorize", "bg", "tint"]; const VS = ` #version 300 es in vec2 tilePosRel; out vec2 tilesetPosPx; uniform vec2 tilesetPosAbs; uniform vec2 tileSize; uniform vec2 targetSize; uniform vec2 targetPosRel; void main() { vec2 targetPosPx = (targetPosRel + tilePosRel) * tileSize; vec2 targetPosNdc = ((targetPosPx / targetSize)-0.5)*2.0; targetPosNdc.y *= -1.0; gl_Position = vec4(targetPosNdc, 0.0, 1.0); tilesetPosPx = tilesetPosAbs + tilePosRel * tileSize; }`.trim(); const FS = ` #version 300 es precision highp float; in vec2 tilesetPosPx; out vec4 fragColor; uniform sampler2D image; uniform bool colorize; uniform vec4 bg; uniform vec4 tint; void main() { fragColor = vec4(0, 0, 0, 1); vec4 texel = texelFetch(image, ivec2(tilesetPosPx), 0); if (colorize) { texel.rgb = tint.a * tint.rgb + (1.0-tint.a) * texel.rgb; fragColor.rgb = texel.a*texel.rgb + (1.0-texel.a)*bg.rgb; fragColor.a = texel.a + (1.0-texel.a)*bg.a; } else { fragColor = texel; } }`.trim(); function createProgram(gl, vss, fss) { const vs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vs, vss); gl.compileShader(vs); if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) { throw new Error(gl.getShaderInfoLog(vs) || ""); } const fs = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fs, fss); gl.compileShader(fs); if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) { throw new Error(gl.getShaderInfoLog(fs) || ""); } const p = gl.createProgram(); gl.attachShader(p, vs); gl.attachShader(p, fs); gl.linkProgram(p); if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { throw new Error(gl.getProgramInfoLog(p) || ""); } return p; } function createQuad(gl) { const pos = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, pos, gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); } function createTexture(gl, data) { 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); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); return t; } let colorCache = {}; function parseColor(color) { if (!(color in colorCache)) { let parsed; if (color == "transparent") { parsed = [0, 0, 0, 0]; } else if (color.indexOf("rgba") > -1) { parsed = (color.match(/[\d.]+/g) || []).map(Number); for (let i = 0; i < 3; i++) { parsed[i] = parsed[i] / 255; } } else { parsed = Color.fromString(color).map($ => $ / 255); parsed.push(1); } colorCache[color] = parsed; } return colorCache[color]; }