From aa97ec883796428d89b6edcdca093401829d4005 Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Tue, 17 Sep 2019 20:19:04 +0200 Subject: [PATCH] wtf --- frontend/www/index.html | 5 +- frontend/www/index.js | 4 +- frontend/www/index.ts | 109 +- frontend/www/static/res/style.css | 57 + frontend/www/static/shaders/frag/simple.glsl | 27 + frontend/www/static/shaders/vert/simple.glsl | 22 + frontend/www/webgl/index.ts | 115 ++ frontend/www/webgl/shader.ts | 129 +- frontend/www/webgl/util.ts | 179 ++- frontend/www/webgl/webgl-utils.d.ts | 20 + frontend/www/webgl/webgl-utils.js | 1293 ++++++++++++++++++ 11 files changed, 1822 insertions(+), 138 deletions(-) create mode 100644 frontend/www/static/res/style.css create mode 100644 frontend/www/static/shaders/frag/simple.glsl create mode 100644 frontend/www/static/shaders/vert/simple.glsl create mode 100644 frontend/www/webgl/index.ts create mode 100644 frontend/www/webgl/webgl-utils.d.ts create mode 100644 frontend/www/webgl/webgl-utils.js diff --git a/frontend/www/index.html b/frontend/www/index.html index fef4ea8..2ea1b5c 100644 --- a/frontend/www/index.html +++ b/frontend/www/index.html @@ -3,9 +3,12 @@ Hello wasm-pack! + - +
+ +
diff --git a/frontend/www/index.js b/frontend/www/index.js index a4ce02d..60f4cb7 100644 --- a/frontend/www/index.js +++ b/frontend/www/index.js @@ -1,7 +1,7 @@ import { Game } from "planetwars"; import { Shader } from "./webgl/shader" -import { main } from './index.ts' +import { set_instance } from './index.ts' const URL = window.location.origin+window.location.pathname; const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); @@ -11,5 +11,5 @@ const game_location = LOCATION + "static/game.json"; fetch(game_location) .then((r) => r.text()) .then((response) => { - main(Game.new(response)); + set_instance(Game.new(response)); }).catch(console.error); diff --git a/frontend/www/index.ts b/frontend/www/index.ts index dec7c89..57562af 100644 --- a/frontend/www/index.ts +++ b/frontend/www/index.ts @@ -1,15 +1,122 @@ import { Game } from "planetwars"; import { memory } from "planetwars/plantwars_bg"; +import { Resizer, resizeCanvasToDisplaySize, FPSCounter } from "./webgl/util"; +import { Shader, Uniform4f, Uniform2fv, Uniform3fv, Uniform1i, Uniform1f, Uniform2f, ShaderFactory } from './webgl/shader'; +import { Renderer } from "./webgl/renderer"; +import { VertexBuffer, IndexBuffer } from "./webgl/buffer"; +import { VertexBufferLayout, VertexArray } from "./webgl/vertexBufferLayout"; +import { callbackify } from "util"; +const COUNTER = new FPSCounter(); +const LOADER = document.getElementById("loader"); + +function set_loading(loading: boolean) { + if (loading) { + if (!LOADER.classList.contains("loading")) { + LOADER.classList.add("loading"); + } + } else { + LOADER.classList.remove("loading"); + } +} + +const URL = window.location.origin+window.location.pathname; +const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); const CANVAS = document.getElementById("c"); +const RESOLUTION = [CANVAS.width, CANVAS.height]; + +const GL = CANVAS.getContext("webgl"); +resizeCanvasToDisplaySize(GL.canvas); +GL.viewport(0, 0, GL.canvas.width, GL.canvas.height); + +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); + +const positionBuffer = new VertexBuffer(GL, [ + -1, -1, + -1, 1, + 1, -1, + 1, 1, +]); + +const layout = new VertexBufferLayout(); +layout.push(GL.FLOAT, 2, 4, "a_position"); +const vao = new VertexArray(); +vao.addBuffer(positionBuffer, layout); + +const indexBuffer = new IndexBuffer(GL, [ + 0, 1, 2, + 1, 2, 3, +]); + +var SHADERFACOTRY: ShaderFactory; +ShaderFactory.create_factory( + LOCATION + "static/shaders/frag/simple.glsl", LOCATION + "static/shaders/vert/simple.glsl" +).then((e) => SHADERFACOTRY = e); function create_array(ptr: number, size: number): Float64Array { return new Float64Array(memory.buffer, ptr, size); } -export function main(game: Game) { +class GameInstance { + resizer: Resizer; + game: Game; + shader: Shader; + renderer: Renderer; + + constructor(game: Game) { + this.game = game; + this.shader = SHADERFACOTRY.create_shader(GL, {"MAX_CIRCLES": "50"}); + this.resizer = new Resizer(CANVAS, [...create_array(game.get_viewbox(), 4)], true); + this.renderer = new Renderer(); + this.renderer.addToDraw(indexBuffer, vao, this.shader); + } + + render(time: number) { + this.shader.uniform(GL, "u_circle_count", new Uniform1i(3)); + + this.shader.uniform(GL, "u_time", new Uniform1f(time * 0.001)); + this.shader.uniform(GL, "u_mouse", new Uniform2f(this.resizer.get_mouse_pos())); + this.shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox())); + this.shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION)); + + this.shader.uniform(GL, "u_circles", new Uniform3fv([ + 0, 0, 3.5, + -2, -2, 2, + 5, 2, 4, + ])); + this.shader.uniform(GL, "u_color", new Uniform4f([1, 1, 0, 1])); + + this.renderer.render(GL); + COUNTER.frame(time); + } +} + +var game_instance: GameInstance; + +export function set_instance(game: Game) { + game_instance = new GameInstance(game); + console.log(game.turn_count()); console.log(create_array(game.get_viewbox(), 4)); } + + +function step(time: number) { + if (game_instance) { + game_instance.render(time); + set_loading(false); + } else { + set_loading(true); + } + + requestAnimationFrame(step); +} +set_loading(true); + +requestAnimationFrame(step); diff --git a/frontend/www/static/res/style.css b/frontend/www/static/res/style.css new file mode 100644 index 0000000..f5bc236 --- /dev/null +++ b/frontend/www/static/res/style.css @@ -0,0 +1,57 @@ + +.loading { + position: relative; + display: inline-block; +} + +.loading::before{ + content: ""; + position: absolute; + width: 100px; + height: 100px; + background-color: #1c5ba2; + border-radius: 100%; + z-index: 5; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + -webkit-animation: slide-top 0.5s ease-out infinite alternate ; + animation: slide-top 0.5s ease-out infinite alternate ; + } + + #c { + position: relative; + } + +/* ---------------------------------------------- + * Generated by Animista on 2019-9-17 14:35:13 + * Licensed under FreeBSD License. + * See http://animista.net/license for more info. + * w: http://animista.net, t: @cssanimista + * ---------------------------------------------- */ + +/** + * ---------------------------------------- + * animation slide-top + * ---------------------------------------- + */ + @-webkit-keyframes slide-top { + 0% { + -webkit-transform: translate(-50%, 50%); + transform: translate(-50%, 50%); + } + 100% { + -webkit-transform: translate(-50%, -150%); + transform: translate(-50%, -150%); + } + } + @keyframes slide-top { + 0% { + -webkit-transform: translate(-50%, 50%); + transform: translate(-50%, 50%); + } + 100% { + -webkit-transform: translate(-50%, -150%); + transform: translate(-50%, -150%); + } + } diff --git a/frontend/www/static/shaders/frag/simple.glsl b/frontend/www/static/shaders/frag/simple.glsl new file mode 100644 index 0000000..9f6fb42 --- /dev/null +++ b/frontend/www/static/shaders/frag/simple.glsl @@ -0,0 +1,27 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform int u_circle_count; +uniform float u_time; +uniform vec2 u_mouse; +uniform vec4 u_viewbox; // [x, y, width, height] +uniform vec2 u_resolution; +uniform vec3 u_circles[$MAX_CIRCLES]; +uniform vec4 u_color; + +varying vec2 v_pos; + +void main() { + vec2 uv = v_pos; + + float alpha = 0.0; + for (int i = 0; i < $MAX_CIRCLES; i++ ){ + if (i >= u_circle_count) { break; } + float d = distance(uv.xy, u_circles[i].xy); + alpha = max(1.0 - d/u_circles[i].z, alpha); + } + + gl_FragColor = u_color; + gl_FragColor.w *= alpha; +} diff --git a/frontend/www/static/shaders/vert/simple.glsl b/frontend/www/static/shaders/vert/simple.glsl new file mode 100644 index 0000000..1d74e16 --- /dev/null +++ b/frontend/www/static/shaders/vert/simple.glsl @@ -0,0 +1,22 @@ +#ifdef GL_ES +precision mediump float; +#endif + +attribute vec2 a_position; + +uniform vec4 u_viewbox; // [x, y, width, height] +uniform vec2 u_resolution; + +varying vec2 v_pos; + +void main() { + + vec2 uv = ( a_position.xy + 1.0 ) * 0.5; + + uv *= u_viewbox.zw; + uv += u_viewbox.xy; + + v_pos = uv.xy; + + gl_Position = vec4(a_position.xy, 0.0, 1.0); +} diff --git a/frontend/www/webgl/index.ts b/frontend/www/webgl/index.ts new file mode 100644 index 0000000..f3e8616 --- /dev/null +++ b/frontend/www/webgl/index.ts @@ -0,0 +1,115 @@ + +import { Shader, Uniform4f, Uniform2fv, Uniform3fv, Uniform1i, Uniform1f, Uniform2f, ShaderFactory } from './shader'; +import { resizeCanvasToDisplaySize, FPSCounter, onload2promise, Resizer } from "./util"; +import { VertexBuffer, IndexBuffer } from './buffer'; +import { VertexArray, VertexBufferLayout } from './vertexBufferLayout'; +import { Renderer } from './renderer'; +import { Texture } from './texture'; + + +async function main() { + + const URL = window.location.origin+window.location.pathname; + const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); + + + // Get A WebGL context + var canvas = document.getElementById("c"); + const resolution = [canvas.width, canvas.height]; + + const resizer = new Resizer(canvas, [0, 0, 900, 900], true); + + var gl = canvas.getContext("webgl"); + if (!gl) { + return; + } + + const renderer = new Renderer(); + + const factory = await ShaderFactory.create_factory(LOCATION + "static/shaders/frag/simple.glsl", LOCATION + "static/shaders/vert/simple.glsl"); + const program = factory.create_shader(gl, {"MAX_CIRCLES": "50"}); + + var positions = [ + -1, -1, 0, 1, + -1, 1, 0, 0, + 1, -1, 1, 1, + 1, 1, 1, 0, + ]; + + var positionBuffer = new VertexBuffer(gl, positions); + var layout = new VertexBufferLayout(); + layout.push(gl.FLOAT, 2, 4, "a_position"); + layout.push(gl.FLOAT, 2, 4, "a_tex"); + + const vao = new VertexArray(); + vao.addBuffer(positionBuffer, layout); + + resizeCanvasToDisplaySize(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 indices = [ + 0, 1, 2, + 1, 2, 3, + ]; + + var indexBuffer = new IndexBuffer(gl, indices); + indexBuffer.bind(gl); + + renderer.addToDraw(indexBuffer, vao, program); + + var blue = 1.0; + var inc = 0.05; + + const counter = new FPSCounter(); + + const step = function (time: number) { + blue += inc; + // if (blue > 1.0 || blue < 0.0) { + // inc = -1 * inc; + // blue += inc; + // } + + program.uniform(gl, "u_circle_count", new Uniform1i(3)); + + 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_circles", new Uniform3fv([ + 450, 450, 100, + 200, 200, 200, + 900, 0, 300, + ])); + program.uniform(gl, "u_color", new Uniform4f([1, blue, 0, 1])); + + renderer.render(gl); + + counter.frame(time); + requestAnimationFrame(step); + } + + requestAnimationFrame(step); +} + +main(); + +const loader = document.getElementById("loader"); +setInterval(() => { + if (loader.classList.contains("loading")) { + loader.classList.remove("loading") + } else { + loader.classList.add("loading"); + } +}, 2000); diff --git a/frontend/www/webgl/shader.ts b/frontend/www/webgl/shader.ts index 409d81a..aba96fd 100644 --- a/frontend/www/webgl/shader.ts +++ b/frontend/www/webgl/shader.ts @@ -1,7 +1,7 @@ import { Dictionary } from './util'; function error(msg: string) { - console.log(msg); + console.error(msg); } const defaultShaderType = [ @@ -9,6 +9,7 @@ const defaultShaderType = [ "FRAGMENT_SHADER" ]; +/// Create Shader from Source string function loadShader( gl: WebGLRenderingContext, shaderSource: string, @@ -41,6 +42,7 @@ function loadShader( return shader; } +/// Actually Create Program with Shader's function createProgram( gl: WebGLRenderingContext, shaders: WebGLShader[], @@ -76,39 +78,45 @@ function createProgram( return program; } -function createShaderFromScript( - gl: WebGLRenderingContext, - scriptId: string, - context: Dictionary, - opt_shaderType: number, - opt_errorCallback: any, -): WebGLShader { - var shaderSource = ""; - var shaderType; - var shaderScript = document.getElementById(scriptId) as HTMLScriptElement; - if (!shaderScript) { - console.log("*** Error: unknown script element" + scriptId); - } - shaderSource = shaderScript.text; +export class ShaderFactory { + frag_source: string; + vert_source: string; - for (let key in context) { - console.log("substitute " + key); - shaderSource = shaderSource.replace(new RegExp("\\$" + key, 'g'), context[key]); + static async create_factory(frag_url: string, vert_url: string): Promise { + 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]); } - if (!opt_shaderType) { - if (shaderScript.type === "x-shader/x-vertex") { - shaderType = 35633; - } else if (shaderScript.type === "x-shader/x-fragment") { - shaderType = 35632; - } else if (shaderType !== gl.VERTEX_SHADER && shaderType !== gl.FRAGMENT_SHADER) { - console.log("*** Error: unknown shader type"); + constructor(frag_source: string, vert_source: string ) { + this.frag_source = frag_source; + this.vert_source = vert_source; + } + + create_shader( + gl: WebGLRenderingContext, + context?: Dictionary, + 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]); } - } - return loadShader( - gl, shaderSource, opt_shaderType ? opt_shaderType : shaderType, - opt_errorCallback); + 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 { @@ -116,22 +124,6 @@ export class Shader { uniformCache: Dictionary; attribCache: Dictionary; - static createProgramFromScripts( - gl: WebGLRenderingContext, - shaderScriptIds: string[], - context = {}, - opt_attribs?: string[], - opt_locations?: number[], - opt_errorCallback?: any, - ): Shader { - var shaders = []; - for (var ii = 0; ii < shaderScriptIds.length; ++ii) { - shaders.push(createShaderFromScript( - gl, shaderScriptIds[ii], context, (gl as any)[defaultShaderType[ii % 2]] as number, opt_errorCallback)); - } - return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback)); - } - static async createProgramFromUrls( gl: WebGLRenderingContext, vert_url: string, @@ -198,6 +190,10 @@ export class Shader { uniform.setUniform(gl, location); } + + clear(gl: WebGLRenderingContext) { + gl.deleteProgram(this.shader); + } } export interface Uniform { @@ -226,6 +222,22 @@ export class Uniform3fv implements Uniform { } } +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[]; constructor(data: number[]) { @@ -265,9 +277,9 @@ export class Uniform2f implements Uniform { x: number; y: number; - constructor(x: number, y: number) { - this.x = x; - this.y = y; + constructor(xy: number[]) { + this.x = xy[0]; + this.y = xy[1]; } setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { @@ -281,25 +293,14 @@ export class Uniform4f implements Uniform { v2: number; v3: number; - constructor(vec: number[]) { - this.v0 = vec[0]; - this.v1 = vec[1]; - this.v2 = vec[2]; - this.v3 = vec[3]; + 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[]; - constructor(data: number[]) { - this.data = data; - } - - setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { - gl.uniformMatrix3fv(location, false, this.data); - } -} diff --git a/frontend/www/webgl/util.ts b/frontend/www/webgl/util.ts index 7aa4a6c..007cb53 100644 --- a/frontend/www/webgl/util.ts +++ b/frontend/www/webgl/util.ts @@ -39,7 +39,7 @@ export class FPSCounter { frame(now: number) { this.count += 1; - if (now - this.last > 1) { + if (now - this.last > 1000) { this.last = now; console.log(this.count + " fps"); this.count = 0; @@ -47,83 +47,122 @@ export class FPSCounter { } } -export class M3 { - _data: any; +export class Resizer { + hoovering: boolean; + dragging: boolean; - constructor(data: any) { - this._data = data; - } + mouse_pos: number[]; + last_drag: number[]; - static ident(): M3 { - return new M3([ - 1, 0, 0, - 0, 1, 0, - 0, 0, 1 - ]); - } + viewbox: number[]; + orig_viewbox: number[]; - multiply(other: M3): M3 { - const a = this._data; - const b = other._data; + el_width: number; - var a00 = a[0 * 3 + 0]; - var a01 = a[0 * 3 + 1]; - var a02 = a[0 * 3 + 2]; - var a10 = a[1 * 3 + 0]; - var a11 = a[1 * 3 + 1]; - var a12 = a[1 * 3 + 2]; - var a20 = a[2 * 3 + 0]; - var a21 = a[2 * 3 + 1]; - var a22 = a[2 * 3 + 2]; - var b00 = b[0 * 3 + 0]; - var b01 = b[0 * 3 + 1]; - var b02 = b[0 * 3 + 2]; - var b10 = b[1 * 3 + 0]; - var b11 = b[1 * 3 + 1]; - var b12 = b[1 * 3 + 2]; - var b20 = b[2 * 3 + 0]; - var b21 = b[2 * 3 + 1]; - var b22 = b[2 * 3 + 2]; + scaleX = 1; + scaleY = 1; - return new M3([ - b00 * a00 + b01 * a10 + b02 * a20, - b00 * a01 + b01 * a11 + b02 * a21, - b00 * a02 + b01 * a12 + b02 * a22, - b10 * a00 + b11 * a10 + b12 * a20, - b10 * a01 + b11 * a11 + b12 * a21, - b10 * a02 + b11 * a12 + b12 * a22, - b20 * a00 + b21 * a10 + b22 * a20, - b20 * a01 + b21 * a11 + b22 * a21, - b20 * a02 + b21 * a12 + b22 * a22, - ]); - } + constructor(el: HTMLCanvasElement, viewbox: number[], keep_aspect_ratio=false) { + console.log("viewbox:" + viewbox); + this.hoovering = false; + this.dragging = false; - translation(x: number, y: number): M3 { - const out = [...this._data]; - out[6] += x; - out[7] += y; - return new M3(out); - } + this.mouse_pos = [0, 0]; + this.last_drag = [0, 0]; - rotate(rad: number): M3 { - var c = Math.cos(rad); - var s = Math.sin(rad); + this.viewbox = [...viewbox]; - const out = new M3([...this._data]); + if (keep_aspect_ratio) { + const or_width = this.viewbox[2]; + const or_height = this.viewbox[3]; + const scaleX = el.height / el.width; + if (scaleX < 1) { + this.scaleX= 1 / scaleX; - return out.multiply(new M3([ - c, -s, 0, - s, c, 0, - 0, 0, 1 - ])); - } + this.viewbox[2] *= this.scaleX; + } else { + this.scaleY = scaleX; + this.viewbox[3] *= scaleX; + } - scale(s_x: number, s_y = s_x, s_z = 1): M3 { - const out = new M3([...this._data]); - return out.multiply(new M3([ - s_x, 0, 0, - 0, s_y, 0, - 0, 0, s_z, - ])); - } + this.viewbox[0] -= (this.viewbox[2] - or_width) / 2; + this.viewbox[1] -= (this.viewbox[3] - or_height) / 2; + } + + this.orig_viewbox = [...this.viewbox]; + + this.el_width = el.width; + + 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; + this.dragging = false; + } + + mousemove(e: MouseEvent) { + this.mouse_pos = [e.offsetX, this.el_width - e.offsetY]; + + if (this.dragging) { + const scale = this.viewbox[3] / this.orig_viewbox[3]; + this.viewbox[0] += (this.last_drag[0] - this.mouse_pos[0]) * scale; + this.viewbox[1] += (this.last_drag[1] - this.mouse_pos[1]) * scale; + + 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 dx = e.deltaY * this.scaleX; + this.viewbox[2] += dx; + this.viewbox[0] -= dx / 2; + this.viewbox[2] = Math.min(this.viewbox[2], this.orig_viewbox[2]); + + const dy = e.deltaY * this.scaleY; + this.viewbox[3] += dy; + this.viewbox[1] -= dy / 2; + 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; + } } diff --git a/frontend/www/webgl/webgl-utils.d.ts b/frontend/www/webgl/webgl-utils.d.ts new file mode 100644 index 0000000..bd6bab2 --- /dev/null +++ b/frontend/www/webgl/webgl-utils.d.ts @@ -0,0 +1,20 @@ +// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] +// Project: [~THE PROJECT NAME~] +// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> + +/*~ This is the module template file. You should rename it to index.d.ts + *~ and place it in a folder with the same name as the module. + *~ For example, if you were writing a file for "super-greeter", this + *~ file should be 'super-greeter/index.d.ts' + */ + +// /*~ If this module is a UMD module that exposes a global variable 'myLib' when +// *~ loaded outside a module loader environment, declare that global here. +// *~ Otherwise, delete this declaration. +// */ +// export as namespace myLib; + +/*~ If this module has methods, declare them as functions like so. + */ +export function createProgramFromScripts(gl: WebGLRenderingContext, shaderScriptIds: string[], opt_attribs?: string[], opt_locations?: number[], opt_errorCallback?: any): any; +export function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement, multiplier?: number): boolean; diff --git a/frontend/www/webgl/webgl-utils.js b/frontend/www/webgl/webgl-utils.js new file mode 100644 index 0000000..da64e73 --- /dev/null +++ b/frontend/www/webgl/webgl-utils.js @@ -0,0 +1,1293 @@ +/* + * Copyright 2012, Gregg Tavares. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Gregg Tavares. nor the names of his + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function(root, factory) { // eslint-disable-line + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], function() { + return factory.call(root); + }); + } else { + // Browser globals + root.webglUtils = factory.call(root); + } +}(this, function() { + "use strict"; + + var topWindow = this; + + /** @module webgl-utils */ + + function isInIFrame(w) { + w = w || topWindow; + return w !== w.top; + } + + if (!isInIFrame()) { + console.log("%c%s", 'color:blue;font-weight:bold;', 'for more about webgl-utils.js see:'); // eslint-disable-line + console.log("%c%s", 'color:blue;font-weight:bold;', 'http://webglfundamentals.org/webgl/lessons/webgl-boilerplate.html'); // eslint-disable-line + } + + /** + * Wrapped logging function. + * @param {string} msg The message to log. + */ + function error(msg) { + if (topWindow.console) { + if (topWindow.console.error) { + topWindow.console.error(msg); + } else if (topWindow.console.log) { + topWindow.console.log(msg); + } + } + } + + + /** + * Error Callback + * @callback ErrorCallback + * @param {string} msg error message. + * @memberOf module:webgl-utils + */ + + + /** + * Loads a shader. + * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use. + * @param {string} shaderSource The shader source. + * @param {number} shaderType The type of shader. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. + * @return {WebGLShader} The created shader. + */ + function loadShader(gl, shaderSource, shaderType, opt_errorCallback) { + 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; + } + + /** + * Creates a program, attaches shaders, binds attrib locations, links the + * program and calls useProgram. + * @param {WebGLShader[]} shaders The shaders to attach + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @memberOf module:webgl-utils + */ + function createProgram( + gl, shaders, opt_attribs, opt_locations, opt_errorCallback) { + 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; + } + + /** + * Loads a shader from a script tag. + * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use. + * @param {string} scriptId The id of the script tag. + * @param {number} opt_shaderType The type of shader. If not passed in it will + * be derived from the type of the script tag. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. + * @return {WebGLShader} The created shader. + */ + function createShaderFromScript( + gl, scriptId, opt_shaderType, opt_errorCallback) { + var shaderSource = ""; + var shaderType; + var shaderScript = document.getElementById(scriptId); + if (!shaderScript) { + console.log ("*** Error: unknown script element" + scriptId); + } + shaderSource = shaderScript.text; + + if (!opt_shaderType) { + if (shaderScript.type === "x-shader/x-vertex") { + shaderType = 35633; + } else if (shaderScript.type === "x-shader/x-fragment") { + shaderType = 35632; + } else if (shaderType !== gl.VERTEX_SHADER && shaderType !== gl.FRAGMENT_SHADER) { + console.log ("*** Error: unknown shader type"); + } + } + + return loadShader( + gl, shaderSource, opt_shaderType ? opt_shaderType : shaderType, + opt_errorCallback); + } + + var defaultShaderType = [ + "VERTEX_SHADER", + "FRAGMENT_SHADER", + ]; + + /** + * Creates a program from 2 script tags. + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {string[]} shaderScriptIds Array of ids of the script + * tags for the shaders. The first is assumed to be the + * vertex shader, the second the fragment shader. + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @return {WebGLProgram} The created program. + * @memberOf module:webgl-utils + */ + function createProgramFromScripts( + gl, shaderScriptIds, opt_attribs, opt_locations, opt_errorCallback) { + var shaders = []; + for (var ii = 0; ii < shaderScriptIds.length; ++ii) { + shaders.push(createShaderFromScript( + gl, shaderScriptIds[ii], gl[defaultShaderType[ii]], opt_errorCallback)); + } + return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback); + } + + /** + * Creates a program from 2 sources. + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {string[]} shaderSourcess Array of sources for the + * shaders. The first is assumed to be the vertex shader, + * the second the fragment shader. + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @return {WebGLProgram} The created program. + * @memberOf module:webgl-utils + */ + function createProgramFromSources( + gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback) { + var shaders = []; + for (var ii = 0; ii < shaderSources.length; ++ii) { + shaders.push(loadShader( + gl, shaderSources[ii], gl[defaultShaderType[ii]], opt_errorCallback)); + } + return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback); + } + + /** + * Returns the corresponding bind point for a given sampler type + */ + function getBindPointForSamplerType(gl, type) { + if (type === gl.SAMPLER_2D) return gl.TEXTURE_2D; // eslint-disable-line + if (type === gl.SAMPLER_CUBE) return gl.TEXTURE_CUBE_MAP; // eslint-disable-line + return undefined; + } + + /** + * @typedef {Object.} Setters + */ + + /** + * Creates setter functions for all uniforms of a shader + * program. + * + * @see {@link module:webgl-utils.setUniforms} + * + * @param {WebGLProgram} program the program to create setters for. + * @returns {Object.} an object with a setter by name for each uniform + * @memberOf module:webgl-utils + */ + function createUniformSetters(gl, program) { + var textureUnit = 0; + + /** + * Creates a setter for a uniform of the given program with it's + * location embedded in the setter. + * @param {WebGLProgram} program + * @param {WebGLUniformInfo} uniformInfo + * @returns {function} the created setter. + */ + function createUniformSetter(program, uniformInfo) { + var location = gl.getUniformLocation(program, uniformInfo.name); + var type = uniformInfo.type; + // Check if this uniform is an array + var isArray = (uniformInfo.size > 1 && uniformInfo.name.substr(-3) === "[0]"); + if (type === gl.FLOAT && isArray) { + return function(v) { + gl.uniform1fv(location, v); + }; + } + if (type === gl.FLOAT) { + return function(v) { + gl.uniform1f(location, v); + }; + } + if (type === gl.FLOAT_VEC2) { + return function(v) { + gl.uniform2fv(location, v); + }; + } + if (type === gl.FLOAT_VEC3) { + return function(v) { + gl.uniform3fv(location, v); + }; + } + if (type === gl.FLOAT_VEC4) { + return function(v) { + gl.uniform4fv(location, v); + }; + } + if (type === gl.INT && isArray) { + return function(v) { + gl.uniform1iv(location, v); + }; + } + if (type === gl.INT) { + return function(v) { + gl.uniform1i(location, v); + }; + } + if (type === gl.INT_VEC2) { + return function(v) { + gl.uniform2iv(location, v); + }; + } + if (type === gl.INT_VEC3) { + return function(v) { + gl.uniform3iv(location, v); + }; + } + if (type === gl.INT_VEC4) { + return function(v) { + gl.uniform4iv(location, v); + }; + } + if (type === gl.BOOL) { + return function(v) { + gl.uniform1iv(location, v); + }; + } + if (type === gl.BOOL_VEC2) { + return function(v) { + gl.uniform2iv(location, v); + }; + } + if (type === gl.BOOL_VEC3) { + return function(v) { + gl.uniform3iv(location, v); + }; + } + if (type === gl.BOOL_VEC4) { + return function(v) { + gl.uniform4iv(location, v); + }; + } + if (type === gl.FLOAT_MAT2) { + return function(v) { + gl.uniformMatrix2fv(location, false, v); + }; + } + if (type === gl.FLOAT_MAT3) { + return function(v) { + gl.uniformMatrix3fv(location, false, v); + }; + } + if (type === gl.FLOAT_MAT4) { + return function(v) { + gl.uniformMatrix4fv(location, false, v); + }; + } + if ((type === gl.SAMPLER_2D || type === gl.SAMPLER_CUBE) && isArray) { + var units = []; + for (var ii = 0; ii < info.size; ++ii) { + units.push(textureUnit++); + } + return function(bindPoint, units) { + return function(textures) { + gl.uniform1iv(location, units); + textures.forEach(function(texture, index) { + gl.activeTexture(gl.TEXTURE0 + units[index]); + gl.bindTexture(bindPoint, texture); + }); + }; + }(getBindPointForSamplerType(gl, type), units); + } + if (type === gl.SAMPLER_2D || type === gl.SAMPLER_CUBE) { + return function(bindPoint, unit) { + return function(texture) { + gl.uniform1i(location, unit); + gl.activeTexture(gl.TEXTURE0 + unit); + gl.bindTexture(bindPoint, texture); + }; + }(getBindPointForSamplerType(gl, type), textureUnit++); + } + throw ("unknown type: 0x" + type.toString(16)); // we should never get here. + } + + var uniformSetters = { }; + var numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); + + for (var ii = 0; ii < numUniforms; ++ii) { + var uniformInfo = gl.getActiveUniform(program, ii); + if (!uniformInfo) { + break; + } + var name = uniformInfo.name; + // remove the array suffix. + if (name.substr(-3) === "[0]") { + name = name.substr(0, name.length - 3); + } + var setter = createUniformSetter(program, uniformInfo); + uniformSetters[name] = setter; + } + return uniformSetters; + } + + /** + * Set uniforms and binds related textures. + * + * example: + * + * var programInfo = createProgramInfo( + * gl, ["some-vs", "some-fs"); + * + * var tex1 = gl.createTexture(); + * var tex2 = gl.createTexture(); + * + * ... assume we setup the textures with data ... + * + * var uniforms = { + * u_someSampler: tex1, + * u_someOtherSampler: tex2, + * u_someColor: [1,0,0,1], + * u_somePosition: [0,1,1], + * u_someMatrix: [ + * 1,0,0,0, + * 0,1,0,0, + * 0,0,1,0, + * 0,0,0,0, + * ], + * }; + * + * gl.useProgram(program); + * + * This will automatically bind the textures AND set the + * uniforms. + * + * setUniforms(programInfo.uniformSetters, uniforms); + * + * For the example above it is equivalent to + * + * var texUnit = 0; + * gl.activeTexture(gl.TEXTURE0 + texUnit); + * gl.bindTexture(gl.TEXTURE_2D, tex1); + * gl.uniform1i(u_someSamplerLocation, texUnit++); + * gl.activeTexture(gl.TEXTURE0 + texUnit); + * gl.bindTexture(gl.TEXTURE_2D, tex2); + * gl.uniform1i(u_someSamplerLocation, texUnit++); + * gl.uniform4fv(u_someColorLocation, [1, 0, 0, 1]); + * gl.uniform3fv(u_somePositionLocation, [0, 1, 1]); + * gl.uniformMatrix4fv(u_someMatrix, false, [ + * 1,0,0,0, + * 0,1,0,0, + * 0,0,1,0, + * 0,0,0,0, + * ]); + * + * Note it is perfectly reasonable to call `setUniforms` multiple times. For example + * + * var uniforms = { + * u_someSampler: tex1, + * u_someOtherSampler: tex2, + * }; + * + * var moreUniforms { + * u_someColor: [1,0,0,1], + * u_somePosition: [0,1,1], + * u_someMatrix: [ + * 1,0,0,0, + * 0,1,0,0, + * 0,0,1,0, + * 0,0,0,0, + * ], + * }; + * + * setUniforms(programInfo.uniformSetters, uniforms); + * setUniforms(programInfo.uniformSetters, moreUniforms); + * + * @param {Object.|module:webgl-utils.ProgramInfo} setters the setters returned from + * `createUniformSetters` or a ProgramInfo from {@link module:webgl-utils.createProgramInfo}. + * @param {Object.} an object with values for the + * uniforms. + * @memberOf module:webgl-utils + */ + function setUniforms(setters, values) { + setters = setters.uniformSetters || setters; + Object.keys(values).forEach(function(name) { + var setter = setters[name]; + if (setter) { + setter(values[name]); + } + }); + } + + /** + * Creates setter functions for all attributes of a shader + * program. You can pass this to {@link module:webgl-utils.setBuffersAndAttributes} to set all your buffers and attributes. + * + * @see {@link module:webgl-utils.setAttributes} for example + * @param {WebGLProgram} program the program to create setters for. + * @return {Object.} an object with a setter for each attribute by name. + * @memberOf module:webgl-utils + */ + function createAttributeSetters(gl, program) { + var attribSetters = { + }; + + function createAttribSetter(index) { + return function(b) { + gl.bindBuffer(gl.ARRAY_BUFFER, b.buffer); + gl.enableVertexAttribArray(index); + gl.vertexAttribPointer( + index, b.numComponents || b.size, b.type || gl.FLOAT, b.normalize || false, b.stride || 0, b.offset || 0); + }; + } + + var numAttribs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); + for (var ii = 0; ii < numAttribs; ++ii) { + var attribInfo = gl.getActiveAttrib(program, ii); + if (!attribInfo) { + break; + } + var index = gl.getAttribLocation(program, attribInfo.name); + attribSetters[attribInfo.name] = createAttribSetter(index); + } + + return attribSetters; + } + + /** + * Sets attributes and binds buffers (deprecated... use {@link module:webgl-utils.setBuffersAndAttributes}) + * + * Example: + * + * var program = createProgramFromScripts( + * gl, ["some-vs", "some-fs"); + * + * var attribSetters = createAttributeSetters(program); + * + * var positionBuffer = gl.createBuffer(); + * var texcoordBuffer = gl.createBuffer(); + * + * var attribs = { + * a_position: {buffer: positionBuffer, numComponents: 3}, + * a_texcoord: {buffer: texcoordBuffer, numComponents: 2}, + * }; + * + * gl.useProgram(program); + * + * This will automatically bind the buffers AND set the + * attributes. + * + * setAttributes(attribSetters, attribs); + * + * Properties of attribs. For each attrib you can add + * properties: + * + * * type: the type of data in the buffer. Default = gl.FLOAT + * * normalize: whether or not to normalize the data. Default = false + * * stride: the stride. Default = 0 + * * offset: offset into the buffer. Default = 0 + * + * For example if you had 3 value float positions, 2 value + * float texcoord and 4 value uint8 colors you'd setup your + * attribs like this + * + * var attribs = { + * a_position: {buffer: positionBuffer, numComponents: 3}, + * a_texcoord: {buffer: texcoordBuffer, numComponents: 2}, + * a_color: { + * buffer: colorBuffer, + * numComponents: 4, + * type: gl.UNSIGNED_BYTE, + * normalize: true, + * }, + * }; + * + * @param {Object.|model:webgl-utils.ProgramInfo} setters Attribute setters as returned from createAttributeSetters or a ProgramInfo as returned {@link module:webgl-utils.createProgramInfo} + * @param {Object.} attribs AttribInfos mapped by attribute name. + * @memberOf module:webgl-utils + * @deprecated use {@link module:webgl-utils.setBuffersAndAttributes} + */ + function setAttributes(setters, attribs) { + setters = setters.attribSetters || setters; + Object.keys(attribs).forEach(function(name) { + var setter = setters[name]; + if (setter) { + setter(attribs[name]); + } + }); + } + + /** + * Creates a vertex array object and then sets the attributes + * on it + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {Object.} setters Attribute setters as returned from createAttributeSetters + * @param {Object.} attribs AttribInfos mapped by attribute name. + * @param {WebGLBuffer} [indices] an optional ELEMENT_ARRAY_BUFFER of indices + */ + function createVAOAndSetAttributes(gl, setters, attribs, indices) { + var vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + setAttributes(setters, attribs); + if (indices) { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indices); + } + // We unbind this because otherwise any change to ELEMENT_ARRAY_BUFFER + // like when creating buffers for other stuff will mess up this VAO's binding + gl.bindVertexArray(null); + return vao; + } + + /** + * Creates a vertex array object and then sets the attributes + * on it + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {Object.| module:webgl-utils.ProgramInfo} programInfo as returned from createProgramInfo or Attribute setters as returned from createAttributeSetters + * @param {module:webgl-utils:BufferInfo} bufferInfo BufferInfo as returned from createBufferInfoFromArrays etc... + * @param {WebGLBuffer} [indices] an optional ELEMENT_ARRAY_BUFFER of indices + */ + function createVAOFromBufferInfo(gl, programInfo, bufferInfo) { + return createVAOAndSetAttributes(gl, programInfo.attribSetters || programInfo, bufferInfo.attribs, bufferInfo.indices); + } + + /** + * @typedef {Object} ProgramInfo + * @property {WebGLProgram} program A shader program + * @property {Object} uniformSetters: object of setters as returned from createUniformSetters, + * @property {Object} attribSetters: object of setters as returned from createAttribSetters, + * @memberOf module:webgl-utils + */ + + /** + * Creates a ProgramInfo from 2 sources. + * + * A ProgramInfo contains + * + * programInfo = { + * program: WebGLProgram, + * uniformSetters: object of setters as returned from createUniformSetters, + * attribSetters: object of setters as returned from createAttribSetters, + * } + * + * @param {WebGLRenderingContext} gl The WebGLRenderingContext + * to use. + * @param {string[]} shaderSourcess Array of sources for the + * shaders or ids. The first is assumed to be the vertex shader, + * the second the fragment shader. + * @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in + * @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console + * on error. If you want something else pass an callback. It's passed an error message. + * @return {module:webgl-utils.ProgramInfo} The created program. + * @memberOf module:webgl-utils + */ + function createProgramInfo( + gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback) { + shaderSources = shaderSources.map(function(source) { + var script = document.getElementById(source); + return script ? script.text : source; + }); + var program = webglUtils.createProgramFromSources(gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback); + if (!program) { + return null; + } + var uniformSetters = createUniformSetters(gl, program); + var attribSetters = createAttributeSetters(gl, program); + return { + program: program, + uniformSetters: uniformSetters, + attribSetters: attribSetters, + }; + } + + /** + * Sets attributes and buffers including the `ELEMENT_ARRAY_BUFFER` if appropriate + * + * Example: + * + * var programInfo = createProgramInfo( + * gl, ["some-vs", "some-fs"); + * + * var arrays = { + * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, + * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, + * }; + * + * var bufferInfo = createBufferInfoFromArrays(gl, arrays); + * + * gl.useProgram(programInfo.program); + * + * This will automatically bind the buffers AND set the + * attributes. + * + * setBuffersAndAttributes(programInfo.attribSetters, bufferInfo); + * + * For the example above it is equivilent to + * + * gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + * gl.enableVertexAttribArray(a_positionLocation); + * gl.vertexAttribPointer(a_positionLocation, 3, gl.FLOAT, false, 0, 0); + * gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); + * gl.enableVertexAttribArray(a_texcoordLocation); + * gl.vertexAttribPointer(a_texcoordLocation, 4, gl.FLOAT, false, 0, 0); + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext. + * @param {Object.} setters Attribute setters as returned from `createAttributeSetters` + * @param {module:webgl-utils.BufferInfo} buffers a BufferInfo as returned from `createBufferInfoFromArrays`. + * @memberOf module:webgl-utils + */ + function setBuffersAndAttributes(gl, setters, buffers) { + setAttributes(setters, buffers.attribs); + if (buffers.indices) { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices); + } + } + + // Add your prefix here. + var browserPrefixes = [ + "", + "MOZ_", + "OP_", + "WEBKIT_", + ]; + + /** + * Given an extension name like WEBGL_compressed_texture_s3tc + * returns the supported version extension, like + * WEBKIT_WEBGL_compressed_teture_s3tc + * @param {string} name Name of extension to look for + * @return {WebGLExtension} The extension or undefined if not + * found. + * @memberOf module:webgl-utils + */ + function getExtensionWithKnownPrefixes(gl, name) { + for (var ii = 0; ii < browserPrefixes.length; ++ii) { + var prefixedName = browserPrefixes[ii] + name; + var ext = gl.getExtension(prefixedName); + if (ext) { + return ext; + } + } + return undefined; + } + + /** + * Resize a canvas to match the size its displayed. + * @param {HTMLCanvasElement} canvas The canvas to resize. + * @param {number} [multiplier] amount to multiply by. + * Pass in window.devicePixelRatio for native pixels. + * @return {boolean} true if the canvas was resized. + * @memberOf module:webgl-utils + */ + function resizeCanvasToDisplaySize(canvas, multiplier) { + 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; + } + + // Add `push` to a typed array. It just keeps a 'cursor' + // and allows use to `push` values into the array so we + // don't have to manually compute offsets + function augmentTypedArray(typedArray, numComponents) { + var cursor = 0; + typedArray.push = function() { + for (var ii = 0; ii < arguments.length; ++ii) { + var value = arguments[ii]; + if (value instanceof Array || (value.buffer && value.buffer instanceof ArrayBuffer)) { + for (var jj = 0; jj < value.length; ++jj) { + typedArray[cursor++] = value[jj]; + } + } else { + typedArray[cursor++] = value; + } + } + }; + typedArray.reset = function(opt_index) { + cursor = opt_index || 0; + }; + typedArray.numComponents = numComponents; + Object.defineProperty(typedArray, 'numElements', { + get: function() { + return this.length / this.numComponents | 0; + }, + }); + return typedArray; + } + + /** + * creates a typed array with a `push` function attached + * so that you can easily *push* values. + * + * `push` can take multiple arguments. If an argument is an array each element + * of the array will be added to the typed array. + * + * Example: + * + * var array = createAugmentedTypedArray(3, 2); // creates a Float32Array with 6 values + * array.push(1, 2, 3); + * array.push([4, 5, 6]); + * // array now contains [1, 2, 3, 4, 5, 6] + * + * Also has `numComponents` and `numElements` properties. + * + * @param {number} numComponents number of components + * @param {number} numElements number of elements. The total size of the array will be `numComponents * numElements`. + * @param {constructor} opt_type A constructor for the type. Default = `Float32Array`. + * @return {ArrayBuffer} A typed array. + * @memberOf module:webgl-utils + */ + function createAugmentedTypedArray(numComponents, numElements, opt_type) { + var Type = opt_type || Float32Array; + return augmentTypedArray(new Type(numComponents * numElements), numComponents); + } + + function createBufferFromTypedArray(gl, array, type, drawType) { + type = type || gl.ARRAY_BUFFER; + var buffer = gl.createBuffer(); + gl.bindBuffer(type, buffer); + gl.bufferData(type, array, drawType || gl.STATIC_DRAW); + return buffer; + } + + function allButIndices(name) { + return name !== "indices"; + } + + function createMapping(obj) { + var mapping = {}; + Object.keys(obj).filter(allButIndices).forEach(function(key) { + mapping["a_" + key] = key; + }); + return mapping; + } + + function getGLTypeForTypedArray(gl, typedArray) { + if (typedArray instanceof Int8Array) { return gl.BYTE; } // eslint-disable-line + if (typedArray instanceof Uint8Array) { return gl.UNSIGNED_BYTE; } // eslint-disable-line + if (typedArray instanceof Int16Array) { return gl.SHORT; } // eslint-disable-line + if (typedArray instanceof Uint16Array) { return gl.UNSIGNED_SHORT; } // eslint-disable-line + if (typedArray instanceof Int32Array) { return gl.INT; } // eslint-disable-line + if (typedArray instanceof Uint32Array) { return gl.UNSIGNED_INT; } // eslint-disable-line + if (typedArray instanceof Float32Array) { return gl.FLOAT; } // eslint-disable-line + throw "unsupported typed array type"; + } + + // This is really just a guess. Though I can't really imagine using + // anything else? Maybe for some compression? + function getNormalizationForTypedArray(typedArray) { + if (typedArray instanceof Int8Array) { return true; } // eslint-disable-line + if (typedArray instanceof Uint8Array) { return true; } // eslint-disable-line + return false; + } + + function isArrayBuffer(a) { + return a.buffer && a.buffer instanceof ArrayBuffer; + } + + function guessNumComponentsFromName(name, length) { + var numComponents; + if (name.indexOf("coord") >= 0) { + numComponents = 2; + } else if (name.indexOf("color") >= 0) { + numComponents = 4; + } else { + numComponents = 3; // position, normals, indices ... + } + + if (length % numComponents > 0) { + throw "can not guess numComponents. You should specify it."; + } + + return numComponents; + } + + function makeTypedArray(array, name) { + if (isArrayBuffer(array)) { + return array; + } + + if (array.data && isArrayBuffer(array.data)) { + return array.data; + } + + if (Array.isArray(array)) { + array = { + data: array, + }; + } + + if (!array.numComponents) { + array.numComponents = guessNumComponentsFromName(name, array.length); + } + + var type = array.type; + if (!type) { + if (name === "indices") { + type = Uint16Array; + } + } + var typedArray = createAugmentedTypedArray(array.numComponents, array.data.length / array.numComponents | 0, type); + typedArray.push(array.data); + return typedArray; + } + + /** + * @typedef {Object} AttribInfo + * @property {number} [numComponents] the number of components for this attribute. + * @property {number} [size] the number of components for this attribute. + * @property {number} [type] the type of the attribute (eg. `gl.FLOAT`, `gl.UNSIGNED_BYTE`, etc...) Default = `gl.FLOAT` + * @property {boolean} [normalized] whether or not to normalize the data. Default = false + * @property {number} [offset] offset into buffer in bytes. Default = 0 + * @property {number} [stride] the stride in bytes per element. Default = 0 + * @property {WebGLBuffer} buffer the buffer that contains the data for this attribute + * @memberOf module:webgl-utils + */ + + + /** + * Creates a set of attribute data and WebGLBuffers from set of arrays + * + * Given + * + * var arrays = { + * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, + * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, + * normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, + * color: { numComponents: 4, data: [255, 255, 255, 255, 255, 0, 0, 255, 0, 0, 255, 255], type: Uint8Array, }, + * indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, + * }; + * + * returns something like + * + * var attribs = { + * a_position: { numComponents: 3, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, + * a_texcoord: { numComponents: 2, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, + * a_normal: { numComponents: 3, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, + * a_color: { numComponents: 4, type: gl.UNSIGNED_BYTE, normalize: true, buffer: WebGLBuffer, }, + * }; + * + * @param {WebGLRenderingContext} gl The webgl rendering context. + * @param {Object.} arrays The arrays + * @param {Object.} [opt_mapping] mapping from attribute name to array name. + * if not specified defaults to "a_name" -> "name". + * @return {Object.} the attribs + * @memberOf module:webgl-utils + */ + function createAttribsFromArrays(gl, arrays, opt_mapping) { + var mapping = opt_mapping || createMapping(arrays); + var attribs = {}; + Object.keys(mapping).forEach(function(attribName) { + var bufferName = mapping[attribName]; + var origArray = arrays[bufferName]; + var array = makeTypedArray(origArray, bufferName); + attribs[attribName] = { + buffer: createBufferFromTypedArray(gl, array), + numComponents: origArray.numComponents || array.numComponents || guessNumComponentsFromName(bufferName), + type: getGLTypeForTypedArray(gl, array), + normalize: getNormalizationForTypedArray(array), + }; + }); + return attribs; + } + + /** + * tries to get the number of elements from a set of arrays. + */ + function getNumElementsFromNonIndexedArrays(arrays) { + var key = Object.keys(arrays)[0]; + var array = arrays[key]; + if (isArrayBuffer(array)) { + return array.numElements; + } else { + return array.data.length / array.numComponents; + } + } + + /** + * @typedef {Object} BufferInfo + * @property {number} numElements The number of elements to pass to `gl.drawArrays` or `gl.drawElements`. + * @property {WebGLBuffer} [indices] The indices `ELEMENT_ARRAY_BUFFER` if any indices exist. + * @property {Object.} attribs The attribs approriate to call `setAttributes` + * @memberOf module:webgl-utils + */ + + + /** + * Creates a BufferInfo from an object of arrays. + * + * This can be passed to {@link module:webgl-utils.setBuffersAndAttributes} and to + * {@link module:webgl-utils:drawBufferInfo}. + * + * Given an object like + * + * var arrays = { + * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, + * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, + * normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, + * indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, + * }; + * + * Creates an BufferInfo like this + * + * bufferInfo = { + * numElements: 4, // or whatever the number of elements is + * indices: WebGLBuffer, // this property will not exist if there are no indices + * attribs: { + * a_position: { buffer: WebGLBuffer, numComponents: 3, }, + * a_normal: { buffer: WebGLBuffer, numComponents: 3, }, + * a_texcoord: { buffer: WebGLBuffer, numComponents: 2, }, + * }, + * }; + * + * The properties of arrays can be JavaScript arrays in which case the number of components + * will be guessed. + * + * var arrays = { + * position: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], + * texcoord: [0, 0, 0, 1, 1, 0, 1, 1], + * normal: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], + * indices: [0, 1, 2, 1, 2, 3], + * }; + * + * They can also by TypedArrays + * + * var arrays = { + * position: new Float32Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]), + * texcoord: new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]), + * normal: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]), + * indices: new Uint16Array([0, 1, 2, 1, 2, 3]), + * }; + * + * Or augmentedTypedArrays + * + * var positions = createAugmentedTypedArray(3, 4); + * var texcoords = createAugmentedTypedArray(2, 4); + * var normals = createAugmentedTypedArray(3, 4); + * var indices = createAugmentedTypedArray(3, 2, Uint16Array); + * + * positions.push([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]); + * texcoords.push([0, 0, 0, 1, 1, 0, 1, 1]); + * normals.push([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]); + * indices.push([0, 1, 2, 1, 2, 3]); + * + * var arrays = { + * position: positions, + * texcoord: texcoords, + * normal: normals, + * indices: indices, + * }; + * + * For the last example it is equivalent to + * + * var bufferInfo = { + * attribs: { + * a_position: { numComponents: 3, buffer: gl.createBuffer(), }, + * a_texcoods: { numComponents: 2, buffer: gl.createBuffer(), }, + * a_normals: { numComponents: 3, buffer: gl.createBuffer(), }, + * }, + * indices: gl.createBuffer(), + * numElements: 6, + * }; + * + * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_position.buffer); + * gl.bufferData(gl.ARRAY_BUFFER, arrays.position, gl.STATIC_DRAW); + * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_texcoord.buffer); + * gl.bufferData(gl.ARRAY_BUFFER, arrays.texcoord, gl.STATIC_DRAW); + * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_normal.buffer); + * gl.bufferData(gl.ARRAY_BUFFER, arrays.normal, gl.STATIC_DRAW); + * gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferInfo.indices); + * gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, arrays.indices, gl.STATIC_DRAW); + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext + * @param {Object.} arrays Your data + * @param {Object.} [opt_mapping] an optional mapping of attribute to array name. + * If not passed in it's assumed the array names will be mapped to an attribute + * of the same name with "a_" prefixed to it. An other words. + * + * var arrays = { + * position: ..., + * texcoord: ..., + * normal: ..., + * indices: ..., + * }; + * + * bufferInfo = createBufferInfoFromArrays(gl, arrays); + * + * Is the same as + * + * var arrays = { + * position: ..., + * texcoord: ..., + * normal: ..., + * indices: ..., + * }; + * + * var mapping = { + * a_position: "position", + * a_texcoord: "texcoord", + * a_normal: "normal", + * }; + * + * bufferInfo = createBufferInfoFromArrays(gl, arrays, mapping); + * + * @return {module:webgl-utils.BufferInfo} A BufferInfo + * @memberOf module:webgl-utils + */ + function createBufferInfoFromArrays(gl, arrays, opt_mapping) { + var bufferInfo = { + attribs: createAttribsFromArrays(gl, arrays, opt_mapping), + }; + var indices = arrays.indices; + if (indices) { + indices = makeTypedArray(indices, "indices"); + bufferInfo.indices = createBufferFromTypedArray(gl, indices, gl.ELEMENT_ARRAY_BUFFER); + bufferInfo.numElements = indices.length; + } else { + bufferInfo.numElements = getNumElementsFromNonIndexedArrays(arrays); + } + + return bufferInfo; + } + + /** + * Creates buffers from typed arrays + * + * Given something like this + * + * var arrays = { + * positions: [1, 2, 3], + * normals: [0, 0, 1], + * } + * + * returns something like + * + * buffers = { + * positions: WebGLBuffer, + * normals: WebGLBuffer, + * } + * + * If the buffer is named 'indices' it will be made an ELEMENT_ARRAY_BUFFER. + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext. + * @param {Object} arrays + * @return {Object} returns an object with one WebGLBuffer per array + * @memberOf module:webgl-utils + */ + function createBuffersFromArrays(gl, arrays) { + var buffers = { }; + Object.keys(arrays).forEach(function(key) { + var type = key === "indices" ? gl.ELEMENT_ARRAY_BUFFER : gl.ARRAY_BUFFER; + var array = makeTypedArray(arrays[key], name); + buffers[key] = createBufferFromTypedArray(gl, array, type); + }); + + // hrm + if (arrays.indices) { + buffers.numElements = arrays.indices.length; + } else if (arrays.position) { + buffers.numElements = arrays.position.length / 3; + } + + return buffers; + } + + /** + * Calls `gl.drawElements` or `gl.drawArrays`, whichever is appropriate + * + * normally you'd call `gl.drawElements` or `gl.drawArrays` yourself + * but calling this means if you switch from indexed data to non-indexed + * data you don't have to remember to update your draw call. + * + * @param {WebGLRenderingContext} gl A WebGLRenderingContext + * @param {module:webgl-utils.BufferInfo} bufferInfo as returned from createBufferInfoFromArrays + * @param {enum} [primitiveType] eg (gl.TRIANGLES, gl.LINES, gl.POINTS, gl.TRIANGLE_STRIP, ...) + * @param {number} [count] An optional count. Defaults to bufferInfo.numElements + * @param {number} [offset] An optional offset. Defaults to 0. + * @memberOf module:webgl-utils + */ + function drawBufferInfo(gl, bufferInfo, primitiveType, count, offset) { + var indices = bufferInfo.indices; + primitiveType = primitiveType === undefined ? gl.TRIANGLES : primitiveType; + var numElements = count === undefined ? bufferInfo.numElements : count; + offset = offset === undefined ? offset : 0; + if (indices) { + gl.drawElements(primitiveType, numElements, gl.UNSIGNED_SHORT, offset); + } else { + gl.drawArrays(primitiveType, offset, numElements); + } + } + + /** + * @typedef {Object} DrawObject + * @property {module:webgl-utils.ProgramInfo} programInfo A ProgramInfo as returned from createProgramInfo + * @property {module:webgl-utils.BufferInfo} bufferInfo A BufferInfo as returned from createBufferInfoFromArrays + * @property {Object} uniforms The values for the uniforms + * @memberOf module:webgl-utils + */ + + /** + * Draws a list of objects + * @param {WebGLRenderingContext} gl A WebGLRenderingContext + * @param {DrawObject[]} objectsToDraw an array of objects to draw. + * @memberOf module:webgl-utils + */ + function drawObjectList(gl, objectsToDraw) { + var lastUsedProgramInfo = null; + var lastUsedBufferInfo = null; + + objectsToDraw.forEach(function(object) { + var programInfo = object.programInfo; + var bufferInfo = object.bufferInfo; + var bindBuffers = false; + + if (programInfo !== lastUsedProgramInfo) { + lastUsedProgramInfo = programInfo; + gl.useProgram(programInfo.program); + bindBuffers = true; + } + + // Setup all the needed attributes. + if (bindBuffers || bufferInfo !== lastUsedBufferInfo) { + lastUsedBufferInfo = bufferInfo; + setBuffersAndAttributes(gl, programInfo.attribSetters, bufferInfo); + } + + // Set the uniforms. + setUniforms(programInfo.uniformSetters, object.uniforms); + + // Draw + drawBufferInfo(gl, bufferInfo); + }); + } + + var isIE = /*@cc_on!@*/false || !!document.documentMode; + // Edge 20+ + var isEdge = !isIE && !!window.StyleMedia; + if (isEdge) { + // Hack for Edge. Edge's WebGL implmentation is crap still and so they + // only respond to "experimental-webgl". I don't want to clutter the + // examples with that so his hack works around it + HTMLCanvasElement.prototype.getContext = function(origFn) { + return function() { + var args = arguments; + var type = args[0]; + if (type === "webgl") { + args = [].slice.call(arguments); + args[0] = "experimental-webgl"; + } + return origFn.apply(this, args); + }; + }(HTMLCanvasElement.prototype.getContext); + } + + return { + createAugmentedTypedArray: createAugmentedTypedArray, + createAttribsFromArrays: createAttribsFromArrays, + createBuffersFromArrays: createBuffersFromArrays, + createBufferInfoFromArrays: createBufferInfoFromArrays, + createAttributeSetters: createAttributeSetters, + createProgram: createProgram, + createProgramFromScripts: createProgramFromScripts, + createProgramFromSources: createProgramFromSources, + createProgramInfo: createProgramInfo, + createUniformSetters: createUniformSetters, + createVAOAndSetAttributes: createVAOAndSetAttributes, + createVAOFromBufferInfo: createVAOFromBufferInfo, + drawBufferInfo: drawBufferInfo, + drawObjectList: drawObjectList, + getExtensionWithKnownPrefixes: getExtensionWithKnownPrefixes, + resizeCanvasToDisplaySize: resizeCanvasToDisplaySize, + setAttributes: setAttributes, + setBuffersAndAttributes: setBuffersAndAttributes, + setUniforms: setUniforms, + }; + +}));