import { SmcErr } from "./errors.js"; import { SmcProgramBuilder } from "./progbuilder.js"; import { hexToRgba } from "./util.js"; export { Smc, UniformType }; const UniformType = { Float1: 0, Float2: 1, Float3: 2, Float4: 3, Int1: 4, Int2: 5, Int3: 6, Int4: 7, }; class Smc { #canvas; #gl; // Position array of a "full-screen" quad (encoded as TRIANGLE_STRIP) // Ref: https://en.wikipedia.org/wiki/Triangle_strip // NOTE: +x,+y is top-right & -x,-y is bottom-left #verticesFullscreen = [ -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, ]; #vertices = this.#verticesFullscreen; #attributes = new Map(); #uniforms = new Map(); #program = null; #clearBitFlags; #maxFps; #minDeltaTimeMs; // in milliseconds #prevTimeMs = 0; #errorDelegate = (_, error) => { throw error }; #initDelegate = (_) => { }; #resizeDelegate = (_) => { }; constructor(canvas) { this.raiseError = this.raiseError.bind(this); this.render = this.render.bind(this); this.renderLoop = this.renderLoop.bind(this); // DEBUG: is this necessary this.setAttribute = this.setAttribute.bind(this); this.setUniform = this.setUniform.bind(this); this.#canvas = canvas; this.#gl = Smc.#getWebGlContext(canvas); // NOTE: smc.isWebGlSupported() should be queried prior if (this.#gl == null) throw new Error("Unable to initialize WebGL. Your browser or machine may not support it."); // clear the entire depth buffer when this.#gl.clear is called this.#gl.clearDepth(1.0); this.#clearBitFlags = this.#gl.COLOR_BUFFER_BIT | this.#gl.DEPTH_BUFFER_BIT; // set WebGL's render context (number of pixels to draw) this.#gl.viewport(0, 0, this.#gl.canvas.width, this.#gl.canvas.height); // set defaults this.setMaxFps(30); this.setClearColor(0., 0., 0., 255.); } static #getWebGlContext(canvas) { try { return canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl"); } catch { return null; }; } static isWebGlSupported() { try { const canvas = document.createElement('canvas'); return !!window.WebGLRenderingContext && Smc.#getWebGlContext(canvas) != null; } catch (e) { return false; } } onError(delegate) { this.#errorDelegate = delegate; return this; } onInit(delegate) { this.#initDelegate = delegate; return this; } onResize(delegate) { this.#resizeDelegate = delegate; return this; } setClearColorHex(color) { color = hexToRgba(color); if (color == null) { // this.raiseError isn't needed because this should // be treated as a "compilation" error not a "runtime" error throw new Error(`setClearColorHex expects an RGB/RGBA hex value, got "${color}"`); } return this.setClearColor(color.r, color.g, color.b, color.a); } setClearColor(r, g, b, a) { this.#gl.clearColor(r / 255., g / 255., b / 255., a / 255.); return this; } setVertices(positions) { this.#vertices = positions; return this; } setMaxFps(fps) { this.#maxFps = fps; this.#minDeltaTimeMs = fps ? 1000 / fps : null; return this; } setProgram(delegate) { const builder = new SmcProgramBuilder(this.#gl, this.raiseError); var result = delegate(builder); this.#program = result.build(); if (!this.#gl.getProgramParameter(this.#program, this.#gl.LINK_STATUS)) { const infoLog = this.#gl.getProgramInfoLog(this.#program); this.raiseError( SmcErr.PROGRAM_INIT, new Error(`Unable to initialize the shader program: ${infoLog}`) ) } this.#addAttribute("aVertex", this.#setVerticesAttribute.bind(this)); // DEBUG: uncomment afterwards this.#addUniform("uResolution", UniformType.Float2, false, (_) => new Float32Array([this.#gl.canvas.width, this.#gl.canvas.height])); this.#addUniform("uTime", UniformType.Float1); this.#addUniform("uDelta", UniformType.Float1); return this; } run() { this.#initDelegate() this.setAttribute("aVertex", this.#vertices); this.setUniform("uResolution", this.#gl.canvas.width, this.#gl.canvas.height); if (this.#maxFps == 0) requestAnimationFrame(this.render) else requestAnimationFrame(this.renderLoop); } // requestAnimationFrame requests the browser to call the renderLoop // callback function before the next repaint. // `time` is the milliseconds elapsed since the page loaded. renderLoop(time) { var delta = time - this.#prevTimeMs; this.render(time, delta); setTimeout( () => requestAnimationFrame(this.renderLoop), Math.max(0, delta - this.#minDeltaTimeMs) ); this.#prevTimeMs = time; } render(time, delta) { this.setUniform("uTime", time * 0.001); this.setUniform("uDelta", delta); this.#gl.clear(this.#gl.COLOR_BUFFER_BIT | this.#gl.DEPTH_BUFFER_BIT); this.#gl.drawArrays(this.#gl.TRIANGLE_STRIP, 0, this.#vertices.length / 2); } #addAttribute(name, setDelegate, required = false) { var location = this.#gl.getAttribLocation(this.#program, name); if (location == -1) { if (required) { this.raiseError( SmcErr.ATTRIBUTE_MISSING, `Linked program missing required attribute: "${name}"` ); } location = null; } this.#attributes.set( name, { setDelegate: setDelegate, location: location, }); } #addUniform(name, type, setEachFrame, setCallback, required = false) { const location = this.#gl.getUniformLocation(this.#program, name); if (location == -1) { if (required) { this.raiseError( SmcErr.UNIFORM_MISSING, `Linked program missing required uniform: "${name}"` ) } location = null; } if (type == UniformType.Float1) var uniformfv = (...values) => this.#gl.uniform1f(location, ...values); else if (type == UniformType.Float2) var uniformfv = (...values) => this.#gl.uniform2f(location, ...values); else if (type == UniformType.Float3) var uniformfv = (...values) => this.#gl.uniform3f(location, ...values); else if (type == UniformType.Float4) var uniformfv = (...values) => this.#gl.uniform4f(location, ...values); else if (type == UniformType.Int1) var uniformfv = (...values) => this.#gl.uniform1i(location, ...values); else if (type == UniformType.Int2) var uniformfv = (...values) => this.#gl.uniform2i(location, ...values); else if (type == UniformType.Int3) var uniformfv = (...values) => this.#gl.uniform3i(location, ...values); else if (type == UniformType.Int4) var uniformfv = (...values) => this.#gl.uniform4i(location, ...values); else { // this.raiseError isn't needed because this should // be treated as a "compilation" error not a "runtime" error throw new Error(`Expected type from enum UniformType, but got "${type}"`); } // simplify function call to a single argument this.#uniforms.set( name, { setDelegate: uniformfv, location: location, setEachFrame: setEachFrame, setCallback: setCallback, } ); } #getAttributeLocation(name) { return this.#attributes.get(name).location; } #getUniformLocation(name) { return this.#uniforms.get(name).location; } setAttribute(name, ...args) { if (this.#getAttributeLocation(name) != null) this.#attributes.get(name).setDelegate(...args); } setUniform(name, ...args) { if (this.#getUniformLocation(name) != null) { this.#uniforms.get(name).setDelegate(...args); } } #setVerticesAttribute(vertices) { this.#vertices = vertices; const buffer = this.#gl.createBuffer(); this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, buffer); this.#gl.bufferData( this.#gl.ARRAY_BUFFER, new Float32Array(vertices), this.#gl.STATIC_DRAW ); this.#gl.vertexAttribPointer( this.#getAttributeLocation("aVertex"), 2, // (size) one vertex == 2 floats this.#gl.FLOAT, // (type) vertex positions given as 32bit floats false, // (normalized) don't normalize 0, // (stride) buffer offset pointer BETWEEN elements (0 => packed) 0, // (offset) buffer offset pointer from START to first element ) this.#gl.enableVertexAttribArray(this.#getAttributeLocation("aVertex")); return buffer; } raiseError(type, error) { this.#errorDelegate(type, error); } }