├── .gitignore ├── examples ├── doge.jpg ├── boxes │ ├── box_argo.png │ ├── box_avatar.png │ ├── box_descendants.png │ ├── box_breaking_bad.png │ ├── box_brick_mansions.png │ ├── box_the_kings_speech.png │ ├── box_crazy_stupid_love.png │ ├── box_gangs_of_new_york.png │ ├── box_quantum_of_solace.png │ ├── box_slumdog_millionaire.png │ └── box_good_night_and_good_luck.png ├── example1.html ├── perf1.html ├── auto-atlassing.html ├── auto-geo-batching.html ├── auto-geo-batching.js ├── example1.js ├── perf1.js └── auto-atlassing.js ├── package.json ├── LICENSE ├── lib ├── components.js ├── texture.js ├── shader.js ├── gl.js └── ui.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /examples/doge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/doge.jpg -------------------------------------------------------------------------------- /examples/boxes/box_argo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_argo.png -------------------------------------------------------------------------------- /examples/boxes/box_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_avatar.png -------------------------------------------------------------------------------- /examples/boxes/box_descendants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_descendants.png -------------------------------------------------------------------------------- /examples/boxes/box_breaking_bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_breaking_bad.png -------------------------------------------------------------------------------- /examples/boxes/box_brick_mansions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_brick_mansions.png -------------------------------------------------------------------------------- /examples/boxes/box_the_kings_speech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_the_kings_speech.png -------------------------------------------------------------------------------- /examples/boxes/box_crazy_stupid_love.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_crazy_stupid_love.png -------------------------------------------------------------------------------- /examples/boxes/box_gangs_of_new_york.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_gangs_of_new_york.png -------------------------------------------------------------------------------- /examples/boxes/box_quantum_of_solace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_quantum_of_solace.png -------------------------------------------------------------------------------- /examples/boxes/box_slumdog_millionaire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_slumdog_millionaire.png -------------------------------------------------------------------------------- /examples/boxes/box_good_night_and_good_luck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davedx/lustro/HEAD/examples/boxes/box_good_night_and_good_luck.png -------------------------------------------------------------------------------- /examples/example1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/perf1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /examples/auto-atlassing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /examples/auto-geo-batching.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lustro", 3 | "version": "0.0.1", 4 | "description": "A reactive, declarative, immediate mode WebGL UI system", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node_modules/instant-server/bin/instant 8080" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/davedx/lustro.git" 13 | }, 14 | "keywords": [ 15 | "reactive", 16 | "declarative", 17 | "webgl", 18 | "ui" 19 | ], 20 | "author": "Dave Clayton ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/davedx/lustro/issues" 24 | }, 25 | "homepage": "https://github.com/davedx/lustro", 26 | "dependencies": { 27 | "gl-matrix": "^2.1.0" 28 | }, 29 | "devDependencies": { 30 | "instant-server": "^1.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Clayton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/components.js: -------------------------------------------------------------------------------- 1 | var Animator = { 2 | name: "Animator", 3 | 4 | start: function() { 5 | this.destTransform = vec3.create(); 6 | this.velocity = vec3.create(); 7 | this.destTime = 0; 8 | }, 9 | 10 | animate: function(params) { 11 | this.destTime = params.time/1000.0; 12 | vec3.add(this.destTransform, this.destTransform, vec3.fromValues(params.transform[0], params.transform[1], params.transform[2])); 13 | vec3.divide(this.velocity, this.destTransform, vec3.fromValues(this.destTime, this.destTime, this.destTime)); 14 | //console.log("V: ", this.velocity); 15 | }, 16 | 17 | update: function(dt) { 18 | if(this.destTime > 0) { 19 | var delta = vec3.fromValues(dt*this.velocity[0], dt*this.velocity[1], dt*this.velocity[2]); 20 | vec3.add(this.root.localPosition, this.root.localPosition, delta); 21 | this.destTime -= dt; 22 | if(this.destTime <= 0) { 23 | vec3.set(this.destTransform, 0, 0, 0); 24 | //console.warn("Stopped animating. At: ", this.root.localPosition); 25 | this.destTime = 0; 26 | } 27 | } 28 | } 29 | }; 30 | 31 | var KeyInput = { 32 | name: "KeyInput", 33 | 34 | start: function() { 35 | document.addEventListener('keydown', function(event) { 36 | if(this.root.handleKeyPress) { 37 | this.root.handleKeyPress(event); 38 | } 39 | }.bind(this)); 40 | } 41 | }; -------------------------------------------------------------------------------- /examples/auto-geo-batching.js: -------------------------------------------------------------------------------- 1 | var Square = UI.component({ 2 | render: function() { 3 | return {}; 4 | } 5 | }); 6 | 7 | var BigView = UI.component({ 8 | render: function() { 9 | var boxes = ["argo", "avatar", "breaking_bad", "brick_mansions", "crazy_stupid_love", 10 | "descendants", "gangs_of_new_york", "good_night_and_good_luck", "quantum_of_solace", 11 | "slumdog_millionaire", "the_kings_speech"]; 12 | var repeats = 2; 13 | while(--repeats) { 14 | boxes = boxes.concat(boxes); 15 | } 16 | var x = 0, y = 0; 17 | var tex_width = 186; 18 | var tex_height = 270; 19 | var scale = 0.5; 20 | var box_width = tex_width * scale; 21 | var box_height = tex_height * scale; 22 | var covers = boxes.map(function(box) { 23 | var box = UI.new(Square, { 24 | top: y, 25 | left: x, 26 | width: box_width, 27 | height: box_height, 28 | background: "boxes/box_"+box+".png" 29 | }); 30 | //console.info(box); 31 | x += box_width; 32 | if(x > 1000-box_width) { 33 | x = 0; 34 | y += box_height; 35 | } 36 | return box; 37 | }); 38 | //console.info(covers); 39 | var args = [Square, { 40 | name: "background-colored", 41 | top: 0, 42 | left: 0, 43 | width: 1000, 44 | height: 700 45 | }].concat(covers); 46 | return UI.new.apply(this, args); 47 | } 48 | }); 49 | 50 | UI.render(BigView, document.getElementById("app")); 51 | 52 | var fps = document.getElementById("fps"); 53 | -------------------------------------------------------------------------------- /examples/example1.js: -------------------------------------------------------------------------------- 1 | var Square = UI.component({ 2 | components: [Animator, KeyInput], 3 | 4 | animationMap: { 5 | moveLeft: { 6 | transform: [-50, 0, 0], 7 | time: 200, 8 | easing: "linear" 9 | }, 10 | moveRight: { 11 | transform: [50, 0, 0], 12 | time: 200, 13 | easing: "linear" 14 | } 15 | }, 16 | 17 | handleKeyPress: function(e) { 18 | if(e.keyCode === 37) { 19 | this.Animator.animate(this.animationMap.moveLeft); 20 | } else if(e.keyCode === 39) { 21 | this.Animator.animate(this.animationMap.moveRight); 22 | } 23 | }, 24 | 25 | render: function() { 26 | return {}; 27 | } 28 | }); 29 | 30 | var BigView = UI.component({ 31 | render: function() { 32 | return UI.new(Square, { 33 | name: "background-colored", 34 | top: 50, 35 | left: 50, 36 | width: 100, 37 | height: 300, 38 | backgroundColor: [1.0, 0, 0, 1.0] 39 | }, 40 | UI.new(Square, { 41 | name: "doged", 42 | top: 100, 43 | left: 100, 44 | width: 128, 45 | height: 128, 46 | background: "doge.jpg" 47 | }), 48 | UI.new(Square, { 49 | name: "child1", 50 | top: 550, 51 | left: 850, 52 | width: 99, 53 | height: 99, 54 | background: "doge.jpg" 55 | }, 56 | UI.new(Square, { 57 | name: "child2", 58 | top: -50, 59 | left: 0, 60 | width: 30, 61 | height: 260 62 | }) 63 | ) 64 | ); 65 | } 66 | }); 67 | 68 | UI.render(BigView, document.getElementById("app")); 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lustro 2 | A reactive, declarative, immediate mode WebGL UI framework 3 | 4 | To run examples, npm install then npm start to spin up a server on 8080. Then browse to http://localhost:8080/examples/ 5 | 6 | ## Features 7 | 8 | * Declare your user interface as a hierarchy of components, the same way you would with React 9 | * (Not yet implemented) Your UI will react to changes in data automatically 10 | * Use an extensible component system to compose behaviour (think Unity3D gameobjects and components) 11 | * Declare animation behaviour in animationMaps (also somewhat inspired by Unity3D) 12 | * (Not yet implemented) Take full advantage of 3D engine optimizations, like texture atlassing and geometry batching 13 | 14 | Note the common theme here: **Declare**! 15 | 16 | ## Principles 17 | 18 | * A UI should be primarily declarative, with lifecycle methods to facilitate imperative logic and react to changes in the data model over time. Inspired by Facebook's React. 19 | * The DOM is too slow. Let's give up on it completely and get as close to the hardware as we can: render everything with WebGL. 20 | * WebGL is stateful and difficult to use, especially for people without graphics programming backgrounds. Instead of building API's on top of WebGL like other common libraries, let's abstract it away completely. Programmers should never have to think about the renderer, matrix stacks, blending modes, and instead be able to use familiar concepts like rgba colors, web images, and so on. 21 | * Keep it simple. Immediate mode means simpler rendering when we introduce animations. Just blit everything to the screen every frame. Don't worry, even your phone's GPU will be able to handle it, at 60fps. If it doesn't, Lustro is doing something wrong. 22 | -------------------------------------------------------------------------------- /examples/perf1.js: -------------------------------------------------------------------------------- 1 | var Square = UI.component({ 2 | components: [Animator, KeyInput], 3 | 4 | animationMap: { 5 | moveLeft: { 6 | transform: [-50, 0, 0], 7 | time: 200, 8 | easing: "linear" 9 | }, 10 | moveRight: { 11 | transform: [50, 0, 0], 12 | time: 200, 13 | easing: "linear" 14 | } 15 | }, 16 | 17 | handleKeyPress: function(e) { 18 | if(e.keyCode === 37) { 19 | this.Animator.animate(this.animationMap.moveLeft); 20 | } else if(e.keyCode === 39) { 21 | this.Animator.animate(this.animationMap.moveRight); 22 | } 23 | }, 24 | 25 | render: function() { 26 | return {}; 27 | } 28 | }); 29 | 30 | var BigView = UI.component({ 31 | render: function() { 32 | var boxes = ["argo", "avatar", "breaking_bad", "brick_mansions", "crazy_stupid_love", 33 | "descendants", "gangs_of_new_york", "good_night_and_good_luck", "quantum_of_solace", 34 | "slumdog_millionaire", "the_kings_speech"]; 35 | var repeats = 8; 36 | while(--repeats) { 37 | boxes = boxes.concat(boxes); 38 | } 39 | var x = 0, y = 0; 40 | var tex_width = 186; 41 | var tex_height = 270; 42 | var scale = 0.1; 43 | var box_width = tex_width * scale; 44 | var box_height = tex_height * scale; 45 | var covers = boxes.map(function(box) { 46 | var box = UI.new(Square, { 47 | top: y, 48 | left: x, 49 | width: box_width, 50 | height: box_height, 51 | background: "boxes/box_"+box+".png" 52 | }); 53 | //console.info(box); 54 | x += box_width; 55 | if(x > 1000-box_width) { 56 | x = 0; 57 | y += box_height; 58 | } 59 | return box; 60 | }); 61 | //console.info(covers); 62 | var args = [Square, { 63 | name: "background-colored", 64 | top: 0, 65 | left: 0, 66 | width: 1000, 67 | height: 700 68 | }].concat(covers); 69 | return UI.new.apply(this, args); 70 | } 71 | }); 72 | 73 | UI.render(BigView, document.getElementById("app")); 74 | 75 | var fps = document.getElementById("fps"); 76 | -------------------------------------------------------------------------------- /examples/auto-atlassing.js: -------------------------------------------------------------------------------- 1 | var Square = UI.component({ 2 | components: [Animator, KeyInput], 3 | 4 | animationMap: { 5 | moveLeft: { 6 | transform: [-50, 0, 0], 7 | time: 200, 8 | easing: "linear" 9 | }, 10 | moveRight: { 11 | transform: [50, 0, 0], 12 | time: 200, 13 | easing: "linear" 14 | } 15 | }, 16 | 17 | handleKeyPress: function(e) { 18 | if(e.keyCode === 37) { 19 | this.Animator.animate(this.animationMap.moveLeft); 20 | } else if(e.keyCode === 39) { 21 | this.Animator.animate(this.animationMap.moveRight); 22 | } 23 | }, 24 | 25 | render: function() { 26 | return {}; 27 | } 28 | }); 29 | 30 | var BigView = UI.component({ 31 | render: function() { 32 | var boxes = ["argo", "avatar", "breaking_bad", "brick_mansions", "crazy_stupid_love", 33 | "descendants", "gangs_of_new_york", "good_night_and_good_luck", "quantum_of_solace", 34 | "slumdog_millionaire", "the_kings_speech"]; 35 | var repeats = 2; 36 | while(--repeats) { 37 | boxes = boxes.concat(boxes); 38 | } 39 | var x = 0, y = 0; 40 | var tex_width = 186; 41 | var tex_height = 270; 42 | var scale = 0.5; 43 | var box_width = tex_width * scale; 44 | var box_height = tex_height * scale; 45 | var covers = boxes.map(function(box) { 46 | var box = UI.new(Square, { 47 | top: y, 48 | left: x, 49 | width: box_width, 50 | height: box_height, 51 | background: "boxes/box_"+box+".png" 52 | }); 53 | //console.info(box); 54 | x += box_width; 55 | if(x > 1000-box_width) { 56 | x = 0; 57 | y += box_height; 58 | } 59 | return box; 60 | }); 61 | //console.info(covers); 62 | var args = [Square, { 63 | name: "background-colored", 64 | top: 0, 65 | left: 0, 66 | width: 1000, 67 | height: 700 68 | }].concat(covers); 69 | return UI.new.apply(this, args); 70 | } 71 | }); 72 | 73 | UI.render(BigView, document.getElementById("app")); 74 | 75 | var fps = document.getElementById("fps"); 76 | -------------------------------------------------------------------------------- /lib/texture.js: -------------------------------------------------------------------------------- 1 | var Texture = (function() { 2 | var textureCache = {}; 3 | var x_offset = 0; 4 | var y_offset = 0; 5 | var atlas_width = 1024; 6 | var atlas_height = 1024; 7 | var biggest_height = 0; 8 | var texture; 9 | return { 10 | initTexture: function(gl, url, done) { 11 | if(textureCache[url]) { 12 | //console.info("Cache hit for texture"); 13 | if(textureCache[url].uvs) { 14 | //console.info("Returning with loaded texture"); 15 | setTimeout(function() { 16 | done(textureCache[url]); 17 | }, 0); 18 | } else { 19 | //console.info("Notifying when done"); 20 | textureCache[url].listeners.push(done); 21 | } 22 | return; 23 | } 24 | if(!texture) { 25 | console.info("Creating texture atlas"); 26 | texture = gl.createTexture(); 27 | gl.bindTexture(gl.TEXTURE_2D, texture); 28 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, atlas_width, atlas_height, 0, gl.RGBA, 29 | gl.UNSIGNED_BYTE, null); 30 | } 31 | var image = new Image(); 32 | var atlassedTexture = {url: url, listeners: []}; 33 | 34 | image.onload = function() { 35 | //console.log("Texture loaded: ", url, texture); 36 | //gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image); 37 | var width = this.width; 38 | var height = this.height; 39 | //console.info("Adding to atlas of texture0 with "+width+", "+height+" at "+x_offset+", "+y_offset); 40 | 41 | if(x_offset+width > atlas_width) { 42 | x_offset = 0; 43 | y_offset += biggest_height; 44 | } 45 | if(y_offset+height > atlas_height) { 46 | console.error("Doesn't fit into atlas"); 47 | } 48 | 49 | var uvs = [ 50 | x_offset/atlas_width, y_offset/atlas_height, 51 | (x_offset+width)/atlas_width, y_offset/atlas_height, 52 | x_offset/atlas_width, (y_offset+height)/atlas_height, 53 | (x_offset+width)/atlas_width, (y_offset+height)/atlas_height]; 54 | gl.texSubImage2D(gl.TEXTURE_2D, 0, x_offset, y_offset, 55 | gl.RGBA, gl.UNSIGNED_BYTE, image); 56 | x_offset += width; 57 | if(height > biggest_height) { 58 | biggest_height = height; 59 | } 60 | 61 | //Note: to support NPOT textures we apply these settings. 62 | //For perf reasons, maybe this should be explicitly requested? 63 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 64 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 65 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 66 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 67 | 68 | atlassedTexture.texture = texture; 69 | atlassedTexture.uvs = uvs; 70 | done(atlassedTexture); 71 | atlassedTexture.listeners.forEach(function(listener) { 72 | listener(atlassedTexture); 73 | }); 74 | } 75 | 76 | image.src = url; 77 | textureCache[url] = atlassedTexture; 78 | 79 | return texture; 80 | } 81 | } 82 | })(); -------------------------------------------------------------------------------- /lib/shader.js: -------------------------------------------------------------------------------- 1 | var Shader = (function() { 2 | var presets = { 3 | fragmentStd: 4 | 'precision mediump float;' + 5 | 6 | 'varying vec2 vTextureCoord;' + 7 | 'varying vec4 vColor;' + 8 | 9 | 'uniform sampler2D uSampler;' + 10 | 'uniform float uUseTexture;' + 11 | 12 | 'void main(void) {' + 13 | ' vec4 texColor = texture2D(uSampler, vTextureCoord) * uUseTexture;' + 14 | ' vec4 vertColor = vColor * (1.0 - uUseTexture);' + 15 | ' gl_FragColor = texColor + vertColor;' + 16 | '}', 17 | vertexStd: 18 | 'attribute vec3 aVertexPosition;' + 19 | 'attribute vec4 aVertexColor;' + 20 | 'attribute vec2 aTextureCoord;' + 21 | 22 | 'uniform mat4 uMVMatrix;' + 23 | 'uniform mat4 uPMatrix;' + 24 | 25 | 'varying vec4 vColor;' + 26 | 'varying vec2 vTextureCoord;' + 27 | 28 | 'void main(void) {' + 29 | ' gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);' + 30 | ' vTextureCoord = aTextureCoord;' + 31 | ' vColor = aVertexColor;' + 32 | '}' 33 | }; 34 | 35 | /** 36 | * gl: WebGL context 37 | * type: one of GL.FRAGMENT_SHADER or GL.VERTEX_SHADER 38 | * str: string containing shader source 39 | */ 40 | var buildShader = function(gl, type, str) { 41 | var shader = gl.createShader(type); 42 | gl.shaderSource(shader, str); 43 | gl.compileShader(shader); 44 | 45 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 46 | console.error(gl.getShaderInfoLog(shader)); 47 | return; 48 | } 49 | 50 | return shader; 51 | }; 52 | 53 | return { 54 | createShaders: function(gl, fragmentShaderPresetId, vertexShaderPresetId) { 55 | var fragmentShaderSource = presets[fragmentShaderPresetId]; 56 | var vertexShaderSource = presets[vertexShaderPresetId]; 57 | var fragmentShader = buildShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); 58 | var vertexShader = buildShader(gl, gl.VERTEX_SHADER, vertexShaderSource); 59 | 60 | var shaderProgram = gl.createProgram(); 61 | gl.attachShader(shaderProgram, vertexShader); 62 | gl.attachShader(shaderProgram, fragmentShader); 63 | gl.linkProgram(shaderProgram); 64 | 65 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 66 | console.error("Could not initialise shaders"); 67 | return; 68 | } 69 | 70 | gl.useProgram(shaderProgram); 71 | 72 | shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); 73 | gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); 74 | 75 | shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord"); 76 | gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); 77 | 78 | shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); 79 | shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); 80 | shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler"); 81 | 82 | shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor"); 83 | gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute); 84 | 85 | shaderProgram.uUseTexture = gl.getUniformLocation(shaderProgram, "uUseTexture"); 86 | 87 | return shaderProgram; 88 | } 89 | } 90 | })(); -------------------------------------------------------------------------------- /lib/gl.js: -------------------------------------------------------------------------------- 1 | var GL = (function() { 2 | var lastBoundTexture; 3 | var lastBoundBufferId; 4 | var staticBuffer; 5 | var bufferId = 0; 6 | 7 | var addToStaticBuffer = function(gl, width, height, backgroundColor, customUvs) { 8 | if(!staticBuffer) { 9 | var squareVertexPositionBuffer; 10 | var squareVertexColorBuffer; 11 | var squareVertexTextureCoordBuffer; 12 | 13 | squareVertexPositionBuffer = gl.createBuffer(); 14 | gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); 15 | 16 | var vertices = [ 17 | width, height, 0.0, 18 | 0.0, height, 0.0, 19 | width, 0.0, 0.0, 20 | 0.0, 0.0, 0.0 21 | ]; 22 | 23 | //console.info("Verts: ", vertices); 24 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 25 | squareVertexPositionBuffer.itemSize = 3; 26 | squareVertexPositionBuffer.numItems = 4; 27 | 28 | squareVertexTextureCoordBuffer = gl.createBuffer(); 29 | gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexTextureCoordBuffer); 30 | var textureCoords = customUvs || [ 31 | 0.0, 0.0, 32 | 1.0, 0.0, 33 | 0.0, 1.0, 34 | 1.0, 1.0]; 35 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW); 36 | squareVertexTextureCoordBuffer.itemSize = 2; 37 | squareVertexTextureCoordBuffer.numItems = 4; 38 | 39 | squareVertexColorBuffer = gl.createBuffer(); 40 | gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer); 41 | colors = []; 42 | var color = [0.5, 0.5, 1.0, 1.0]; 43 | if(backgroundColor) { 44 | color = backgroundColor; 45 | } 46 | for (var i=0; i < 4; i++) { 47 | colors = colors.concat(color); 48 | } 49 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); 50 | squareVertexColorBuffer.itemSize = 4; 51 | squareVertexColorBuffer.numItems = 4; 52 | staticBuffer = { 53 | id: bufferId, 54 | position: squareVertexPositionBuffer, 55 | color: squareVertexColorBuffer, 56 | texture: squareVertexTextureCoordBuffer 57 | }; 58 | bufferId++; 59 | } else { 60 | //TODO: buffer already exists. Add the vertices for this quad into it. 61 | } 62 | return staticBuffer; 63 | }; 64 | 65 | return { 66 | setMatrixUniforms: function(gl, shaderProgram, pMatrix, mvMatrix, texture) { 67 | gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix); 68 | gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix); 69 | gl.uniform1f(shaderProgram.uUseTexture, texture ? 1.0 : 0.0); 70 | }, 71 | 72 | createBuffers: function(gl, width, height, backgroundColor, customUvs, isStatic) { 73 | if(isStatic) { 74 | return addToStaticBuffer(gl, width, height, backgroundColor, customUvs); 75 | } 76 | var squareVertexPositionBuffer; 77 | var squareVertexColorBuffer; 78 | var squareVertexTextureCoordBuffer; 79 | 80 | squareVertexPositionBuffer = gl.createBuffer(); 81 | gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); 82 | 83 | var vertices = [ 84 | width, height, 0.0, 85 | 0.0, height, 0.0, 86 | width, 0.0, 0.0, 87 | 0.0, 0.0, 0.0 88 | ]; 89 | 90 | //console.info("Verts: ", vertices); 91 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 92 | squareVertexPositionBuffer.itemSize = 3; 93 | squareVertexPositionBuffer.numItems = 4; 94 | 95 | squareVertexTextureCoordBuffer = gl.createBuffer(); 96 | gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexTextureCoordBuffer); 97 | var textureCoords = customUvs || [ 98 | 0.0, 0.0, 99 | 1.0, 0.0, 100 | 0.0, 1.0, 101 | 1.0, 1.0]; 102 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW); 103 | squareVertexTextureCoordBuffer.itemSize = 2; 104 | squareVertexTextureCoordBuffer.numItems = 4; 105 | 106 | squareVertexColorBuffer = gl.createBuffer(); 107 | gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer); 108 | colors = []; 109 | var color = [0.5, 0.5, 1.0, 1.0]; 110 | if(backgroundColor) { 111 | color = backgroundColor; 112 | } 113 | for (var i=0; i < 4; i++) { 114 | colors = colors.concat(color); 115 | } 116 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); 117 | squareVertexColorBuffer.itemSize = 4; 118 | squareVertexColorBuffer.numItems = 4; 119 | var buffer = { 120 | id: bufferId, 121 | position: squareVertexPositionBuffer, 122 | color: squareVertexColorBuffer, 123 | texture: squareVertexTextureCoordBuffer 124 | }; 125 | bufferId++; 126 | return buffer; 127 | }, 128 | 129 | clear: function(gl) { 130 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 131 | lastBoundBufferId = -1; 132 | }, 133 | 134 | initDraw: function(gl, pMatrix, mvMatrix) { 135 | gl.clearColor(0.0, 0.0, 0.0, 1.0); 136 | gl.enable(gl.DEPTH_TEST); 137 | gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); 138 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 139 | mat4.ortho(pMatrix, 0, gl.viewportWidth, 0, gl.viewportHeight, 0.1, 100.0); 140 | }, 141 | 142 | drawBuffers: function(gl, pMatrix, mvMatrix, shaderProgram, buffers, texture) { 143 | if(buffers.id === lastBoundBufferId) { 144 | return; 145 | } 146 | window.drawCalls++; 147 | var positionBuffer = buffers.position; 148 | var colorBuffer = buffers.color; 149 | var textureBuffer = buffers.texture; 150 | 151 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 152 | gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, positionBuffer.itemSize, gl.FLOAT, false, 0, 0); 153 | 154 | gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer); 155 | gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, textureBuffer.itemSize, gl.FLOAT, false, 0, 0); 156 | 157 | gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); 158 | gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, colorBuffer.itemSize, gl.FLOAT, false, 0, 0); 159 | 160 | if(lastBoundTexture && texture.url !== lastBoundTexture.url) { 161 | window.textureBinds++; 162 | gl.activeTexture(gl.TEXTURE0); 163 | gl.bindTexture(gl.TEXTURE_2D, texture.texture); 164 | lastBoundTexture = texture; 165 | } 166 | gl.uniform1i(shaderProgram.samplerUniform, 0); 167 | 168 | GL.setMatrixUniforms(gl, shaderProgram, pMatrix, mvMatrix, texture); 169 | 170 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, positionBuffer.numItems); 171 | lastBoundBufferId = buffers.id; 172 | } 173 | } 174 | })(); -------------------------------------------------------------------------------- /lib/ui.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var GLContext = function(canvas) { 4 | try { 5 | //TODO: refactor the hardcoded strings in this block 6 | this.gl = canvas.getContext("experimental-webgl"); 7 | this.gl.viewportWidth = canvas.width; 8 | this.gl.viewportHeight = canvas.height; 9 | this.shaders = Shader.createShaders(this.gl, "fragmentStd", "vertexStd"); 10 | this.mvMatrix = mat4.create(); 11 | this.pMatrix = mat4.create(); 12 | } catch (e) { 13 | console.error(e); 14 | } 15 | }; 16 | 17 | GLContext.prototype.get = function() { 18 | return this.gl; 19 | }; 20 | 21 | GLContext.prototype.getMvMatrix = function() { 22 | return this.mvMatrix; 23 | }; 24 | 25 | GLContext.prototype.getpMatrix = function() { 26 | return this.pMatrix; 27 | }; 28 | 29 | GLContext.prototype.getShaders = function() { 30 | return this.shaders; 31 | }; 32 | 33 | var UI = (function() { 34 | var updateNode = function(node, dt) { 35 | if(node.update) { 36 | node.update(); 37 | } 38 | if(node.components) { 39 | node.components.forEach(function(component) { 40 | if(node[component.name].update) { 41 | node[component.name].update(dt); 42 | } 43 | }); 44 | } 45 | }; 46 | 47 | var lastTime = Date.now(); 48 | var renderNode = function(node, inheritedPosition, root, dt) { 49 | if(node.props) { 50 | //console.log("RENDER NODE: ", node.props.name, node.options.background); 51 | //TODO: only on initialise. 52 | if(node.props.backgroundColor) { 53 | node._backgroundColor = node.props.backgroundColor; 54 | } 55 | var isStatic = !node.Animator; 56 | if(node.props.background) { 57 | if(!node._background) { 58 | node._background = node.props.background; 59 | //console.log("Init texture..."); 60 | Texture.initTexture(root._context.get(), node._background, function(atlassedTexture) { 61 | node._atlassedTexture = atlassedTexture; 62 | node._textureLoaded = true; 63 | // TODO: figure out a nice fully async way to do createBuffers and merge 64 | // it with the call further down 65 | node._buffers = GL.createBuffers(root._context.get(), 66 | node._width, node._height, node._backgroundColor, 67 | node._atlassedTexture.uvs, isStatic); 68 | 69 | //console.log("Texture loaded."); 70 | }); 71 | } 72 | } 73 | if(!node._buffers && !node._background) { 74 | //console.log("Creating buffers"); 75 | node._buffers = GL.createBuffers(root._context.get(), 76 | node._width, node._height, node._backgroundColor, isStatic); 77 | } 78 | 79 | //console.log("Has props, let's draw it ", node.options.buffers); 80 | 81 | var top = node.props.top; 82 | var left = node.props.left; 83 | if(inheritedPosition) { 84 | if(inheritedPosition.top) { 85 | top += inheritedPosition.top; 86 | } 87 | if(inheritedPosition.left) { 88 | left += inheritedPosition.left; 89 | } 90 | } 91 | //console.log("T: ",[left, root._height-(top+node._height), -1.0]); 92 | 93 | mat4.identity(node.tm); 94 | mat4.translate(node.tm, node.tm, vec3.fromValues(left, root._height-(top+node._height), -1.0)); 95 | mat4.translate(node.tm, node.tm, node.localPosition); 96 | var draw = function() { 97 | GL.drawBuffers(root._context.get(), 98 | root._context.getpMatrix(), 99 | node.tm, 100 | root._context.getShaders(), 101 | node._buffers, 102 | node._atlassedTexture); 103 | }; 104 | // update node if necessary 105 | updateNode(node, dt); 106 | 107 | if(!node._atlassedTexture && node._backgroundColor) { 108 | //console.info(node.props, " has no background drawing now! ", node.options); 109 | draw(); 110 | } else if(node._atlassedTexture && node._textureLoaded) { 111 | //console.log("Drawing textured node"); 112 | draw(); 113 | } 114 | } 115 | node.children.forEach(function(child) { 116 | //console.info("Rendering child: ", child.options.background, child.props.background); 117 | renderNode(child, {top: top, left: left}, root, dt); 118 | }); 119 | node.render(); 120 | }; 121 | return { 122 | /** 123 | Returns a factory method to create an instance of this component. 124 | */ 125 | component: function(def) { 126 | return function(props) { 127 | var _this = Object.create(def); 128 | _this.tm = mat4.create(); 129 | _this.localPosition = vec3.fromValues(0, 0, -1.0); 130 | //mat4.translate(_this.lt, _this.lt, vec3.fromValues(0, 0, -1.0)); 131 | //console.info("this.tm = ", _this.tm); 132 | if(_this.start) { 133 | _this.start(); 134 | } 135 | if(_this.components) { 136 | _this.components.forEach(function(component) { 137 | _this[component.name] = Object.create(component); 138 | _this[component.name].root = _this; 139 | if(_this[component.name].start) { 140 | _this[component.name].start(); 141 | } 142 | }.bind(_this)); 143 | } 144 | return _this; 145 | } 146 | }, 147 | new: function(component, props) { 148 | //1. clone def 149 | //2. set props 150 | //3. init matrices 151 | //4. call lifecycle method(s) 152 | var component = arguments[0]; 153 | var props = arguments[1]; 154 | var inst = component(props); 155 | inst.props = props; 156 | inst._width = props.width; 157 | inst._height = props.height; 158 | inst.children = []; 159 | //console.log("INST: ", inst); 160 | for(var i=2; i