aboutsummaryrefslogtreecommitdiff
path: root/web/pw-visualizer/src/webgl
diff options
context:
space:
mode:
authorIlion Beyst <ilion.beyst@gmail.com>2021-12-29 21:24:57 +0100
committerIlion Beyst <ilion.beyst@gmail.com>2021-12-29 21:25:29 +0100
commit0c6d978442b244ca3f29c1ffdd44b5007ae7ad93 (patch)
treebaae5fa459a49ecd362e548e0649e2f58c669a70 /web/pw-visualizer/src/webgl
parent3eeaab6cec70e7a06a99a1ac2662974f71064bee (diff)
downloadplanetwars.dev-0c6d978442b244ca3f29c1ffdd44b5007ae7ad93.tar.xz
planetwars.dev-0c6d978442b244ca3f29c1ffdd44b5007ae7ad93.zip
separate out visualizer library
Diffstat (limited to 'web/pw-visualizer/src/webgl')
-rw-r--r--web/pw-visualizer/src/webgl/buffer.ts55
-rw-r--r--web/pw-visualizer/src/webgl/index.ts122
-rw-r--r--web/pw-visualizer/src/webgl/renderer.ts157
-rw-r--r--web/pw-visualizer/src/webgl/shader.ts327
-rw-r--r--web/pw-visualizer/src/webgl/text.ts192
-rw-r--r--web/pw-visualizer/src/webgl/texture.ts106
-rw-r--r--web/pw-visualizer/src/webgl/util.ts229
-rw-r--r--web/pw-visualizer/src/webgl/vertexBufferLayout.ts115
8 files changed, 1303 insertions, 0 deletions
diff --git a/web/pw-visualizer/src/webgl/buffer.ts b/web/pw-visualizer/src/webgl/buffer.ts
new file mode 100644
index 0000000..2739fbe
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/buffer.ts
@@ -0,0 +1,55 @@
+
+export class Buffer {
+ buffer: WebGLBuffer;
+ data: any;
+ count: number;
+ type: number;
+
+ constructor(gl: WebGLRenderingContext, data: number[], type: number) {
+ this.buffer = gl.createBuffer();
+ this.type = type;
+
+ if (data)
+ this.updateData(gl, data);
+ }
+
+ _toArray(data: number[]): any {
+ return new Float32Array(data);
+ }
+
+ updateData(gl: WebGLRenderingContext, data: number[]) {
+ this.data = data;
+ this.count = data.length;
+ gl.bindBuffer(this.type, this.buffer);
+ gl.bufferData(this.type, this._toArray(data), gl.STATIC_DRAW);
+ }
+
+ bind(gl: WebGLRenderingContext) {
+ gl.bindBuffer(this.type, this.buffer);
+ }
+
+ getCount(): number {
+ return this.count;
+ }
+}
+
+export class VertexBuffer extends Buffer {
+ constructor(gl: WebGLRenderingContext, data: any) {
+ super(gl, data, gl.ARRAY_BUFFER);
+ }
+
+ _toArray(data: number[]): any {
+ return new Float32Array(data);
+ }
+}
+
+
+export class IndexBuffer extends Buffer {
+ constructor(gl: WebGLRenderingContext, data: any) {
+ super(gl, data, gl.ELEMENT_ARRAY_BUFFER);
+ }
+
+ _toArray(data: number[]): any {
+ return new Uint16Array(data);
+ }
+}
diff --git a/web/pw-visualizer/src/webgl/index.ts b/web/pw-visualizer/src/webgl/index.ts
new file mode 100644
index 0000000..fdb7886
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/index.ts
@@ -0,0 +1,122 @@
+import { Uniform4f, Uniform1f, Uniform2f, ShaderFactory, UniformMatrix3fv, Uniform3f } from './shader';
+import { resizeCanvasToDisplaySize, FPSCounter, onload2promise, Resizer, url_to_mesh } from "./util";
+import { VertexBuffer, IndexBuffer } from './buffer';
+import { VertexArray, VertexBufferLayout } from './vertexBufferLayout';
+import { Renderer } from './renderer';
+import { Texture } from './texture';
+
+const URL = window.location.origin+window.location.pathname;
+const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1);
+
+async function create_texture_from_svg(gl: WebGLRenderingContext, name: string, path: string, width: number, height: number): Promise<Texture> {
+
+ const [mesh, factory] = await Promise.all([
+ url_to_mesh(path),
+ ShaderFactory.create_factory(LOCATION + "static/shaders/frag/static_color.glsl", LOCATION + "static/shaders/vert/svg.glsl")
+ ]);
+
+ const program = factory.create_shader(gl);
+ const renderer = new Renderer();
+
+ var positionBuffer = new VertexBuffer(gl, mesh.positions);
+ var layout = new VertexBufferLayout();
+ layout.push(gl.FLOAT, 3, 4, "a_position");
+
+ const vao = new VertexArray();
+ vao.addBuffer(positionBuffer, layout);
+
+ program.bind(gl);
+ vao.bind(gl, program);
+
+ var indexBuffer = new IndexBuffer(gl, mesh.cells);
+ indexBuffer.bind(gl);
+
+ renderer.addToDraw(indexBuffer, vao, program, {});
+
+ return Texture.fromRenderer(gl, name, width, height, renderer);
+}
+
+
+async function main() {
+
+ // Get A WebGL context
+ var canvas = <HTMLCanvasElement>document.getElementById("c");
+ const resolution = [canvas.width, canvas.height];
+
+ const resizer = new Resizer(canvas, [-10, -10, 20, 20], true);
+
+ var gl = canvas.getContext("webgl");
+ if (!gl) {
+ return;
+ }
+
+ const mesh = await url_to_mesh("static/res/images/earth.svg");
+ console.log(Math.max(...mesh.positions), Math.min(...mesh.positions));
+ const renderer = new Renderer();
+
+ const factory = await ShaderFactory.create_factory(LOCATION + "static/shaders/frag/static_color.glsl", LOCATION + "static/shaders/vert/simple.glsl");
+ const program = factory.create_shader(gl);
+
+ var positionBuffer = new VertexBuffer(gl, mesh.positions);
+ var layout = new VertexBufferLayout();
+ layout.push(gl.FLOAT, 3, 4, "a_position");
+ // layout.push(gl.FLOAT, 2, 4, "a_tex");
+
+ const vao = new VertexArray();
+ vao.addBuffer(positionBuffer, layout);
+
+ resizeCanvasToDisplaySize(<HTMLCanvasElement>gl.canvas);
+
+ // Tell WebGL how to convert from clip space to pixels
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
+
+ // Clear the canvas
+ gl.clearColor(0, 0, 0, 0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+
+ program.bind(gl);
+ vao.bind(gl, program);
+
+ var indexBuffer = new IndexBuffer(gl, mesh.cells);
+ indexBuffer.bind(gl);
+
+ renderer.addToDraw(indexBuffer, vao, program, {});
+
+ const counter = new FPSCounter();
+
+ const step = function (time: number) {
+
+ // console.log(resizer.get_viewbox());
+
+ program.uniform(gl, "u_time", new Uniform1f(time * 0.001));
+ program.uniform(gl, "u_mouse", new Uniform2f(resizer.get_mouse_pos()));
+ program.uniform(gl, "u_viewbox", new Uniform4f(resizer.get_viewbox()));
+ program.uniform(gl, "u_resolution", new Uniform2f(resolution));
+ program.uniform(gl, "u_trans", new UniformMatrix3fv([1, 0, 0, 0, 1, 0, 0, 0, 1]));
+ program.uniform(gl, "u_color", new Uniform3f(1.0, 0.5, 0.0));
+
+ renderer.render(gl);
+
+ counter.frame(time);
+ requestAnimationFrame(step);
+ }
+
+ requestAnimationFrame(step);
+}
+
+
+main();
+
+document.getElementById("loader").classList.remove("loading");
+
+// const loader = document.getElementById("loader");
+// setInterval(() => {
+// if (loader.classList.contains("loading")) {
+// loader.classList.remove("loading")
+// } else {
+// loader.classList.add("loading");
+// }
+// }, 2000);
diff --git a/web/pw-visualizer/src/webgl/renderer.ts b/web/pw-visualizer/src/webgl/renderer.ts
new file mode 100644
index 0000000..c3b219f
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/renderer.ts
@@ -0,0 +1,157 @@
+import type { IndexBuffer } from './buffer';
+import type { VertexArray } from './vertexBufferLayout';
+import type { Texture } from './texture';
+import type { Dictionary } from './util';
+import type { Shader, Uniform } from './shader';
+import { Uniform1i } from './shader';
+
+function sortedIndex(array, value) {
+ var low = 0,
+ high = array.length;
+
+ while (low < high) {
+ var mid = (low + high) >>> 1;
+ if (array[mid] < value) low = mid + 1;
+ else high = mid;
+ }
+ return low;
+}
+
+export interface Renderable {
+ getUniforms() : Dictionary<Uniform>;
+ render(gl: WebGLRenderingContext): void;
+ updateVAOBuffer(gl: WebGLRenderingContext, index: number, data: number[]);
+ updateIndexBuffer(gl: WebGLRenderingContext, data: number[]);
+}
+
+export class DefaultRenderable implements Renderable {
+ ibo: IndexBuffer;
+ va: VertexArray;
+ shader: Shader;
+ textures: Texture[];
+ uniforms: Dictionary<Uniform>;
+
+ constructor(
+ ibo: IndexBuffer,
+ va: VertexArray,
+ shader: Shader,
+ textures: Texture[],
+ uniforms: Dictionary<Uniform>,
+ ) {
+ this.ibo = ibo;
+ this.va = va;
+ this.shader = shader;
+ this.textures = textures;
+ this.uniforms = uniforms;
+ }
+
+ getUniforms(): Dictionary<Uniform> {
+ return this.uniforms;
+ }
+
+ updateVAOBuffer(gl: WebGLRenderingContext, index: number, data: number[]) {
+ this.va.updateBuffer(gl, index, data);
+ }
+
+ updateIndexBuffer(gl: WebGLRenderingContext, data: number[]) {
+ this.ibo.updateData(gl, data);
+ }
+
+ render(gl: WebGLRenderingContext): void {
+
+ const indexBuffer = this.ibo;
+ const vertexArray = this.va;
+ const uniforms = this.uniforms;
+
+ const shader = this.shader;
+ const textures = this.textures;
+ let texLocation = 0;
+
+ for (let texture of textures) {
+
+ shader.uniform(gl, texture.name, new Uniform1i(texLocation));
+ texture.bind(gl, texLocation);
+
+ texLocation ++;
+ // if (texLocation > maxTextures) {
+ // console.error("Using too many textures, this is not supported yet\nUndefined behaviour!");
+ // }
+ }
+
+ if (vertexArray && shader && uniforms) {
+ for(let key in uniforms) {
+ shader.uniform(gl, key, uniforms[key]);
+ }
+
+ vertexArray.bind(gl, shader);
+
+ if (indexBuffer) {
+ indexBuffer.bind(gl);
+ gl.drawElements(gl.TRIANGLES, indexBuffer.getCount(), gl.UNSIGNED_SHORT, 0);
+ } else {
+ console.error("IndexBuffer is required to render, for now");
+ }
+ }
+
+ }
+}
+
+export class Renderer {
+ renderables: { [id: number] : [Renderable, boolean][]; };
+ renderable_layers: number[];
+
+ constructor() {
+ this.renderables = {};
+ this.renderable_layers = [];
+ }
+
+ updateUniform(i: number, f: (uniforms: Dictionary<Uniform>) => void, layer=0, ) {
+ f(this.renderables[layer][i][0].getUniforms());
+ }
+
+ disableRenderable(i: number, layer=0) {
+ this.renderables[layer][i][1] = false;
+ }
+
+ enableRenderable(i: number, layer=0) {
+ this.renderables[layer][i][1] = true;
+ }
+
+ addRenderable(item: Renderable, layer=0): number {
+ if(!this.renderables[layer]) {
+ const idx = sortedIndex(this.renderable_layers, layer);
+ this.renderable_layers.splice(idx, 0, layer);
+ this.renderables[layer] = [];
+ }
+
+ this.renderables[layer].push([item, true]);
+ return this.renderables[layer].length - 1;
+ }
+
+ addToDraw(indexBuffer: IndexBuffer, vertexArray: VertexArray, shader: Shader, uniforms?: Dictionary<Uniform>, texture?: Texture[], layer=0): number {
+ return this.addRenderable(
+ new DefaultRenderable(
+ indexBuffer,
+ vertexArray,
+ shader,
+ texture || [],
+ uniforms || {},
+ ), layer
+ );
+ }
+
+ render(gl: WebGLRenderingContext, frameBuffer?: WebGLFramebuffer, width?: number, height?: number) {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
+ gl.viewport(0, 0, width || gl.canvas.width, height || gl.canvas.height);
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+ const maxTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
+
+ for (let layer of this.renderable_layers) {
+ for (let [r, e] of this.renderables[layer]) {
+ if (!e) continue;
+ r.render(gl);
+ }
+ }
+ }
+}
diff --git a/web/pw-visualizer/src/webgl/shader.ts b/web/pw-visualizer/src/webgl/shader.ts
new file mode 100644
index 0000000..942c4c2
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/shader.ts
@@ -0,0 +1,327 @@
+import type { Dictionary } from './util';
+
+function error(msg: string) {
+ console.error(msg);
+}
+
+const defaultShaderType = [
+ "VERTEX_SHADER",
+ "FRAGMENT_SHADER"
+];
+
+/// Create Shader from Source string
+function loadShader(
+ gl: WebGLRenderingContext,
+ shaderSource: string,
+ shaderType: number,
+ opt_errorCallback: any,
+): WebGLShader {
+ var errFn = opt_errorCallback || error;
+ // Create the shader object
+ var shader = gl.createShader(shaderType);
+
+ // Load the shader source
+ gl.shaderSource(shader, shaderSource);
+
+ // Compile the shader
+ gl.compileShader(shader);
+
+ // Check the compile status
+ var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+ if (!compiled) {
+ // Something went wrong during compilation; get the error
+ var lastError = gl.getShaderInfoLog(shader);
+ errFn("*** Error compiling shader '" + shader + "':" + lastError);
+ gl.deleteShader(shader);
+ return null;
+ }
+
+ return shader;
+}
+
+/// Actually Create Program with Shader's
+function createProgram(
+ gl: WebGLRenderingContext,
+ shaders: WebGLShader[],
+ opt_attribs: string[],
+ opt_locations: number[],
+ opt_errorCallback: any,
+): WebGLProgram {
+ var errFn = opt_errorCallback || error;
+ var program = gl.createProgram();
+ shaders.forEach(function (shader) {
+ gl.attachShader(program, shader);
+ });
+ if (opt_attribs) {
+ opt_attribs.forEach(function (attrib, ndx) {
+ gl.bindAttribLocation(
+ program,
+ opt_locations ? opt_locations[ndx] : ndx,
+ attrib);
+ });
+ }
+ gl.linkProgram(program);
+
+ // Check the link status
+ var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
+ if (!linked) {
+ // something went wrong with the link
+ var lastError = gl.getProgramInfoLog(program);
+ errFn("Error in program linking:" + lastError);
+
+ gl.deleteProgram(program);
+ return null;
+ }
+ return program;
+}
+
+export class ShaderFactory {
+ frag_source: string;
+ vert_source: string;
+
+ static async create_factory(frag_url: string, vert_url: string): Promise<ShaderFactory> {
+ const sources = await Promise.all([
+ fetch(frag_url).then((r) => r.text()),
+ fetch(vert_url).then((r) => r.text()),
+ ]);
+
+ return new ShaderFactory(sources[0], sources[1]);
+ }
+
+ constructor(frag_source: string, vert_source: string ) {
+ this.frag_source = frag_source;
+ this.vert_source = vert_source;
+ }
+
+ create_shader(
+ gl: WebGLRenderingContext,
+ context?: Dictionary<string>,
+ opt_attribs?: string[],
+ opt_locations?: number[],
+ opt_errorCallback?: any,
+ ): Shader {
+ let vert = this.vert_source.slice();
+ let frag = this.frag_source.slice();
+ for (let key in context) {
+ vert = vert.replace(new RegExp("\\$" + key, 'g'), context[key]);
+ frag = frag.replace(new RegExp("\\$" + key, 'g'), context[key]);
+ }
+
+ const shaders = [
+ loadShader(gl, vert, gl.VERTEX_SHADER, opt_errorCallback),
+ loadShader(gl, frag, gl.FRAGMENT_SHADER, opt_errorCallback),
+ ];
+
+ return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback));
+ }
+}
+
+export class Shader {
+ shader: WebGLProgram;
+ uniformCache: Dictionary<WebGLUniformLocation>;
+ attribCache: Dictionary<number>;
+
+ static async createProgramFromUrls(
+ gl: WebGLRenderingContext,
+ vert_url: string,
+ frag_url: string,
+ context?: Dictionary<string>,
+ opt_attribs?: string[],
+ opt_locations?: number[],
+ opt_errorCallback?: any,
+ ): Promise<Shader> {
+ const sources = (await Promise.all([
+ fetch(vert_url).then((r) => r.text()),
+ fetch(frag_url).then((r) => r.text()),
+ ])).map(x => {
+ for (let key in context) {
+ x = x.replace(new RegExp("\\$" + key, 'g'), context[key]);
+ }
+ return x;
+ });
+
+ const shaders = [
+ loadShader(gl, sources[0], 35633, opt_errorCallback),
+ loadShader(gl, sources[1], 35632, opt_errorCallback),
+ ];
+ return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback));
+ }
+
+ constructor(shader: WebGLProgram) {
+ this.shader = shader;
+ this.uniformCache = {};
+ this.attribCache = {};
+ }
+
+ bind(gl: WebGLRenderingContext) {
+ gl.useProgram(this.shader);
+ }
+
+ // Different locations have different types :/
+ getUniformLocation(gl: WebGLRenderingContext, name: string): WebGLUniformLocation {
+ if (this.uniformCache[name] === undefined) {
+ this.uniformCache[name] = gl.getUniformLocation(this.shader, name);
+ }
+
+ return this.uniformCache[name];
+ }
+
+ getAttribLocation(gl: WebGLRenderingContext, name: string): number {
+ if (this.attribCache[name] === undefined) {
+ this.attribCache[name] = gl.getAttribLocation(this.shader, name);
+ }
+
+ return this.attribCache[name];
+ }
+
+ uniform<T extends Uniform>(
+ gl: WebGLRenderingContext,
+ name: string,
+ uniform: T,
+ ) {
+ this.bind(gl);
+ const location = this.getUniformLocation(gl, name);
+ if (location < 0) {
+ console.error("No location found with name " + name);
+ }
+
+ uniform.setUniform(gl, location);
+ }
+
+ clear(gl: WebGLRenderingContext) {
+ gl.deleteProgram(this.shader);
+ }
+}
+
+export interface Uniform {
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation): void;
+}
+
+export class Uniform2fv implements Uniform {
+ data: number[] | Float32Array;
+ constructor(data: number[] | Float32Array) {
+ this.data = data;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform2fv(location, this.data);
+ }
+}
+
+export class Uniform3fv implements Uniform {
+ data: number[] | Float32Array;
+ constructor(data: number[] | Float32Array) {
+ this.data = data;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform3fv(location, this.data);
+ }
+}
+
+export class Uniform3f implements Uniform {
+ x: number;
+ y: number;
+ z: number;
+
+ constructor(x: number, y: number, z: number) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform3f(location, this.x ,this.y, this.z);
+ }
+}
+
+export class Uniform1iv implements Uniform {
+ data: number[] | Int32List;
+ constructor(data: number[] | Int32List) {
+ this.data = data;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform1iv(location, this.data);
+ }
+}
+
+export class Uniform1i implements Uniform {
+ texture: number;
+
+ constructor(texture: number) {
+ this.texture = texture;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform1i(location, this.texture);
+ }
+}
+
+export class Uniform1f implements Uniform {
+ texture: number;
+
+ constructor(texture: number) {
+ this.texture = texture;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform1f(location, this.texture);
+ }
+}
+
+export class Uniform2f implements Uniform {
+ x: number;
+ y: number;
+
+ constructor(xy: number[]) {
+ this.x = xy[0];
+ this.y = xy[1];
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform2f(location, this.x, this.y);
+ }
+}
+
+export class Uniform4f implements Uniform {
+ v0: number;
+ v1: number;
+ v2: number;
+ v3: number;
+
+ constructor(xyzw: number[]) {
+ this.v0 = xyzw[0];
+ this.v1 = xyzw[1];
+ this.v2 = xyzw[2];
+ this.v3 = xyzw[3];
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform4f(location, this.v0, this.v1, this.v2, this.v3);
+ }
+}
+
+export class UniformMatrix3fv implements Uniform {
+ data: number[] | Float32Array;
+ constructor(data: number[] | Float32Array) {
+ this.data = data;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniformMatrix3fv(location, false, this.data);
+ }
+}
+
+export class UniformBool implements Uniform {
+ data: boolean;
+ constructor(data: boolean) {
+ this.data = data;
+ }
+
+ setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) {
+ gl.uniform1i(location, this.data ? 1 : 0);
+ }
+}
+
+export default Shader; \ No newline at end of file
diff --git a/web/pw-visualizer/src/webgl/text.ts b/web/pw-visualizer/src/webgl/text.ts
new file mode 100644
index 0000000..3f1cec6
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/text.ts
@@ -0,0 +1,192 @@
+import type { Dictionary } from "./util";
+import type { Shader, UniformMatrix3fv } from "./shader";
+import { Texture } from "./texture";
+import { DefaultRenderable } from "./renderer";
+import { IndexBuffer, VertexBuffer } from "./buffer";
+import { VertexBufferLayout, VertexArray } from "./vertexBufferLayout";
+
+
+export enum Align {
+ Begin,
+ End,
+ Middle,
+}
+
+export class GlypInfo {
+ x: number;
+ y: number;
+ width: number;
+}
+
+export class FontInfo {
+ letterHeight: number;
+ spaceWidth: number;
+ spacing: number;
+ textureWidth: number;
+ textureHeight: number;
+ glyphInfos: Dictionary<GlypInfo>;
+}
+
+export class LabelFactory {
+ texture: Texture;
+ font: FontInfo;
+ shader: Shader;
+
+ constructor(gl: WebGLRenderingContext, loc: string, font: FontInfo, shader: Shader) {
+ this.texture = Texture.fromImage(gl, loc, 'font');
+ this.font = font;
+ this.shader = shader;
+ }
+
+ build(gl: WebGLRenderingContext, transform?: UniformMatrix3fv): Label {
+ return new Label(gl, this.shader, this.texture, this.font, transform);
+ }
+}
+
+export class Label {
+ inner: DefaultRenderable;
+
+ font: FontInfo;
+
+ constructor(gl: WebGLRenderingContext, shader: Shader, tex: Texture, font: FontInfo, transform?: UniformMatrix3fv) {
+ this.font = font;
+
+ const uniforms = transform ? { "u_trans": transform, "u_trans_next": transform, } : {};
+ const ib = new IndexBuffer(gl, []);
+ const vb_pos = new VertexBuffer(gl, []);
+ const vb_tex = new VertexBuffer(gl, []);
+
+ const layout_pos = new VertexBufferLayout();
+ layout_pos.push(gl.FLOAT, 2, 4, "a_position");
+
+ const layout_tex = new VertexBufferLayout();
+ layout_tex.push(gl.FLOAT, 2, 4, "a_texCoord");
+
+ const vao = new VertexArray();
+ vao.addBuffer(vb_pos, layout_pos);
+ vao.addBuffer(vb_tex, layout_tex);
+
+ this.inner = new DefaultRenderable(ib, vao, shader, [tex], uniforms);
+ }
+
+ getRenderable(): DefaultRenderable {
+ return this.inner;
+ }
+
+ setText(gl: WebGLRenderingContext, text: string, h_align = Align.Begin, v_align = Align.Begin) {
+ const idxs = [];
+ const verts_pos = [];
+ const verts_tex = [];
+
+ const letterHeight = this.font.letterHeight / this.font.textureHeight;
+ let xPos = 0;
+
+ switch (h_align) {
+ case Align.Begin:
+ break;
+ case Align.End:
+ xPos = -1 * [...text].map(n => this.font.glyphInfos[n] ? this.font.glyphInfos[n].width : this.font.spaceWidth).reduce((a, b) => a + b, 0) / this.font.letterHeight;
+ break;
+ case Align.Middle:
+ xPos = -1 * [...text].map(n => this.font.glyphInfos[n] ? this.font.glyphInfos[n].width : this.font.spaceWidth).reduce((a, b) => a + b, 0) / this.font.letterHeight / 2;
+ break;
+ }
+ let yStart = 0;
+ switch (v_align) {
+ case Align.Begin:
+ break;
+ case Align.End:
+ yStart = 1;
+ break;
+ case Align.Middle:
+ yStart = 0.5;
+ break;
+ }
+
+ let j = 0;
+ for (let i = 0; i < text.length; i++) {
+ const info = this.font.glyphInfos[text[i]];
+ if (info) {
+ const dx = info.width / this.font.letterHeight;
+ const letterWidth = info.width / this.font.textureWidth;
+ const x0 = info.x / this.font.textureWidth;
+ const y0 = info.y / this.font.textureHeight;
+ verts_pos.push(xPos, yStart);
+ verts_pos.push(xPos + dx, yStart);
+ verts_pos.push(xPos, yStart-1);
+ verts_pos.push(xPos + dx, yStart-1);
+
+ verts_tex.push(x0, y0);
+ verts_tex.push(x0 + letterWidth, y0);
+ verts_tex.push(x0, y0 + letterHeight);
+ verts_tex.push(x0 + letterWidth, y0 + letterHeight);
+
+ xPos += dx;
+
+ idxs.push(j+0, j+1, j+2, j+1, j+2, j+3);
+ j += 4;
+ } else {
+ // Just move xPos
+ xPos += this.font.spaceWidth / this.font.letterHeight;
+ }
+ }
+
+ this.inner.updateIndexBuffer(gl, idxs);
+ this.inner.updateVAOBuffer(gl, 0, verts_pos);
+ this.inner.updateVAOBuffer(gl, 1, verts_tex);
+ }
+}
+
+export function defaultLabelFactory(gl: WebGLRenderingContext, shader: Shader): LabelFactory {
+ const fontInfo = {
+ letterHeight: 8,
+ spaceWidth: 8,
+ spacing: -1,
+ textureWidth: 64,
+ textureHeight: 40,
+ glyphInfos: {
+ 'a': { x: 0, y: 0, width: 8, },
+ 'b': { x: 8, y: 0, width: 8, },
+ 'c': { x: 16, y: 0, width: 8, },
+ 'd': { x: 24, y: 0, width: 8, },
+ 'e': { x: 32, y: 0, width: 8, },
+ 'f': { x: 40, y: 0, width: 8, },
+ 'g': { x: 48, y: 0, width: 8, },
+ 'h': { x: 56, y: 0, width: 8, },
+ 'i': { x: 0, y: 8, width: 8, },
+ 'j': { x: 8, y: 8, width: 8, },
+ 'k': { x: 16, y: 8, width: 8, },
+ 'l': { x: 24, y: 8, width: 8, },
+ 'm': { x: 32, y: 8, width: 8, },
+ 'n': { x: 40, y: 8, width: 8, },
+ 'o': { x: 48, y: 8, width: 8, },
+ 'p': { x: 56, y: 8, width: 8, },
+ 'q': { x: 0, y: 16, width: 8, },
+ 'r': { x: 8, y: 16, width: 8, },
+ 's': { x: 16, y: 16, width: 8, },
+ 't': { x: 24, y: 16, width: 8, },
+ 'u': { x: 32, y: 16, width: 8, },
+ 'v': { x: 40, y: 16, width: 8, },
+ 'w': { x: 48, y: 16, width: 8, },
+ 'x': { x: 56, y: 16, width: 8, },
+ 'y': { x: 0, y: 24, width: 8, },
+ 'z': { x: 8, y: 24, width: 8, },
+ '0': { x: 16, y: 24, width: 8, },
+ '1': { x: 24, y: 24, width: 8, },
+ '2': { x: 32, y: 24, width: 8, },
+ '3': { x: 40, y: 24, width: 8, },
+ '4': { x: 48, y: 24, width: 8, },
+ '5': { x: 56, y: 24, width: 8, },
+ '6': { x: 0, y: 32, width: 8, },
+ '7': { x: 8, y: 32, width: 8, },
+ '8': { x: 16, y: 32, width: 8, },
+ '9': { x: 24, y: 32, width: 8, },
+ '-': { x: 32, y: 32, width: 8, },
+ '*': { x: 40, y: 32, width: 8, },
+ '!': { x: 48, y: 32, width: 8, },
+ '?': { x: 56, y: 32, width: 8, },
+ },
+ };
+
+ return new LabelFactory(gl, '/static/res/assets/font.png', fontInfo, shader);
+}
diff --git a/web/pw-visualizer/src/webgl/texture.ts b/web/pw-visualizer/src/webgl/texture.ts
new file mode 100644
index 0000000..9d6adcf
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/texture.ts
@@ -0,0 +1,106 @@
+import type { Renderer } from "./renderer";
+
+export class Texture {
+ texture: WebGLTexture;
+ width: number;
+ height: number;
+ loaded: boolean;
+ name: string;
+
+ static fromImage(
+ gl: WebGLRenderingContext,
+ path: string,
+ name: string,
+ ): Texture {
+ const out = new Texture(gl, name);
+
+ const image = new Image();
+ image.onload = out.setImage.bind(out, gl, image);
+ image.onerror = error;
+ image.src = path;
+
+ return out;
+ }
+
+ static fromRenderer(
+ gl: WebGLRenderingContext,
+ name: string,
+ width: number,
+ height: number,
+ renderer: Renderer
+ ): Texture {
+ const out = new Texture(gl, name);
+ out.width = width;
+ out.height = height;
+
+ gl.texImage2D(
+ gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
+ gl.RGBA, gl.UNSIGNED_BYTE, null);
+
+ const fb = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+
+ const attachmentPoint = gl.COLOR_ATTACHMENT0;
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, out.texture, 0);
+
+ renderer.render(gl, fb, width, height);
+
+ out.loaded = true;
+
+ return out;
+ }
+
+ constructor(
+ gl: WebGLRenderingContext,
+ name: string,
+ ) {
+ this.loaded = false;
+ this.name = name;
+
+ this.texture = gl.createTexture();
+ this.bind(gl);
+
+ 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);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA,
+ gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
+ }
+
+ setImage(gl: WebGLRenderingContext, image: HTMLImageElement) {
+ this.bind(gl);
+ this.width = image.width;
+ this.height = image.height;
+
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+
+ this.unbind(gl);
+
+ this.loaded = true;
+ }
+
+ bind(gl: WebGLRenderingContext, location=0) {
+ gl.activeTexture(gl.TEXTURE0 + location);
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
+ }
+
+ unbind(gl: WebGLRenderingContext) {
+ gl.bindTexture(gl.TEXTURE_2D, null);
+ }
+
+
+ getWidth(): number {
+ return this.width;
+ }
+
+ getHeight(): number {
+ return this.height;
+ }
+}
+
+function error(e: any) {
+ console.error("IMAGE LOAD ERROR");
+ console.error(e);
+}
diff --git a/web/pw-visualizer/src/webgl/util.ts b/web/pw-visualizer/src/webgl/util.ts
new file mode 100644
index 0000000..3ed2b4d
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/util.ts
@@ -0,0 +1,229 @@
+import { parse as parsePath } from 'extract-svg-path';
+import svgMesh3d from 'svg-mesh-3d';
+
+export interface Dictionary<T> {
+ [Key: string]: T;
+}
+
+
+interface OnLoadable {
+ onload: any;
+}
+
+export function onload2promise<T extends OnLoadable>(obj: T): Promise<T> {
+ return new Promise(resolve => {
+ obj.onload = () => resolve(obj);
+ });
+}
+
+export function resizeCanvasToDisplaySize(
+ canvas: HTMLCanvasElement,
+ multiplier?: number,
+): boolean {
+ multiplier = multiplier || 1;
+ var width = canvas.clientWidth * multiplier | 0;
+ var height = canvas.clientHeight * multiplier | 0;
+ if (canvas.width !== width || canvas.height !== height) {
+ canvas.width = width;
+ canvas.height = height;
+ return true;
+ }
+ return false;
+}
+
+export class FPSCounter {
+ last: number;
+ count: number;
+ _delta: number;
+ _prev: number;
+
+ _frame_start: number;
+ _total_frametime: number;
+
+ constructor() {
+ this.last = 0;
+ this.count = 0;
+ this._delta = 0;
+ this._prev = 0;
+ }
+
+ frame(now: number) {
+ this._frame_start = performance.now();
+ this.count += 1;
+ this._delta = now - this._prev;
+ this._prev = now;
+
+ if (now - this.last > 1000) {
+ this.last = now;
+ console.log(`${this.count} fps, ${(this._total_frametime / this.count).toFixed(2)}ms avg per frame`);
+ this.count = 0;
+ this._total_frametime = 0;
+ }
+ }
+
+ frame_end() {
+ this._total_frametime += (performance.now() - this._frame_start);
+ }
+
+ delta(now: number): number {
+ return this._delta;
+ }
+}
+
+export class Resizer {
+ hoovering = false;
+ dragging = false;
+
+ mouse_pos = [0, 0];
+ last_drag = [0, 0];
+
+ viewbox: number[];
+ orig_viewbox: number[];
+
+ el_box: number[];
+
+ scaleX = 1;
+ scaleY = 1;
+
+ constructor(el: HTMLCanvasElement, viewbox: number[], keep_aspect_ratio=false) {
+ viewbox = [-viewbox[0] - viewbox[2], - viewbox[1] - viewbox[3], viewbox[2], viewbox[3]];
+ this.viewbox = [...viewbox];
+ this.el_box = [el.width, el.height];
+
+ if (keep_aspect_ratio) {
+ const or_width = this.viewbox[2];
+ const or_height = this.viewbox[3];
+
+ const width_percentage = this.viewbox[2] / el.width;
+ const height_percentage = this.viewbox[3] / el.height;
+
+ if (width_percentage < height_percentage) {
+ // width should be larger
+ this.viewbox[2] = height_percentage * el.width;
+ } else {
+ // height should be larger
+ this.viewbox[3] = width_percentage * el.height;
+ }
+
+ this.viewbox[0] -= (this.viewbox[2] - or_width) / 2;
+ this.viewbox[1] -= (this.viewbox[3] - or_height) / 2;
+
+ this.scaleX = this.viewbox[2] / this.viewbox[3];
+ }
+
+ this.orig_viewbox = [...this.viewbox];
+
+ el.addEventListener("mouseenter", this.mouseenter.bind(this), { capture: false, passive: true});
+ el.addEventListener("mouseleave", this.mouseleave.bind(this), { capture: false, passive: true});
+ el.addEventListener("mousemove", this.mousemove.bind(this), { capture: false, passive: true});
+ el.addEventListener("mousedown", this.mousedown.bind(this), { capture: false, passive: true});
+ el.addEventListener("mouseup", this.mouseup.bind(this), { capture: false, passive: true});
+
+ window.addEventListener('wheel', this.wheel.bind(this), { capture: false, passive: true});
+ }
+
+ _clip_viewbox() {
+ this.viewbox[0] = Math.max(this.viewbox[0], this.orig_viewbox[0]);
+ this.viewbox[1] = Math.max(this.viewbox[1], this.orig_viewbox[1]);
+
+ this.viewbox[0] = Math.min(this.viewbox[0] + this.viewbox[2], this.orig_viewbox[0] + this.orig_viewbox[2]) - this.viewbox[2];
+ this.viewbox[1] = Math.min(this.viewbox[1] + this.viewbox[3], this.orig_viewbox[1] + this.orig_viewbox[3]) - this.viewbox[3];
+ }
+
+ mouseenter() {
+ this.hoovering = true;
+ }
+
+ mouseleave() {
+ this.hoovering = false;
+ }
+
+ mousemove(e: MouseEvent) {
+ this.mouse_pos = [e.offsetX, this.el_box[1] - e.offsetY];
+
+ if (this.dragging) {
+ const scaleX = this.viewbox[2] / this.el_box[0];
+ const scaleY = this.viewbox[3] / this.el_box[1];
+
+ this.viewbox[0] += (this.last_drag[0] - this.mouse_pos[0]) * scaleX;
+ this.viewbox[1] += (this.last_drag[1] - this.mouse_pos[1]) * scaleY;
+
+ this.last_drag = [...this.mouse_pos];
+
+ this._clip_viewbox();
+ }
+ }
+
+ mousedown() {
+ this.dragging = true;
+ this.last_drag = [...this.mouse_pos];
+ }
+
+ mouseup() {
+ this.dragging = false;
+ }
+
+ wheel(e: WheelEvent) {
+ if (this.hoovering) {
+ const delta = e.deltaY > 0 ? 0.1 * this.viewbox[2] : -0.1 * this.viewbox[2];
+ const dx = delta * this.scaleX;
+ const dy = delta * this.scaleY;
+
+ const mouse_dx = this.mouse_pos[0] / this.el_box[0];
+ const mouse_dy = this.mouse_pos[1] / this.el_box[1];
+
+ this._zoom([dx, dy], [mouse_dx, mouse_dy]);
+ }
+ }
+
+ _zoom(deltas: number[], center: number[]) {
+ this.viewbox[2] += deltas[0];
+ this.viewbox[0] -= deltas[0] * center[0];
+ this.viewbox[2] = Math.min(this.viewbox[2], this.orig_viewbox[2]);
+
+ this.viewbox[3] += deltas[1];
+ this.viewbox[1] -= deltas[1] * center[1];
+ this.viewbox[3] = Math.min(this.viewbox[3], this.orig_viewbox[3]);
+
+ this._clip_viewbox();
+ }
+
+ get_viewbox(): number[] {
+ return this.viewbox;
+ }
+
+ get_mouse_pos(): number[] {
+ return this.mouse_pos;
+ }
+}
+
+export class Mesh {
+ cells: number[];
+ positions: number[];
+
+ constructor(mesh: any) {
+ this.cells = mesh.cells.flat();
+ this.positions = mesh.positions.flat();
+ }
+}
+
+export async function url_to_mesh(url: string): Promise<Mesh> {
+
+ return new Promise(function(resolve) {
+ fetch(url)
+ .then(resp => resp.text())
+ .then(data => {
+ // var div = document.createElement('div');
+ // div.innerHTML = data;
+ // var svg = div.querySelector('svg');
+
+ var svgPath = parsePath(data);
+ var mesh = svgMesh3d(svgPath, {
+ delaunay: false,
+ scale: 10,
+ });
+
+ resolve(new Mesh(mesh));
+ })
+ });
+}
diff --git a/web/pw-visualizer/src/webgl/vertexBufferLayout.ts b/web/pw-visualizer/src/webgl/vertexBufferLayout.ts
new file mode 100644
index 0000000..f44ed47
--- /dev/null
+++ b/web/pw-visualizer/src/webgl/vertexBufferLayout.ts
@@ -0,0 +1,115 @@
+import type { VertexBuffer } from './buffer';
+import type { Shader } from './shader';
+
+export class VertexBufferElement {
+ type: number;
+ amount: number;
+ type_size: number;
+ normalized: boolean;
+ index: string;
+
+ constructor(
+ type: number,
+ amount: number,
+ type_size: number,
+ index: string,
+ normalized: boolean,
+ ) {
+ this.type = type;
+ this.amount = amount;
+ this.type_size = type_size;
+ this.normalized = normalized;
+ this.index = index;
+ }
+}
+
+export class VertexBufferLayout {
+ elements: VertexBufferElement[];
+ stride: number;
+ offset: number;
+
+ constructor(offset = 0) {
+ this.elements = [];
+ this.stride = 0;
+ this.offset = offset;
+ }
+
+ // Maybe wrong normalized type
+ push(
+ type: number,
+ amount: number,
+ type_size: number,
+ index: string,
+ normalized = false,
+ ) {
+ this.elements.push(new VertexBufferElement(type, amount, type_size, index, normalized));
+ this.stride += amount * type_size;
+ }
+
+ getElements(): VertexBufferElement[] {
+ return this.elements;
+ }
+
+ getStride(): number {
+ return this.stride;
+ }
+}
+
+// glEnableVertexAttribArray is to specify what location of the current program the follow data is needed
+// glVertexAttribPointer tells gl that that data is at which location in the supplied data
+export class VertexArray {
+ // There is no renderer ID, always at bind buffers and use glVertexAttribPointer
+ buffers: VertexBuffer[];
+ layouts: VertexBufferLayout[];
+
+ constructor() {
+ this.buffers = [];
+ this.layouts = [];
+ }
+
+ addBuffer(vb: VertexBuffer, layout: VertexBufferLayout) {
+ this.buffers.push(vb);
+ this.layouts.push(layout);
+ }
+
+ updateBuffer(gl: WebGLRenderingContext, index: number, data: number[]) {
+ this.buffers[index].updateData(gl, data);
+ }
+
+ /// Bind buffers providing program data
+ bind(gl: WebGLRenderingContext, shader: Shader) {
+ shader.bind(gl);
+ for(let i = 0; i < this.buffers.length; i ++) {
+ const buffer = this.buffers[i];
+ const layout = this.layouts[i];
+
+ buffer.bind(gl);
+ const elements = layout.getElements();
+ let offset = layout.offset;
+
+ for (let j = 0; j < elements.length; j ++) {
+ const element = elements[j];
+ const location = shader.getAttribLocation(gl, element.index);
+
+ if (location >= 0) {
+ gl.enableVertexAttribArray(location);
+ gl.vertexAttribPointer(
+ location, element.amount, element.type,
+ element.normalized, layout.stride, offset
+ );
+ }
+
+ offset += element.amount * element.type_size;
+ }
+ }
+ }
+
+ /// Undo bind operation
+ unbind(gl: WebGLRenderingContext) {
+ this.layouts.forEach((layout) => {
+ layout.getElements().forEach((_, index) => {
+ gl.disableVertexAttribArray(index);
+ });
+ })
+ }
+}