├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── example ├── demo2Frag.glsl ├── demo2Vert.glsl ├── index.js └── test.glsl ├── font_atlas.js ├── font_info.js ├── images ├── demo_screen.png ├── tut_button.png ├── tut_checkbox.png ├── tut_gui.png ├── tut_radio.png ├── tut_radio_lines.png ├── tut_rgb.png ├── tut_separator.png └── tut_xyz.png ├── index.js ├── package.json └── shaders.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.json 3 | *.fnt 4 | node_modules/ 5 | out_0.png 6 | font_bitmap.png 7 | out_pretty.js 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pnp-gui 2 | 3 | pnp-gui(plug n' play GUI) is a WebGL gui toolkit whose main goal is to be easy to use, bloat-free, and be easy to 4 | integrate into any project. Impatient people probably want a demo right away, 5 | and [here](https://github.com/Erkaman/pnp-gui#demo) it is. 6 | 7 | pnp-gui takes much inspiration from the fantastic [imgui](https://github.com/ocornut/imgui) 8 | project. Like imgui, it provides an Immediate Mode GUI toolkit. Also like imgui, 9 | it aims to be as bloat-free as possible, and to be easy to integrate into 10 | any project. Very little code needs to be written to create a simple GUI, 11 | and a font comes pre-packaged with the library. 12 | 13 | pnp-gui is mainly meant to be used by programmers who want to make a simple GUI 14 | for debugging purposes, or who want a simple GUI that can be used 15 | to tweak a couple of variables in their applications. Also, in the provided demo, 16 | some possible use-cases of pnp-gui are shown. 17 | 18 | However, do note that due to the simplicity of the toolkit, there are many drawbacks. 19 | pnp-gui does not provide you with any way to do advanced widget and window layout. 20 | More advanced GUI toolkits such as Swing and QT provides ways to create advanced 21 | window layouts. However, in order to reduce complexity, the window layout 22 | options of pnp-gui have been made very limited. 23 | 24 | Another important thing to note is that pnp-gui does not provide any way of 25 | creating beautiful interfaces that can be presented to the end user. You can 26 | tweak a couple of colors and spacing constants here and there, but there is 27 | no support at all for skinning the GUI. Again, we empathize that the toolkit 28 | is mainly meant to be used by programmers who want a simple GUI for debugging 29 | purposes. 30 | 31 | [![NPM](https://nodei.co/npm/pnp-gui.png)](https://www.npmjs.com/package/pnp-gui) 32 | 33 | 34 | ## Demo 35 | 36 | A demo is given at this link: 37 | http://erkaman.github.io/pnp-gui/ 38 | 39 | It should look like this: 40 | 41 | ![text](images/demo_screen.png) 42 | 43 | First, note that the GUI in the demo is all rendered using WebGL, and no HTML is used at all. It is actually two demos in one, and the demo can be toggled with the upper radio button. 44 | In the first demo, you can use the widgets of pnp-gui to change the lighting and color 45 | settings of a simple model. In the second demo, you can use the widgets 46 | of pnp-gui to modify the lighting, color and geometry of a simple heightmap. Note that you can move the 47 | window by dragging and dropping. 48 | 49 | And the source code of the demo can be found at `example/index.js` 50 | 51 | See also [here](https://github.com/Erkaman/glsl-godrays#readme) for an example of an actual application using pnp-gui 52 | 53 | ## Tutorial 54 | 55 | In this section, we give a tutorial that demonstrates how easy it is to use 56 | the toolkit. First we give the source code of another simple demo: 57 | 58 | ```javascript 59 | /* global requestAnimationFrame */ 60 | 61 | var bunny = require('bunny'); 62 | var mat4 = require('gl-mat4'); 63 | var vec3 = require('gl-vec3'); 64 | var Geometry = require('gl-geometry'); 65 | var glShader = require('gl-shader'); 66 | var normals = require('normals'); 67 | var glslify = require('glslify') 68 | var createOrbitCamera = require('orbit-camera'); 69 | var shell = require("gl-now")(); 70 | var createGui = require("pnp-gui"); 71 | var cameraPosFromViewMatrix = require('gl-camera-pos-from-view-matrix'); 72 | var dragon = require('stanford-dragon/3'); 73 | var boundingBox = require('vertices-bounding-box'); 74 | var transform = require('geo-3d-transform-mat4'); 75 | var randomArray = require('random-array'); 76 | 77 | var demo1Shader, bunnyGeo, dragonGeo; 78 | 79 | var camera = createOrbitCamera([0, -1000, 0], [0, 0, 0], [0, 1, 0]); 80 | 81 | var mouseLeftDownPrev = false; 82 | 83 | const RENDER_BUNNY = 0; 84 | const RENDER_DRAGON = 1; 85 | 86 | /* 87 | Variables that can be modified through the GUI. 88 | */ 89 | 90 | var bg = [0.6, 0.7, 1.0]; // clear color. 91 | 92 | var demo1DiffuseColor = [0.42, 0.34, 0.0]; 93 | var demo1AmbientLight = [0.77, 0.72, 0.59]; 94 | var demo1LightColor = [0.40, 0.47, 0.0]; 95 | var demo1SunDir = [-0.69, 1.33, 0.57]; 96 | var demo1SpecularPower = {val: 12.45}; 97 | var demo1HasSpecular = {val: true}; 98 | var demo1RenderModel = {val: RENDER_BUNNY}; 99 | 100 | const demo1Vert = ` 101 | precision mediump float; 102 | 103 | attribute vec3 aPosition; 104 | attribute vec3 aNormal; 105 | 106 | varying vec3 vNormal; 107 | varying vec3 vPosition; 108 | 109 | uniform mat4 uProjection; 110 | uniform mat4 uView; 111 | 112 | void main() { 113 | vNormal = aNormal; 114 | vPosition = aPosition; 115 | 116 | gl_Position = uProjection * uView * vec4(aPosition, 1.0); 117 | } 118 | ` 119 | 120 | const demo1Frag = ` 121 | precision mediump float; 122 | 123 | varying vec3 vNormal; 124 | varying vec3 vPosition; 125 | 126 | uniform vec3 uDiffuseColor; 127 | uniform vec3 uAmbientLight; 128 | uniform vec3 uLightColor; 129 | uniform vec3 uLightDir; 130 | uniform vec3 uEyePos; 131 | uniform mat4 uView; 132 | uniform float uSpecularPower; 133 | uniform float uHasSpecular; 134 | 135 | void main() { 136 | vec3 n = vNormal; 137 | vec3 l = normalize(uLightDir); 138 | vec3 v = normalize(uEyePos - vPosition); 139 | 140 | vec3 ambient = uAmbientLight * uDiffuseColor; 141 | vec3 diffuse = uDiffuseColor * uLightColor * dot(n, l) ; 142 | vec3 specular = pow(clamp(dot(normalize(l+v),n),0.0,1.0) , uSpecularPower) * vec3(1.0,1.0,1.0); 143 | 144 | gl_FragColor = vec4(ambient + diffuse + specular*uHasSpecular, 1.0); 145 | } 146 | ` 147 | 148 | // center geometry on (0,0,0) 149 | function centerGeometry(geo, scale) { 150 | 151 | // Calculate the bounding box. 152 | var bb = boundingBox(geo.positions); 153 | 154 | // Translate the geometry center to the origin. 155 | var translate = [-0.5 * (bb[0][0] + bb[1][0]), -0.5 * (bb[0][1] + bb[1][1]), -0.5 * (bb[0][2] + bb[1][2])]; 156 | var m = mat4.create(); 157 | mat4.scale(m, m, [scale, scale, scale]); 158 | mat4.translate(m, m, translate); 159 | 160 | geo.positions = transform(geo.positions, m) 161 | } 162 | 163 | shell.on("gl-init", function () { 164 | var gl = shell.gl 165 | 166 | gl.enable(gl.DEPTH_TEST); 167 | gl.enable(gl.CULL_FACE); 168 | 169 | gui = new createGui(gl) 170 | gui.windowSizes = [360, 580]; 171 | 172 | centerGeometry(bunny, 80.0); 173 | bunnyGeo = Geometry(gl) 174 | .attr('aPosition', bunny.positions) 175 | .attr('aNormal', normals.vertexNormals(bunny.cells, bunny.positions)) 176 | .faces(bunny.cells) 177 | 178 | centerGeometry(dragon, 16.0); 179 | dragonGeo = Geometry(gl) 180 | .attr('aPosition', dragon.positions) 181 | .attr('aNormal', normals.vertexNormals(dragon.cells, dragon.positions)) 182 | .faces(dragon.cells) 183 | 184 | demo1Shader = glShader(gl, demo1Vert, demo1Frag); 185 | }); 186 | 187 | function demo1Randomize() { 188 | demo1DiffuseColor = randomArray(0, 1).oned(3); 189 | demo1AmbientLight = randomArray(0, 1).oned(3); 190 | demo1LightColor = randomArray(0, 1).oned(3); 191 | demo1SunDir = randomArray(-2, +2).oned(3); 192 | demo1SpecularPower.val = Math.round(randomArray(0, 40).oned(1)[0]); 193 | } 194 | 195 | shell.on("gl-render", function (t) { 196 | var gl = shell.gl 197 | var canvas = shell.canvas; 198 | 199 | gl.clearColor(bg[0], bg[1], bg[2], 1); 200 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 201 | gl.viewport(0, 0, canvas.width, canvas.height); 202 | 203 | var model = mat4.create(); 204 | var projection = mat4.create(); 205 | var scratchMat = mat4.create(); 206 | var view = camera.view(scratchMat); 207 | var scratchVec = vec3.create(); 208 | 209 | mat4.perspective(projection, Math.PI / 2, canvas.width / canvas.height, 0.1, 10000.0); 210 | 211 | demo1Shader.bind(); 212 | 213 | demo1Shader.uniforms.uView = view; 214 | demo1Shader.uniforms.uProjection = projection; 215 | demo1Shader.uniforms.uDiffuseColor = demo1DiffuseColor; 216 | demo1Shader.uniforms.uAmbientLight = demo1AmbientLight; 217 | demo1Shader.uniforms.uLightColor = demo1LightColor; 218 | demo1Shader.uniforms.uLightDir = demo1SunDir; 219 | demo1Shader.uniforms.uEyePos = cameraPosFromViewMatrix(scratchVec, view); 220 | demo1Shader.uniforms.uSpecularPower = demo1SpecularPower.val; 221 | demo1Shader.uniforms.uHasSpecular = demo1HasSpecular.val ? 1.0 : 0.0; 222 | 223 | if (demo1RenderModel.val == RENDER_BUNNY) { 224 | bunnyGeo.bind(demo1Shader); 225 | bunnyGeo.draw(); 226 | } else if (demo1RenderModel.val == RENDER_DRAGON) { 227 | dragonGeo.bind(demo1Shader); 228 | dragonGeo.draw(); 229 | } 230 | 231 | var pressed = shell.wasDown("mouse-left"); 232 | var io = { 233 | mouseLeftDownCur: pressed, 234 | mouseLeftDownPrev: mouseLeftDownPrev, 235 | 236 | mousePositionCur: shell.mouse, 237 | mousePositionPrev: shell.prevMouse 238 | }; 239 | mouseLeftDownPrev = pressed; 240 | 241 | gui.begin(io, "Window"); 242 | 243 | 244 | gui.textLine("Lighting Settings"); 245 | 246 | gui.radioButton("Bunny", demo1RenderModel, RENDER_BUNNY); 247 | gui.sameLine(); 248 | gui.radioButton("Dragon", demo1RenderModel, RENDER_DRAGON); 249 | 250 | gui.draggerRgb("Ambient Light", demo1AmbientLight); 251 | gui.draggerRgb("Diffuse Color", demo1DiffuseColor); 252 | gui.draggerRgb("Light Color", demo1LightColor); 253 | 254 | gui.checkbox("Has Specular Lighting", demo1HasSpecular); 255 | if (demo1HasSpecular.val) 256 | gui.sliderFloat("Specular Power", demo1SpecularPower, 0, 40); 257 | 258 | gui.draggerFloat3("Light Direction", demo1SunDir, [-2, +2], ["X:", "Y:", "Z:"]); 259 | 260 | if (gui.button("Randomize")) { 261 | demo1Randomize(); 262 | } 263 | 264 | gui.separator(); 265 | 266 | gui.textLine("Miscellaneous"); 267 | gui.draggerRgb("Background", bg); 268 | 269 | gui.end(gl, canvas.width, canvas.height); 270 | }); 271 | 272 | shell.on("tick", function () { 273 | 274 | // if interacting with the GUI, do not let the mouse control the camera. 275 | if (gui.hasMouseFocus()) 276 | return; 277 | 278 | if (shell.wasDown("mouse-left")) { 279 | var speed = 2.0; 280 | camera.rotate([(shell.mouseX / shell.width - 0.5) * speed, (shell.mouseY / shell.height - 0.5) * speed], 281 | [(shell.prevMouseX / shell.width - 0.5) * speed, (shell.prevMouseY / shell.height - 0.5) * speed]) 282 | } 283 | if (shell.scroll[1]) { 284 | camera.zoom(shell.scroll[1] * 0.6); 285 | } 286 | }); 287 | ``` 288 | 289 | the above demo results in the following GUI: 290 | 291 | 292 | 293 | 294 | Let us now go through the important parts of the above demo. 295 | First of all, the code that creates the gui object: 296 | 297 | ```javascript 298 | gui = new createGui(gl) 299 | gui.windowSizes = [360, 580]; 300 | ``` 301 | 302 | In the first line, we simply create the gui object. In the second line, we set the 303 | window sizes of the GUI. As remarked in the Introduction, we do not provide any means of doing advanced 304 | GUI layout. So if the window does not fit all widgets you place out, 305 | you will have to solve this by setting the value of `gui.windowSizes` so that all widgets fit. 306 | Note that you can tweak this value at any time, not just after creation. 307 | 308 | After creating the gui object, we are now ready to use it. You need to first provide png-gui with IO-information: 309 | 310 | ```javascript 311 | var pressed = shell.wasDown("mouse-left"); 312 | var io = { 313 | mouseLeftDownCur: pressed, 314 | mouseLeftDownPrev: mouseLeftDownPrev, 315 | 316 | mousePositionCur: shell.mouse, 317 | mousePositionPrev: shell.prevMouse 318 | }; 319 | mouseLeftDownPrev = pressed; 320 | ``` 321 | 322 | before you can start placing out the widgets, you must provide the toolkit 323 | with IO-information. Currently, the toolkit only needs the current mouse position, 324 | the mouse position in the previous frame, whether the left mouse button is 325 | down the current frame, and whether the left mouse button was down the previous frame. 326 | And we put all information into a single `io` object. We then pass this 327 | object to pnp-gui 328 | 329 | ```javascript 330 | gui.begin(io, "Window"); 331 | ``` 332 | 333 | where we specify the io-information, and the window title. After this method 334 | has been called, we can start placing out GUI widgets: 335 | 336 | ```javascript 337 | gui.textLine("Lighting Settings"); 338 | 339 | gui.radioButton("Bunny", demo1RenderModel, RENDER_BUNNY); 340 | gui.sameLine(); 341 | gui.radioButton("Dragon", demo1RenderModel, RENDER_DRAGON); 342 | 343 | gui.draggerRgb("Ambient Light", demo1AmbientLight); 344 | gui.draggerRgb("Diffuse Color", demo1DiffuseColor); 345 | gui.draggerRgb("Light Color", demo1LightColor); 346 | 347 | gui.checkbox("Has Specular Lighting", demo1HasSpecular); 348 | if (demo1HasSpecular.val) 349 | gui.sliderFloat("Specular Power", demo1SpecularPower, 0, 40); 350 | 351 | gui.draggerFloat3("Light Direction", demo1SunDir, [-2, +2], ["X:", "Y:", "Z:"]); 352 | 353 | if (gui.button("Randomize")) { 354 | demo1Randomize(); 355 | } 356 | 357 | 358 | gui.separator(); 359 | 360 | gui.textLine("Miscellaneous"); 361 | gui.draggerRgb("Background", bg); 362 | ``` 363 | 364 | Let us step by step go through the above. By calling 365 | 366 | ```javascript 367 | gui.textLine("Lighting Settings"); 368 | ``` 369 | 370 | we can simply write out a line of text. 371 | 372 | Let us look at the next couple lines. 373 | These lines renders two radio buttons that are used to choose the model that 374 | is being rendered. 375 | 376 | ```javascript 377 | gui.radioButton("Bunny", demo1RenderModel, RENDER_BUNNY); 378 | gui.sameLine(); 379 | gui.radioButton("Dragon", demo1RenderModel, RENDER_DRAGON); 380 | ``` 381 | 382 | the above lines results in: 383 | 384 | 385 | 386 | 387 | Note here that `demo1RenderModel` is defined as 388 | 389 | ``` 390 | var demo1RenderModel = {val: RENDER_BUNNY}; 391 | ``` 392 | 393 | Since `demo1RenderModel.val` has value `RENDER_BUNNY`, and because first radio button 394 | also has the value `RENDER_BUNNY`, the first radio button is rendered as filled in. 395 | However, if the user clicks on the second radio button, `demo1RenderModel.val` 396 | will, by the toolkit, be modified to `RENDER_DRAGON`, and thus the second 397 | radio button is rendered as filled in. 398 | 399 | It is also easy to add another radio button. Just add another line 400 | 401 | ```javascript 402 | gui.radioButton("Cat", demo1RenderModel, RENDER_CAT); 403 | ``` 404 | 405 | and, if `RENDER_CAT` has a value distinct from `RENDER_BUNNY` and `RENDER_BUNNY`, it will work. 406 | 407 | Finally, note that between the two calls to `radioButton`, we are calling 408 | `sameLine`. This makes it so that the two radio buttons are put on the same line. Otherwise, they would have been placed on different lines. Like this: 409 | 410 | 411 | 412 | `sameLine` is a very flexible function, and can be used with any two widgets! 413 | 414 | Let us now look at the next three lines of the GUI. Here we are using three `draggerRgb` widgets to control the 415 | ambient light, diffuse color, and light color of the model: 416 | 417 | ```javascript 418 | gui.draggerRgb("Ambient Light", demo1AmbientLight); 419 | gui.draggerRgb("Diffuse Color", demo1DiffuseColor); 420 | gui.draggerRgb("Light Color", demo1LightColor); 421 | ``` 422 | 423 | the above results in: 424 | 425 | 426 | 427 | `draggerRgb` is a useful widget that can be used to let the user input a color. The input parameters to the function `draggerRgb` is 428 | a label string, and an array that contains the color being modified by the widget. Note, for instance, that 429 | `demo1DiffuseColor` is defined as 430 | 431 | ``` 432 | var demo1DiffuseColor = [0.42, 0.34, 0.0]; 433 | ``` 434 | 435 | and this array will be modified by the toolkit when the user manipulates the first of the three `draggerRgb` widgets. 436 | 437 | Let us now look at the next part of the GUI. Next, comes a checkbox that controls whether the model should have 438 | specular lighting. If it should have specular lighting, then we also render a slider widget that controls the specular power. 439 | This is all rendered with the following code: 440 | 441 | ```javascript 442 | gui.checkbox("Has Specular Lighting", demo1HasSpecular); 443 | if (demo1HasSpecular.val) 444 | gui.sliderFloat("Specular Power", demo1SpecularPower, 0, 40); 445 | ``` 446 | 447 | the above results in this: 448 | 449 | 450 | 451 | 452 | and especially note that if the checkbox is not checked, then the below slider widget is not rendered! 453 | 454 | The `checkbox` widget is a simple widget. If the user checks the checkbox, then `demo1SpecularPower.val` is `true`, othewise, it is `false`. The next couple of lines are however quite interesting 455 | 456 | ```javascript 457 | if (demo1HasSpecular.val) 458 | gui.sliderFloat("Specular Power", demo1SpecularPower, 0, 40); 459 | ``` 460 | 461 | this shows that in order to remove a widget in pnp-gui, all we have to do is not render the widget at all. And that's it. So if `demo1HasSpecular.val`, then render widget, otherwise do not render it at all. 462 | 463 | The `sliderFloat` is also a relatively simple widget. `demo1SpecularPower` is defined as 464 | 465 | ```javascript 466 | var demo1SpecularPower = {val: 12.45}; 467 | ``` 468 | 469 | and if the user manipulates `sliderFloat`, then `demo1SpecularPower.val` is modified. However, since the third and fourth parameters are `0` and `40`, the value is restricted to be in the range `[0,40]`. Also, if you want a slider widget for integer values, simply use `sliderInt`. 470 | 471 | Now let us look at the next line of code in the GUI. This is a widget that allow you to change the light direction 472 | of the light that is illuminating the model: 473 | 474 | ```javascript 475 | gui.draggerFloat3("Light Direction", demo1SunDir, [-2, +2], ["X:", "Y:", "Z:"]); 476 | ``` 477 | 478 | the code results in 479 | 480 | 481 | 482 | 483 | `draggerFloat3` is a very flexible widget. You can use it when you want to set three values in a single widget. 484 | As can be observed it is really a single widget composed of three subwidgets. 485 | And as has probably already been guessed, `demo1SunDir` is simply an array of three: 486 | 487 | ``` 488 | var demo1SunDir = [-0.69, 1.33, 0.57]; 489 | ``` 490 | 491 | The argument `[-2, +2]` specifies that the min and max values of all the three subwidgets is `-2` and `+2`. However, 492 | you can have even more fine-grained control of the subwidgets. If you instead pass `[[0,1], [-2,1], [-1,0] ` you 493 | can specify that the first subwidget specifies values in range `[0,1]`, the second one in range `[-2,1]` and the third one 494 | in range `[-1,0]`. 495 | 496 | Optionally, you can also specify labels for all the subwidgets. The argument `["X:", "Y:", "Z:"]` specifies a label for each 497 | of the three subwidgets. 498 | 499 | Finally, in addition to `draggerFloat3`, there are also the widgets `draggerFloat1`, `draggerFloat2` and `draggerFloat4`. But they work exactly like `draggerFloat3`, except that they handle arrays of size 1, 2, 4. 500 | 501 | Now let us look at the next lines of code in the Gui. Next, we have a button that randomizes all the values in the above widgets if you press it: 502 | 503 | ```javascript 504 | if (gui.button("Randomize")) { 505 | demo1Randomize(); 506 | } 507 | ``` 508 | 509 | it looks like this: 510 | 511 | 512 | 513 | 514 | As can be seen, `button` is very easy to use; if the button was pressed the current frame, return `true`, otherwise, return false `false`. 515 | 516 | Let us go to the next line in the GUI. It is a widget that places out a separator: 517 | 518 | ``` 519 | gui.separator(); 520 | ``` 521 | 522 | it is the gray line in the following image: 523 | 524 | 525 | 526 | 527 | 528 | As can be observed, the separator can be used to introduce logical groupings into a GUI. 529 | 530 | the remainder of the GUI code looks like this: 531 | 532 | ```javascript 533 | gui.textLine("Miscellaneous"); 534 | gui.draggerRgb("Background", bg); 535 | gui.end(gl, canvas.width, canvas.height); 536 | ``` 537 | 538 | the first two lines should already be perfectly understandable by now. So let us skip to the third line. In order to use pnp-gui, you 539 | must do the following every single frame: 540 | 541 | ``` 542 | gui.begin(io, "Window"); 543 | 544 | // place out widgets here.... 545 | 546 | gui.end(gl, canvas.width, canvas.height); 547 | ``` 548 | 549 | so you MUST place out the widgets every single frame. It will not work if you only place them out the first frame. 550 | This is because pnp-gui does, as is stated in the FAQ, not save any retained state. It will not remember where you 551 | placed out the widgets for you, instead the GUI is created on the fly every single frame. Therefore, you must specify 552 | the widgets every single frame. 553 | 554 | Now the tutorial is almost over. The last thing we would like to cover are these lines: 555 | 556 | ```javascript 557 | shell.on("tick", function () { 558 | 559 | // if interacting with the GUI, do not let the mouse control the camera. 560 | if (gui.hasMouseFocus()) 561 | return; 562 | 563 | if (shell.wasDown("mouse-left")) { 564 | var speed = 2.0; 565 | camera.rotate([(shell.mouseX / shell.width - 0.5) * speed, (shell.mouseY / shell.height - 0.5) * speed], 566 | [(shell.prevMouseX / shell.width - 0.5) * speed, (shell.prevMouseY / shell.height - 0.5) * speed]) 567 | } 568 | if (shell.scroll[1]) { 569 | camera.zoom(shell.scroll[1] * 0.6); 570 | } 571 | }); 572 | ``` 573 | 574 | these lines do the following: if the user is not interacting with the GUI, we let the user manipulate the camera using the 575 | mouse. However, if the user is interacting with the GUI, then `gui.hasMouseFocus()` will return true. If the user 576 | is using the GUI, we often do not want the user to be able to interact with the 3D view as well, so we do not allow that if `gui.hasMouseFocus()` returns `true`. 577 | 578 | So, by using `gui.hasMouseFocus()`, we can ensure that the user does not use the GUI and the 3D application at the same time. 579 | 580 | ## FAQ 581 | 582 | What is an Immediate Mode GUI? 583 | 584 | This basically means that there is no retained state in the GUI; that is to say, 585 | there are no objects that are used to store the widgets of the GUI. Instead the 586 | GUI is being created on the fly every single frame. While this may seem unnatural 587 | to people who have not used such a GUI before, this kind of design actually results in 588 | a GUI that is very intuitive to use for programmers. If you wish to see this in action, please 589 | see the [tutorial](https://github.com/Erkaman/pnp-gui#tutorial) 590 | 591 | Does pnp-gui support text input? 592 | 593 | This is not yet supported. 594 | 595 | Is it possible to change the default font? 596 | 597 | As was stated in the introduction, png-gui does not, by design, offer very many features when it comes to changing the visual appearance. 598 | Therefore, this feature is not supported. 599 | 600 | If you anyways wish to change the font, you will have to dig into the source code of the project. The font is stored as a font-atlas `.png` packed using `ndpack-image` and as a `.json` file of character info, and both files were generated by [this](https://github.com/Erkaman/small_proggy_font_creator) C++ program. 601 | 602 | Can you render unicode text strings with pnp-gui? 603 | 604 | Currently, the toolkit can only render text strings where the characters are in the range `0x20`-`0x7E` in the ASCII table. But for rendering English text, you do not need any other characters. If you use any other character than these, the result is undefined. 605 | 606 | Can you create multiple windows? 607 | 608 | Not yet supported. 609 | 610 | Can you change the font size? 611 | 612 | Not yet supported. 613 | 614 | To what degree can you customize the appearance of the GUI? 615 | 616 | This toolkit is not really meant to be used to create beautiful GUIs, but there are certain visual aspects that you can customize. There are many constants that control the spacing and colors of the GUI. To, for instance, change the transparency of the window, you simply do this: 617 | 618 | ``` 619 | gui.windowAlpha = 0.5; 620 | ``` 621 | 622 | The rest of the constants that can be customized are all listed in the function `_setupDefaultSettings` in `index.js`. Note that these constants can be customized at ANY time; you do not have to do all customization at the initialization of the GUI. 623 | 624 | 625 | 626 | ## API 627 | 628 | Note that this section should be treated as a quick reference, and if you want more details, you should read the [tutorial](https://github.com/Erkaman/pnp-gui#tutorial) 629 | 630 | 631 | ### `var gui = new require("pnp-gui")(gl);` 632 | 633 | create the GUI. 634 | 635 | * `gl` your WebGL context 636 | 637 | ### `gui.begin(io, windowTitle)` 638 | 639 | Creates a window where you can place out your widgets. Should always be called before you place out your widgets. 640 | 641 | * `io` an object that contains the properties 642 | * `mouseLeftDownCur` whether the left mouse button is down for the current frame. 643 | * `mouseLeftDownPrev` whether the left mouse button was down for the previous frame. 644 | * `mousePositionCur` the mouse position of the current frame 645 | * `mousePositionPrev` the mouse position of the previous frame 646 | * `windowTitle` the title of the window. 647 | 648 | ### `gui.end(gl, canvasWidth, canvasHeight)` 649 | 650 | Should be called once you have placed out your widgets. 651 | 652 | * `gl` your WebGL context 653 | * `canvasWidth` the width of the canvas. 654 | * `canvasHeight` the height of the canvas. 655 | 656 | ### `gui.hasMouseFocus()` 657 | 658 | Returns `true` if the user is interacting with the GUI in some manner. Else `false`. 659 | 660 | ### `gui.button(str)` 661 | 662 | Places a button. Returns `true` if the button is clicked, else `false` 663 | 664 | * `str` the text string in the button. 665 | 666 | ### `gui.sameLine()` 667 | 668 | The next widget is placed on the same line as the previous widget, if this method is called between them. 669 | 670 | ### `gui.sliderFloat(labelStr, value, min, max, numDecimalDigits)` 671 | 672 | A slider that can be used to set a floating point value. 673 | 674 | * `labelStr` The right label of the slider. 675 | * `value` the value manipulated by this widget. Should be on the form `{val: v}` 676 | * `min` the minimum value of the slider. 677 | * `max` the maximum value of the slider. 678 | * `numDecimalDigits` the number of decimal digits to display the value in the slider with. 679 | 680 | ### `gui.sliderInt(labelStr, value, min, max)` 681 | 682 | Same as `sliderFloat`, expect the values are rounded to integers. 683 | 684 | ### `gui.draggerFloat3(labelStr, value, minMaxValues, subLabels)` 685 | 686 | Places out a list of three dragger subwidgets. This widget can be used to manipulate an array of length 3 with a single widget. 687 | 688 | * `labelStr` The right label of the widget. 689 | * `value` the array that is manipulated by this widget. Should be an array of length 3. 690 | * `minMaxValues` if an array on the form `[min,max]`, then this parameter specifies the min and max values of ALL the three subwidgets. However, if it is an array on the form `[ [min1,max1], [min2,max2], [min3,max3] ]`, then `[min1,max1]` is the min and max values of the first subwidget, and so on. 691 | * `subLabels` specifies the labels of the subwidgets. Defaults to an array of empty strings. 692 | 693 | ### `gui.draggerFloat4(labelStr, value, minMaxValues, subLabels)` 694 | 695 | Same as `draggerFloat3`, except it handles arrays of length 4. 696 | 697 | ### `gui.draggerFloat2(labelStr, value, minMaxValues, subLabels)` 698 | 699 | Same as `draggerFloat3`, except it handles arrays of length 2. 700 | 701 | ### `gui.draggerFloat1(labelStr, value, minMaxValues, subLabels)` 702 | 703 | Same as `draggerFloat3`, except it handles arrays of length 1. 704 | 705 | ### `gui.draggerRgb(labelStr, value)` 706 | 707 | Basically a specialized version of `draggerFloat3` that allows you to manipulate RGB-values(array of length 3), and `minMaxValues` is set to `[0,1]`. 708 | 709 | ### `gui.radioButton(labelStr, value, id)` 710 | 711 | Places out a single radio button. For more detailed usage, see the [tutorial](https://github.com/Erkaman/pnp-gui#tutorial) 712 | 713 | * `labelStr` The right label of the radio button. 714 | * `value`. The value manipulated by this widget. Should be on the form `{val: v}`. 715 | * `id` the id of this button. If `value.val == id`, then this radio button is the chosen radio button, in a group of radio buttons. 716 | 717 | ### `gui.checkBox(labelStr, value)` 718 | 719 | Places out a checkbox. 720 | 721 | * `labelStr` The right label of the checkbox. 722 | * `value`. The value manipulated by this widget. Should be on the form `{val: v}`. If `value.val` is true, then the checkbox is checked, otherwise it is not. 723 | 724 | 725 | ### `gui.separator()` 726 | 727 | Places out a separator, that can be used to create logical groupings in a GUI. 728 | 729 | ### `gui.textLine(str)` 730 | 731 | Places out a single line of text. 732 | 733 | * `str` the string this widget renders. 734 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * several windows. 2 | * fix so that the font size can be changed without breaking the entire layout. 3 | * Combo box. 4 | * Text Input. 5 | * Scrollbar. 6 | * fix _draggerFloatN max-min default. 7 | -------------------------------------------------------------------------------- /example/demo2Frag.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | #pragma glslify: snoise2 = require(glsl-noise/simplex/2d) 4 | 5 | varying vec3 vNormal; 6 | varying vec3 vColor; 7 | varying float vHeight; 8 | varying vec3 vPosition; 9 | 10 | uniform vec3 uAmbientLight; 11 | uniform vec3 uLightColor; 12 | uniform vec3 uLightDir; 13 | 14 | uniform vec3 uSnowColor; 15 | uniform vec3 uGrassColor; 16 | uniform vec3 uSandColor; 17 | 18 | uniform float uTextureNoiseScale; 19 | uniform float uTextureNoiseStrength; 20 | 21 | vec3 terrainColor() { 22 | 23 | float snowHeight = 0.5; 24 | float grassHeight = -0.4; 25 | 26 | // interpolation area size. 27 | float s = 0.1; 28 | 29 | // use smooth step functions to interpolate between the three colors. 30 | vec3 snow = uSnowColor * smoothstep(snowHeight, snowHeight+s, vHeight); 31 | vec3 grass = 32 | uGrassColor * (1.0-smoothstep(snowHeight, snowHeight+s, vHeight)) * (smoothstep(grassHeight, grassHeight+s, vHeight)); 33 | vec3 sand = 34 | uSandColor * (1.0 - smoothstep(grassHeight, grassHeight+s, vHeight)); 35 | 36 | // add some noise for some visual variation. 37 | vec3 noise = vec3(snoise2(uTextureNoiseScale*vec2(vPosition.x, vPosition.x+vPosition.y+vPosition.z))) ; 38 | 39 | return (snow + grass + sand) + noise*uTextureNoiseStrength ; 40 | } 41 | 42 | void main() { 43 | 44 | vec3 n = vNormal; 45 | vec3 l = normalize(uLightDir); 46 | vec3 diffuseColor = terrainColor(); 47 | 48 | vec3 ambient = uAmbientLight * diffuseColor; 49 | vec3 diffuse = diffuseColor * uLightColor * dot(n, l) ; 50 | 51 | gl_FragColor = vec4(ambient + diffuse, 1.0); 52 | } 53 | -------------------------------------------------------------------------------- /example/demo2Vert.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | attribute vec3 aPosition; 4 | attribute vec3 aNormal; 5 | 6 | varying vec3 vNormal; 7 | varying vec3 vColor; 8 | varying float vHeight; 9 | varying vec3 vPosition; 10 | 11 | uniform mat4 uProjection; 12 | uniform mat4 uView; 13 | uniform float uHeightScale; 14 | uniform float uNoiseScale; 15 | uniform vec2 uPosition; 16 | 17 | #pragma glslify: snoise2 = require(glsl-noise/simplex/2d) 18 | 19 | float height(vec2 coord) { 20 | return ( snoise2(vec2(coord.xy)*uNoiseScale)) ; 21 | } 22 | 23 | float height(float x, float y) { 24 | return height(vec2(x,y)) ; 25 | } 26 | 27 | vec3 getNormal(vec2 texCoord) 28 | { 29 | float eps = 1e-5; 30 | vec3 p = vec3(texCoord.x, 0.0, texCoord.y); 31 | 32 | // approximate the derivatives by the method of finite differences. 33 | vec3 va = vec3(2.0*eps, height(p.x+eps,p.z) - height(p.x-eps,p.z), 0.0 ); 34 | vec3 vb = vec3(0.0, height(p.x,p.z+eps) - height(p.x,p.z-eps), 2.0*eps ); 35 | 36 | return normalize(cross(normalize(vb), normalize(va) )); 37 | } 38 | 39 | void main() { 40 | // compute normal, based on the noise. 41 | vNormal = getNormal(aPosition.xz); 42 | 43 | float h = height(aPosition.xz); 44 | 45 | // output height. 46 | vHeight = h; 47 | 48 | float horizontalScale = 3000.0; 49 | 50 | // now use the height value to modify the originally flat plane. 51 | vec3 pos = 52 | 100.0 * vec3(uPosition.x, 0.0, uPosition.y) + 53 | vec3(horizontalScale, uHeightScale, horizontalScale)* vec3(aPosition.x,h ,aPosition.z); 54 | 55 | // output pos. 56 | vPosition = pos; 57 | gl_Position = uProjection * uView* vec4(pos, 1.0); 58 | 59 | // vNormal = (vec3(height(aPosition.xz)*0.5 + 0.5)); 60 | 61 | } -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* global requestAnimationFrame */ 3 | 4 | var bunny = require('bunny'); 5 | var mat4 = require('gl-mat4'); 6 | var vec3 = require('gl-vec3'); 7 | var Geometry = require('gl-geometry'); 8 | var glShader = require('gl-shader'); 9 | var normals = require('normals'); 10 | var glslify = require('glslify') 11 | var createOrbitCamera = require('orbit-camera'); 12 | var shell = require("gl-now")(); 13 | var createGui = require("../index.js"); 14 | var cameraPosFromViewMatrix = require('gl-camera-pos-from-view-matrix'); 15 | var dragon = require('stanford-dragon/3'); 16 | var boundingBox = require('vertices-bounding-box'); 17 | var transform = require('geo-3d-transform-mat4'); 18 | var randomArray = require('random-array'); 19 | 20 | var demo1Shader, demo2Shader, bunnyGeo, dragonGeo, planeGeo; 21 | 22 | var camera = createOrbitCamera([0, -1000, 0], [0, 0, 0], [0, 1, 0]); 23 | 24 | var mouseLeftDownPrev = false; 25 | 26 | const RENDER_BUNNY = 0; 27 | const RENDER_DRAGON = 1; 28 | const DEMO_MODEL = 2; 29 | const DEMO_HEIGHTMAP = 3; 30 | 31 | /* 32 | Variables that can be modified through the GUI. 33 | */ 34 | 35 | var bg = [0.6, 0.7, 1.0]; // clear color. 36 | var demo = {val: DEMO_MODEL}; 37 | 38 | var demo1DiffuseColor = [0.42, 0.34, 0.0]; 39 | var demo1AmbientLight = [0.77, 0.72, 0.59]; 40 | var demo1LightColor = [0.40, 0.47, 0.0]; 41 | var demo1SunDir = [-0.69, 1.33, 0.57]; 42 | var demo1SpecularPower = {val: 12.45}; 43 | var demo1HasSpecular = {val: true}; 44 | var demo1RenderModel = {val: RENDER_BUNNY}; 45 | 46 | var demo2AmbientLight = [0.85, 0.52, 0.66]; 47 | var demo2LightColor = [0.38, 0.44, 0.03]; 48 | var demo2SunDir = [1.35, 0.61, 1.12]; 49 | var demo2SnowColor = [0.6, 0.6, 0.6]; 50 | var demo2GrassColor = [0.12, 0.34, 0.12]; 51 | var demo2SandColor = [0.50, 0.4, 0.21]; 52 | var demo2HeightScale = {val: 200.0}; 53 | var demo2NoiseScale = {val: 2.0}; 54 | var demo2HeightmapPosition = [0.0, 0.0]; 55 | 56 | var demo2TextureNoiseScale = {val: 0.3}; 57 | var demo2TextureNoiseStrength = {val: 0.01}; 58 | 59 | 60 | const demo1Vert = ` 61 | precision mediump float; 62 | attribute vec3 aPosition; 63 | attribute vec3 aNormal; 64 | varying vec3 vNormal; 65 | varying vec3 vPosition; 66 | uniform mat4 uProjection; 67 | uniform mat4 uView; 68 | void main() { 69 | vNormal = aNormal; 70 | vPosition = aPosition; 71 | 72 | gl_Position = uProjection * uView * vec4(aPosition, 1.0); 73 | } 74 | ` 75 | 76 | const demo1Frag = ` 77 | precision mediump float; 78 | varying vec3 vNormal; 79 | varying vec3 vPosition; 80 | uniform vec3 uDiffuseColor; 81 | uniform vec3 uAmbientLight; 82 | uniform vec3 uLightColor; 83 | uniform vec3 uLightDir; 84 | uniform vec3 uEyePos; 85 | uniform mat4 uView; 86 | uniform float uSpecularPower; 87 | uniform float uHasSpecular; 88 | void main() { 89 | vec3 n = vNormal; 90 | vec3 l = normalize(uLightDir); 91 | vec3 v = normalize(uEyePos - vPosition); 92 | vec3 ambient = uAmbientLight * uDiffuseColor; 93 | vec3 diffuse = uDiffuseColor * uLightColor * dot(n, l) ; 94 | vec3 specular = pow(clamp(dot(normalize(l+v),n),0.0,1.0) , uSpecularPower) * vec3(1.0,1.0,1.0); 95 | 96 | gl_FragColor = vec4(ambient + diffuse + specular*uHasSpecular, 1.0); 97 | } 98 | ` 99 | 100 | // center geometry on (0,0,0) 101 | function centerGeometry(geo, scale) { 102 | 103 | // Calculate the bounding box. 104 | var bb = boundingBox(geo.positions); 105 | 106 | // Translate the geometry center to the origin. 107 | var translate = [-0.5 * (bb[0][0] + bb[1][0]), -0.5 * (bb[0][1] + bb[1][1]), -0.5 * (bb[0][2] + bb[1][2])]; 108 | var m = mat4.create(); 109 | mat4.scale(m, m, [scale, scale, scale]); 110 | mat4.translate(m, m, translate); 111 | 112 | geo.positions = transform(geo.positions, m) 113 | } 114 | 115 | // create a flat plane, that's been tesselated into n quads(rendered as triangles) on the width and depth. 116 | function createPlane(n) { 117 | var positions = []; 118 | var cells = []; 119 | 120 | for (var iy = 0; iy <= n; ++iy) { 121 | for (var ix = 0; ix <= n; ++ix) { 122 | var x = -1 / 2 + ix / n; 123 | var y = 1 / 2 - iy / n; 124 | positions.push([x, 0, y]); 125 | if (iy < n && ix < n) { 126 | cells.push([iy * (n + 1) + ix + 1, (iy + 1) * (n + 1) + ix + 1, iy * (n + 1) + ix]); 127 | cells.push([iy * (n + 1) + ix, (iy + 1) * (n + 1) + ix + 1, (iy + 1) * (n + 1) + ix]); 128 | } 129 | } 130 | } 131 | 132 | return {positions: positions, cells: cells}; 133 | } 134 | 135 | shell.on("gl-init", function () { 136 | var gl = shell.gl 137 | 138 | gl.enable(gl.DEPTH_TEST); 139 | gl.enable(gl.CULL_FACE); 140 | 141 | gui = new createGui(gl) 142 | gui.windowSizes = [360, 580]; 143 | 144 | centerGeometry(bunny, 80.0); 145 | bunnyGeo = Geometry(gl) 146 | .attr('aPosition', bunny.positions) 147 | .attr('aNormal', normals.vertexNormals(bunny.cells, bunny.positions)) 148 | .faces(bunny.cells) 149 | 150 | centerGeometry(dragon, 16.0); 151 | dragonGeo = Geometry(gl) 152 | .attr('aPosition', dragon.positions) 153 | .attr('aNormal', normals.vertexNormals(dragon.cells, dragon.positions)) 154 | .faces(dragon.cells) 155 | 156 | var plane = createPlane(200); 157 | 158 | planeGeo = Geometry(gl) 159 | .attr('aPosition', plane.positions) 160 | .attr('aNormal', normals.vertexNormals(plane.cells, plane.positions)) 161 | .faces(plane.cells); 162 | 163 | demo1Shader = glShader(gl, demo1Vert, demo1Frag); 164 | demo2Shader = glShader(gl, 165 | glslify("./demo2Vert.glsl"), 166 | glslify("./demo2Frag.glsl")); 167 | 168 | }); 169 | 170 | function demo1Randomize() { 171 | demo1DiffuseColor = randomArray(0, 1).oned(3); 172 | demo1AmbientLight = randomArray(0, 1).oned(3); 173 | demo1LightColor = randomArray(0, 1).oned(3); 174 | demo1SunDir = randomArray(-2, +2).oned(3); 175 | demo1SpecularPower.val = Math.round(randomArray(0, 40).oned(1)[0]); 176 | } 177 | 178 | function demo2RandomizeLighting() { 179 | demo2AmbientLight = randomArray(0, 1).oned(3); 180 | demo2LightColor = randomArray(0, 1).oned(3); 181 | demo2SunDir = randomArray(0, +2).oned(3); 182 | } 183 | 184 | function demo2RandomizeColor() { 185 | demo2SnowColor = randomArray(0, 1).oned(3); 186 | demo2GrassColor = randomArray(0, 1).oned(3); 187 | demo2SandColor = randomArray(0, 1).oned(3); 188 | } 189 | 190 | shell.on("gl-render", function (t) { 191 | var gl = shell.gl 192 | var canvas = shell.canvas; 193 | 194 | gl.clearColor(bg[0], bg[1], bg[2], 1); 195 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 196 | gl.viewport(0, 0, canvas.width, canvas.height); 197 | 198 | var model = mat4.create(); 199 | var projection = mat4.create(); 200 | var scratchMat = mat4.create(); 201 | var view = camera.view(scratchMat); 202 | var scratchVec = vec3.create(); 203 | 204 | mat4.perspective(projection, Math.PI / 2, canvas.width / canvas.height, 0.1, 10000.0); 205 | 206 | if (demo.val == DEMO_MODEL) { 207 | demo1Shader.bind(); 208 | 209 | demo1Shader.uniforms.uView = view; 210 | demo1Shader.uniforms.uProjection = projection; 211 | demo1Shader.uniforms.uDiffuseColor = demo1DiffuseColor; 212 | demo1Shader.uniforms.uAmbientLight = demo1AmbientLight; 213 | demo1Shader.uniforms.uLightColor = demo1LightColor; 214 | demo1Shader.uniforms.uLightDir = demo1SunDir; 215 | demo1Shader.uniforms.uEyePos = cameraPosFromViewMatrix(scratchVec, view); 216 | demo1Shader.uniforms.uSpecularPower = demo1SpecularPower.val; 217 | demo1Shader.uniforms.uHasSpecular = demo1HasSpecular.val ? 1.0 : 0.0; 218 | 219 | if (demo1RenderModel.val == RENDER_BUNNY) { 220 | bunnyGeo.bind(demo1Shader); 221 | bunnyGeo.draw(); 222 | } else if (demo1RenderModel.val == RENDER_DRAGON) { 223 | dragonGeo.bind(demo1Shader); 224 | dragonGeo.draw(); 225 | } 226 | } else { 227 | demo2Shader.bind(); 228 | 229 | demo2Shader.uniforms.uView = view; 230 | demo2Shader.uniforms.uProjection = projection; 231 | 232 | demo2Shader.uniforms.uAmbientLight = demo2AmbientLight; 233 | demo2Shader.uniforms.uLightColor = demo2LightColor; 234 | demo2Shader.uniforms.uLightDir = demo2SunDir; 235 | 236 | demo2Shader.uniforms.uSnowColor = demo2SnowColor; 237 | demo2Shader.uniforms.uGrassColor = demo2GrassColor; 238 | demo2Shader.uniforms.uSandColor = demo2SandColor; 239 | 240 | demo2Shader.uniforms.uHeightScale = demo2HeightScale.val; 241 | demo2Shader.uniforms.uNoiseScale = demo2NoiseScale.val; 242 | demo2Shader.uniforms.uPosition = demo2HeightmapPosition; 243 | 244 | demo2Shader.uniforms.uTextureNoiseScale = demo2TextureNoiseScale.val; 245 | demo2Shader.uniforms.uTextureNoiseStrength = demo2TextureNoiseStrength.val; 246 | 247 | planeGeo.bind(demo2Shader); 248 | planeGeo.draw(); 249 | } 250 | 251 | var pressed = shell.wasDown("mouse-left"); 252 | var io = { 253 | mouseLeftDownCur: pressed, 254 | mouseLeftDownPrev: mouseLeftDownPrev, 255 | 256 | mousePositionCur: shell.mouse, 257 | mousePositionPrev: shell.prevMouse 258 | }; 259 | mouseLeftDownPrev = pressed; 260 | 261 | gui.begin(io, "Window"); 262 | 263 | gui.textLine("Choose a Demo"); 264 | gui.radioButton("Model", demo, DEMO_MODEL); 265 | gui.sameLine(); 266 | gui.radioButton("Heightmap", demo, DEMO_HEIGHTMAP); 267 | gui.separator(); 268 | 269 | if (demo.val == DEMO_MODEL) { 270 | 271 | gui.textLine("Lighting Settings"); 272 | 273 | gui.radioButton("Bunny", demo1RenderModel, RENDER_BUNNY); 274 | gui.sameLine(); 275 | gui.radioButton("Dragon", demo1RenderModel, RENDER_DRAGON); 276 | 277 | gui.draggerRgb("Ambient Light", demo1AmbientLight); 278 | gui.draggerRgb("Diffuse Color", demo1DiffuseColor); 279 | gui.draggerRgb("Light Color", demo1LightColor); 280 | 281 | gui.checkbox("Has Specular Lighting", demo1HasSpecular); 282 | if (demo1HasSpecular.val) 283 | gui.sliderFloat("Specular Power", demo1SpecularPower, 0, 40, 3); 284 | 285 | gui.draggerFloat3("Light Direction", demo1SunDir, [-2, +2], ["X:", "Y:", "Z:"]); 286 | 287 | if (gui.button("Randomize")) { 288 | demo1Randomize(); 289 | } 290 | } else { 291 | gui.textLine("Lighting Settings"); 292 | 293 | gui.draggerRgb("Ambient Light", demo2AmbientLight); 294 | gui.draggerRgb("Light Color", demo2LightColor); 295 | gui.draggerFloat3("Light Direction", demo2SunDir, [0, +2], ["X:", "Y:", "Z:"]); 296 | 297 | if (gui.button("Randomize")) { 298 | demo2RandomizeLighting(); 299 | } 300 | gui.separator(); 301 | 302 | gui.textLine("Heightmap Geometry"); 303 | 304 | gui.sliderFloat("Height Scale", demo2HeightScale, 0, 400); 305 | gui.sliderFloat("Noise Scale", demo2NoiseScale, 0, 20.0); 306 | gui.draggerFloat2("Position", demo2HeightmapPosition, [-10, +10], ["X:", "Z:"]); 307 | 308 | gui.separator(); 309 | 310 | gui.textLine("Heightmap Color"); 311 | 312 | gui.draggerRgb("Snow Color", demo2SnowColor); 313 | gui.draggerRgb("Grass Color", demo2GrassColor); 314 | gui.draggerRgb("Sand Color", demo2SandColor); 315 | 316 | 317 | if (gui.button("Randomize")) { 318 | demo2RandomizeColor(); 319 | } 320 | 321 | gui.sliderFloat("Color Noise Scale", demo2TextureNoiseScale, 0.01, 0.4); 322 | gui.sliderFloat("Color Noise Strength", demo2TextureNoiseStrength, 0.01, 0.2); 323 | } 324 | 325 | gui.separator(); 326 | 327 | gui.textLine("Miscellaneous"); 328 | gui.draggerRgb("Background", bg); 329 | 330 | gui.end(gl, canvas.width, canvas.height); 331 | }); 332 | 333 | shell.on("tick", function () { 334 | 335 | // if interacting with the GUI, do not let the mouse control the camera. 336 | if (gui.hasMouseFocus()) 337 | return; 338 | 339 | if (shell.wasDown("mouse-left")) { 340 | var speed = 2.0; 341 | camera.rotate([(shell.mouseX / shell.width - 0.5) * speed, (shell.mouseY / shell.height - 0.5) * speed], 342 | [(shell.prevMouseX / shell.width - 0.5) * speed, (shell.prevMouseY / shell.height - 0.5) * speed]) 343 | } 344 | if (shell.scroll[1]) { 345 | camera.zoom(shell.scroll[1] * 0.6); 346 | } 347 | }); -------------------------------------------------------------------------------- /example/test.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | attribute vec3 aPosition; 4 | attribute vec3 aNormal; 5 | 6 | varying vec3 vNormal; 7 | varying vec3 vPosition; 8 | 9 | uniform mat4 uProjection; 10 | uniform mat4 uView; 11 | uniform mat4 uModel; 12 | 13 | void main() { 14 | vNormal = aNormal; 15 | vPosition = aPosition; 16 | 17 | gl_Position = uProjection * uView * uModel* vec4(aPosition, 1.0); 18 | } -------------------------------------------------------------------------------- /font_atlas.js: -------------------------------------------------------------------------------- 1 | module.exports=require('ndpack-image')(256,256,4,"iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAAklEQVR4AewaftIAAAtaSURBVO3BAW4cWYIlQfcP3f/KPh3YDmwiQVFkldRTPfHMBGJmHukH/1ahUqFSofKqQqVC5VWFyqsKlVuFyq3iovKqQuVWoVKh8q5C5V2FyqsKlYp3KhUq7ypULhU3lVcVKq8qbiq3ipvKRypULhXvVG4VF5VLxTuVS8VN5VZxU7lUqFwqVN5VXFQqPqJScVH5SMVN5VXFTeVdxUXlVvFK5V3FTeVS8UrlVvFOpeKdyq3ipnKpUHlVoVJxU7lU3FQqVF5VqFSofOYHv0HFpeKiUqFSoVKhcqtQuVSofKRC5aJSoXKpULlVqHyFyl9RoXKrULlUvKtQuVWoVKjcKlReVbxT+UiFyiuVVxWXCpVbhUqFyq1C5TMVF5Wbys+oXCouKrcKlVuFyqVC5Vah8hUqtwqVW4XKrULlonKrULmpfETlIxUqtwqVz6hUqNxULhUfqVD5qh98QcWt4lKhUqFSofKfpFJxqVC5VNwqLipfofKuQuUzFSoVr1Q+ovKZCpWKX6lQ+UyFyl+hclO5VFxU3lV8ROWmcqm4qKh8pELlUnFRqVC5VKhcKlR+pULllcpHVCpUvqtC5ZXK71ah8h0/eKNyUbmpVKhUqPyKSoVKhcorlYqLyqVCReVSofJKpUKlQqVCpUJF5VKhcqtQ+R0qVG4qv1Kh8qrionKrUPlIxU3lVnFT+YxKxU3lu1QuFReVm8qvVFxUPlKh8qpCpeI7Kn63ipvKreKm8hUVf1eFynf94G9SqbhUqNxUXlWoXCpULhUq36XySuUzKt9VofJ3Vai8U7lUqFSofETlVYXKReVWoXKrULlVqNwqVFQq3lWoXCpUbiqXiovKZyouKj9TofJdFR9ReVWh8neofETlMxU3lYvKq4rvqPirfvALFbeKS4XKTaVCpULlVqFSoVKh8kqlQuWvUPmIyp+i8lUVKl9V8apC5T9B5VbxVSpfofJXqVRcKlQuFSqvKlT+aVQuFb+LyqVC5TsO/6byEZWLykVF5QkqVP6OCpV3FR9RUVFRuahcKr6rQuW7KlQuKv9JKq9UKi4qKip/l0rFq4qPVKj8FSoVf5pKxXf84N8qVL6r4lahcqtQuahUqFxUKm4qFSr/ZCoVN5VLxa3ionKpeKWiUnFT+RWVipvKRaXipvIZlYqbyq3iovKuQuVPq1B5pVLxSuUrKl6p3FQqbiq3ipvKq4pXKp9Rqbip/ErFpUKl4qbyEZUKlUvFK5VXAvEbVKj8bhUqtwqVP61CZeb/ssNvojIz/10E4h+uQqVCZWZ+D4GYmUf6wb9UXFRuFReViovKrULlVnFRuVW8UnlVcVG5VbxSeVXxSuVS8TMqFSqvKlQqVN5VqFSovKtQqVB5VaFSofKuQqXiIyoVN5UKlXcVN5Vbhcqt4mdUKlRuFTeVS4XKRypuKh+pULlU3FReVdxULhWvVCpUPlKh8qpCpULlXcVFpeKmUnFRuVS8UrlV3FQuFa9UbhWvVCpUKi4qt4pXKhUqrypUbhUqtx/8BhUqlwqVm8qtQuVSoXKpULmp3CpULhUqrypUVG4VKn9ChcrvovKu4qbyKyqXis+ofEalQuWicqtQ+ZkKlVuFys9UqNwqVC4VKq8qVG4Vv6LyTuUzKhU3lUuFyqVC5aJyq1CpULlVqFxUbhUqFSq3iluFyqVCpULldzj8m0rFpULllUrFuwqVm0rFZypUbioVf4XK36VS8apC5StUKm4VKheVilcVKh+pUPmISsVXVKhUfIfKraKi4rtUfqWiomI+pnJTuahUVLyrqKj4ih/8F1GpuKj8p1WoVKi8Uqm4qLxSqVCpUPlIhco/QYXKreJWcVP5lYqPqLyruFRcVH6l4qZyqbio3CouKu8qVP5bqFwqVG4q33F4oVKh8hGViu+oqKhQ+YqKigqVVyoqFRW/i0rFpULld1KpUPlIhcqvqFR8pkLlolLxO6moqFR8pOJSoaKi8jMVF5WLispXqKioVFxU3ql8pELlv5FKxV/1gz9M5btULhU/o3KpUPmTVCpeVajcKlQuFSpfVaHyT6JS8VUqFTeVz6hU3FQ+o1Lxu1Wo/E4qFTeVW8VN5aJS8VUVr1R+RqXiZ37wRuUzKhU3lQqVS4XKZ1QqVC4VKu9UKlQuFSp/ikqFykdULioVtwqVm0qFyq9UqHyHSoXKR1ReqfwVKt+h8h0q36Fyq/i7Kj6jclO5qVxUXqncVD6i8hGVW8VF5SMq36HyMz/4DVQqLipfoVJxUalQeadSoaJS8UrlK1QqXqn8E1S8U5k/S+VSofLfTqXipvIrP/gXlY+oXFReqbxTeafyGZV3Ku9Ubiq/ovIRlV9R+YjKK5WbyjuVdyrvVP4KlQqV/7SKm8pnVCp+h4qbyu+iUqHy307lOwRi/pEqVH6mQuVWoTI/V6Ey/88P5h+t4qLyTuVSMT9XcVOZ/08gZuaRDv9S8ariUvGRikvFq4pLxauKS0VFRUXFu4qKS8WlouIjFRUVt4qZ+bUffFGFyiuVCpUKlc+ovKp4VaHyTuVSoXKrULlUVFxUZubXfvAvKhUqFSpfpVKh8h0VKn+Hyk1lZr7n8AUVKhW/Q4XKzPzvOvybSoXKTaXiVYXKrUKl4isqVGbmf98PvkDlolJxq1C5qFSoqFS8q7hU3FQqLiqfUam4qMzM3yMQf1iFyq9UqNwqVG4VKjPzexxm5rF+8IdU3FRm5p/nB3+Iysz8s/3gH0Sl4qJyq7iozMzvIxAz80iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe60fFzDzTYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXmsw8w81mFmHuswM491mJnHOszMYx1m5rEOM/NYh5l5rMPMPNZhZh7rMDOPdZiZxzrMzGMdZuaxDjPzWIeZeazDzDzWYWYe6zAzj3WYmcc6zMxjHWbmsQ4z81iHmXms/wFPVfm15inbwgAAAABJRU5ErkJggg==") 2 | -------------------------------------------------------------------------------- /font_info.js: -------------------------------------------------------------------------------- 1 | module.exports = {"chars":[{"x0":1,"y0":1,"x1":1,"y1":1,"xoff":0.000000,"yoff":0.000000,"xadvance":7.000000,"xoff2":0.000000,"yoff2":0.000000},{"x0":2,"y0":1,"x1":3,"y1":9,"xoff":3.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":4.000000,"yoff2":0.000000},{"x0":4,"y0":1,"x1":7,"y1":4,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":-6.000000},{"x0":8,"y0":1,"x1":15,"y1":9,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":16,"y0":1,"x1":21,"y1":10,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":1.000000},{"x0":22,"y0":1,"x1":29,"y1":9,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":30,"y0":1,"x1":36,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":37,"y0":1,"x1":38,"y1":4,"xoff":3.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":4.000000,"yoff2":-6.000000},{"x0":39,"y0":1,"x1":42,"y1":12,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":2.000000},{"x0":43,"y0":1,"x1":46,"y1":12,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":2.000000},{"x0":47,"y0":1,"x1":52,"y1":6,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":-1.000000},{"x0":53,"y0":1,"x1":58,"y1":6,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":-1.000000},{"x0":59,"y0":1,"x1":61,"y1":5,"xoff":1.000000,"yoff":-2.000000,"xadvance":7.000000,"xoff2":3.000000,"yoff2":2.000000},{"x0":62,"y0":1,"x1":67,"y1":2,"xoff":1.000000,"yoff":-4.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":-3.000000},{"x0":68,"y0":1,"x1":69,"y1":3,"xoff":2.000000,"yoff":-2.000000,"xadvance":7.000000,"xoff2":3.000000,"yoff2":0.000000},{"x0":70,"y0":1,"x1":75,"y1":11,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":1.000000},{"x0":76,"y0":1,"x1":81,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":82,"y0":1,"x1":87,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":88,"y0":1,"x1":93,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":94,"y0":1,"x1":99,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":100,"y0":1,"x1":106,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":107,"y0":1,"x1":112,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":113,"y0":1,"x1":118,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":119,"y0":1,"x1":124,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":125,"y0":1,"x1":130,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":131,"y0":1,"x1":136,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":137,"y0":1,"x1":138,"y1":7,"xoff":3.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":4.000000,"yoff2":0.000000},{"x0":139,"y0":1,"x1":141,"y1":9,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":3.000000,"yoff2":2.000000},{"x0":142,"y0":1,"x1":148,"y1":6,"xoff":0.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":-1.000000},{"x0":149,"y0":1,"x1":155,"y1":4,"xoff":1.000000,"yoff":-5.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":-2.000000},{"x0":156,"y0":1,"x1":162,"y1":6,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":-1.000000},{"x0":163,"y0":1,"x1":168,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":169,"y0":1,"x1":176,"y1":9,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":177,"y0":1,"x1":183,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":184,"y0":1,"x1":190,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":191,"y0":1,"x1":197,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":198,"y0":1,"x1":204,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":205,"y0":1,"x1":210,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":211,"y0":1,"x1":216,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":217,"y0":1,"x1":223,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":224,"y0":1,"x1":230,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":231,"y0":1,"x1":234,"y1":9,"xoff":2.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":0.000000},{"x0":235,"y0":1,"x1":239,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":0.000000},{"x0":240,"y0":1,"x1":246,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":247,"y0":1,"x1":252,"y1":9,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":1,"y0":13,"x1":8,"y1":21,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":9,"y0":13,"x1":15,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":16,"y0":13,"x1":22,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":23,"y0":13,"x1":28,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":29,"y0":13,"x1":35,"y1":22,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":1.000000},{"x0":36,"y0":13,"x1":42,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":43,"y0":13,"x1":49,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":50,"y0":13,"x1":57,"y1":21,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":58,"y0":13,"x1":64,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":65,"y0":13,"x1":72,"y1":21,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":73,"y0":13,"x1":80,"y1":21,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":81,"y0":13,"x1":87,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":88,"y0":13,"x1":95,"y1":21,"xoff":0.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":96,"y0":13,"x1":102,"y1":21,"xoff":1.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":103,"y0":13,"x1":106,"y1":24,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":2.000000},{"x0":107,"y0":13,"x1":112,"y1":23,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":1.000000},{"x0":113,"y0":13,"x1":116,"y1":24,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":2.000000},{"x0":117,"y0":13,"x1":122,"y1":19,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":-3.000000},{"x0":123,"y0":13,"x1":130,"y1":14,"xoff":0.000000,"yoff":0.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":1.000000},{"x0":131,"y0":13,"x1":133,"y1":15,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":4.000000,"yoff2":-7.000000},{"x0":134,"y0":13,"x1":139,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":140,"y0":13,"x1":145,"y1":22,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":146,"y0":13,"x1":151,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":152,"y0":13,"x1":157,"y1":22,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":158,"y0":13,"x1":163,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":164,"y0":13,"x1":169,"y1":22,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":170,"y0":13,"x1":175,"y1":22,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":3.000000},{"x0":176,"y0":13,"x1":181,"y1":22,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":182,"y0":13,"x1":184,"y1":22,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":4.000000,"yoff2":0.000000},{"x0":185,"y0":13,"x1":189,"y1":24,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":5.000000,"yoff2":2.000000},{"x0":190,"y0":13,"x1":195,"y1":22,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":196,"y0":13,"x1":198,"y1":22,"xoff":2.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":4.000000,"yoff2":0.000000},{"x0":199,"y0":13,"x1":206,"y1":19,"xoff":0.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":207,"y0":13,"x1":212,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":213,"y0":13,"x1":218,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":219,"y0":13,"x1":224,"y1":22,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":3.000000},{"x0":225,"y0":13,"x1":230,"y1":22,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":3.000000},{"x0":231,"y0":13,"x1":236,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":237,"y0":13,"x1":242,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":243,"y0":13,"x1":247,"y1":21,"xoff":2.000000,"yoff":-8.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":248,"y0":13,"x1":253,"y1":19,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":1,"y0":25,"x1":6,"y1":31,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":7,"y0":25,"x1":14,"y1":31,"xoff":0.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":0.000000},{"x0":15,"y0":25,"x1":20,"y1":31,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":21,"y0":25,"x1":26,"y1":34,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":3.000000},{"x0":27,"y0":25,"x1":32,"y1":31,"xoff":1.000000,"yoff":-6.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":0.000000},{"x0":33,"y0":25,"x1":38,"y1":36,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":2.000000},{"x0":39,"y0":25,"x1":40,"y1":36,"xoff":3.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":4.000000,"yoff2":2.000000},{"x0":41,"y0":25,"x1":46,"y1":36,"xoff":1.000000,"yoff":-9.000000,"xadvance":7.000000,"xoff2":6.000000,"yoff2":2.000000},{"x0":47,"y0":25,"x1":54,"y1":27,"xoff":0.000000,"yoff":-5.000000,"xadvance":7.000000,"xoff2":7.000000,"yoff2":-3.000000}]} -------------------------------------------------------------------------------- /images/demo_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/demo_screen.png -------------------------------------------------------------------------------- /images/tut_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_button.png -------------------------------------------------------------------------------- /images/tut_checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_checkbox.png -------------------------------------------------------------------------------- /images/tut_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_gui.png -------------------------------------------------------------------------------- /images/tut_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_radio.png -------------------------------------------------------------------------------- /images/tut_radio_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_radio_lines.png -------------------------------------------------------------------------------- /images/tut_rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_rgb.png -------------------------------------------------------------------------------- /images/tut_separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_separator.png -------------------------------------------------------------------------------- /images/tut_xyz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/pnp-gui/0c741a0bf4d1534503d35f9712c223f4c81508b8/images/tut_xyz.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Require dependencies 3 | */ 4 | var createShader = require("gl-shader"); 5 | var mat4 = require("gl-mat4"); 6 | var createTexture = require('gl-texture2d'); 7 | var hashString = require('hash-string'); 8 | var clamp = require('clamp'); 9 | var createBuffer = require('gl-buffer'); 10 | 11 | /* 12 | Require resources. 13 | */ 14 | var fontInfo = require("./font_info.js"); 15 | var fontAtlas = require("./font_atlas.js"); 16 | var shaders = require("./shaders.js"); 17 | 18 | /* 19 | Constructor 20 | */ 21 | function GUI(gl) { 22 | 23 | /* 24 | We use this single shader to render the GUI. 25 | */ 26 | this.shader = createShader(gl, shaders.vert, shaders.frag); 27 | 28 | /* 29 | These buffers contain all the geometry data. 30 | */ 31 | this.positionBufferObject = createBuffer(gl, [], gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW); 32 | this.colorBufferObject = createBuffer(gl, [], gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW); 33 | this.uvBufferObject = createBuffer(gl, [], gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW); 34 | this.indexBufferObject = createBuffer(gl, [], gl.ELEMENT_ARRAY_BUFFER, gl.DYNAMIC_DRAW); 35 | 36 | 37 | this._setupDefaultSettings(); 38 | 39 | this.fontAtlasTexture = createTexture(gl, fontAtlas); 40 | this.fontAtlasTexture.magFilter = gl.LINEAR; 41 | this.fontAtlasTexture.minFilter = gl.LINEAR; 42 | 43 | /* 44 | DO NOT CHANGE THIS VALUE. The entire GUI layout will break! 45 | */ 46 | this.textScale = 1.0; 47 | 48 | /* 49 | Keeps track of the ID of the widget that is currently being pressed down. 50 | We need to keep track of this, because otherwise we can't, for instance, affect the value of a 51 | slider while the mouse is OUTSIDE the hitbox of the slider. 52 | */ 53 | this.activeWidgetId = null; 54 | 55 | /* 56 | See _moveWindowCaret() for an explanation. 57 | */ 58 | this.sameLineActive = false; 59 | this.prevWidgetSizes = null; 60 | } 61 | 62 | 63 | GUI.prototype.sameLine = function () { 64 | this.sameLineActive = true; 65 | }; 66 | 67 | GUI.prototype.sliderFloat = function (str, value, min, max, numDecimalDigits) { 68 | 69 | if(typeof numDecimalDigits === 'undefined') { 70 | numDecimalDigits = 2; // default value 71 | } 72 | 73 | this._slider(str, value, min, max, false, numDecimalDigits); 74 | }; 75 | 76 | GUI.prototype.sliderInt = function (str, value, min, max) { 77 | this._slider(str, value, min, max, true, 0); 78 | }; 79 | 80 | GUI.prototype.draggerFloat4 = function (labelStr, value, minMaxValues, subLabels) { 81 | this._draggerFloatN(labelStr, value, 4, minMaxValues, subLabels); 82 | }; 83 | 84 | GUI.prototype.draggerFloat3 = function (labelStr, value, minMaxValues, subLabels) { 85 | this._draggerFloatN(labelStr, value, 3, minMaxValues, subLabels); 86 | }; 87 | 88 | GUI.prototype.draggerFloat2 = function (labelStr, value, minMaxValues, subLabels) { 89 | this._draggerFloatN(labelStr, value, 2, minMaxValues, subLabels); 90 | }; 91 | 92 | 93 | GUI.prototype.draggerFloat1 = function (labelStr, value, minMaxValues, subLabels) { 94 | this._draggerFloatN(labelStr, value, 1, minMaxValues, subLabels); 95 | }; 96 | 97 | GUI.prototype.draggerRgb = function (labelStr, value) { 98 | this._draggerFloatN( 99 | labelStr, value, 3, [0, 1], ["R:", "G:", "B:"], 100 | [ 101 | [this.draggerRgbRedColor, this.draggerRgbRedColorHover], 102 | [this.draggerRgbGreenColor, this.draggerRgbGreenColorHover], 103 | [this.draggerRgbBlueColor, this.draggerRgbBlueColorHover] 104 | ]); 105 | }; 106 | 107 | /* 108 | If value.val == id, then that means this radio button is chosen. 109 | */ 110 | GUI.prototype.radioButton = function (labelStr, value, id) { 111 | 112 | this._moveWindowCaret(); 113 | 114 | /* 115 | Radio button IO 116 | */ 117 | 118 | 119 | var zeroHeight = this._getTextSizes("0")[1]; 120 | 121 | 122 | var innerRadius = (zeroHeight * this.radioButtonInnerRadius); 123 | var outerRadius = (zeroHeight * this.radioButtonOuterRadius); 124 | 125 | 126 | var radioButtonPosition = this.windowCaret; 127 | 128 | 129 | var radioButtonSizes = [outerRadius * 2, outerRadius * 2]; 130 | 131 | var mouseCollision = _inCircle(radioButtonPosition, radioButtonSizes, this.io.mousePositionCur); 132 | 133 | 134 | if (this.io.mouseLeftDownCur == true && this.io.mouseLeftDownPrev == false && mouseCollision) { 135 | value.val = id; 136 | } 137 | 138 | var isHover = mouseCollision; 139 | 140 | 141 | /* 142 | CHECKBOX RENDERING 143 | */ 144 | 145 | this._circle(radioButtonPosition, radioButtonSizes, 146 | isHover ? this.radioButtonOuterColorHover : this.radioButtonOuterColor, this.radioButtonCircleSegments ); 147 | 148 | 149 | if (value.val == id) { 150 | var p = radioButtonPosition; 151 | var s = radioButtonSizes; 152 | var innerCirclePosition = [ 153 | Math.round(0.5 * (p[0] + (p[0] + s[0]) - innerRadius * 2 )), 154 | Math.round(0.5 * (p[1] + (p[1] + s[1]) - innerRadius * 2 )), 155 | ]; 156 | 157 | this._circle(innerCirclePosition, [innerRadius * 2, innerRadius * 2], 158 | isHover ? this.radioButtonInnerColorHover : this.radioButtonInnerColor, this.radioButtonCircleSegments ); 159 | } 160 | 161 | 162 | // now render radio button label. 163 | var labelPosition = [radioButtonPosition[0] + radioButtonSizes[0] + this.widgetLabelHorizontalSpacing, radioButtonPosition[1]] 164 | var labelStrSizes = [this._getTextSizes(labelStr)[0], radioButtonSizes[1]]; 165 | this._textCenter(labelPosition, labelStrSizes, labelStr); 166 | 167 | this.prevWidgetSizes = [radioButtonSizes[0] + labelStrSizes[0], radioButtonSizes[1]]; 168 | } 169 | 170 | 171 | GUI.prototype.checkbox = function (labelStr, value) { 172 | 173 | this._moveWindowCaret(); 174 | 175 | /* 176 | CHECKBOX IO(if checkbox clicked, flip boolean value.) 177 | */ 178 | 179 | // use height of zero to determine size of checkbox, to ensure that the textl label does become higher 180 | // than the checkbox. 181 | var zeroHeight = this._getTextSizes("0")[1]; 182 | 183 | 184 | var innerSize = zeroHeight * this.checkBoxInnerSizeRatio; 185 | var outerSize = zeroHeight * this.checkBoxOuterSizeRatio; 186 | 187 | var checkboxPosition = this.windowCaret; 188 | var checkboxSizes = [outerSize, outerSize]; 189 | 190 | var mouseCollision = _inBox(checkboxPosition, checkboxSizes, this.io.mousePositionCur); 191 | 192 | if (this.io.mouseLeftDownCur == true && this.io.mouseLeftDownPrev == false && mouseCollision) { 193 | value.val = !value.val; 194 | } 195 | 196 | var isHover = mouseCollision; 197 | 198 | /* 199 | CHECKBOX RENDERING 200 | */ 201 | 202 | // render outer box. 203 | this._box( 204 | checkboxPosition, 205 | checkboxSizes, isHover ? this.checkboxOuterColorHover : this.checkboxOuterColor); 206 | 207 | 208 | // now render a centered inner box, that shows whether the checkbox is true, or false. 209 | 210 | if (value.val) { 211 | var p = checkboxPosition; 212 | var s = checkboxSizes; 213 | var innerboxPosition = [ 214 | Math.round(0.5 * (p[0] + (p[0] + s[0]) - innerSize )), 215 | Math.round(0.5 * (p[1] + (p[1] + s[1]) - innerSize )), 216 | ]; 217 | 218 | this._box( 219 | innerboxPosition, 220 | [innerSize, innerSize], isHover ? this.checkboxInnerColorHover : this.checkboxInnerColor); 221 | } 222 | 223 | // now render checkbox label. 224 | var labelPosition = [checkboxPosition[0] + checkboxSizes[0] + this.widgetLabelHorizontalSpacing, checkboxPosition[1]] 225 | var labelStrSizes = [this._getTextSizes(labelStr)[0], checkboxSizes[1]]; 226 | this._textCenter(labelPosition, labelStrSizes, labelStr); 227 | 228 | this.prevWidgetSizes = [checkboxSizes[0] + labelStrSizes[0], checkboxSizes[1]]; 229 | } 230 | 231 | 232 | GUI.prototype.button = function (str) { 233 | 234 | this._moveWindowCaret(); 235 | 236 | var widgetId = hashString(str); 237 | 238 | /* 239 | BUTTON RENDERING 240 | */ 241 | 242 | var buttonPosition = this.windowCaret; 243 | 244 | var textSizes = this._getTextSizes(str); 245 | 246 | // button size is text size, plus the spacing around the text. 247 | var buttonSizes = [ 248 | textSizes[0] + this.buttonSpacing * 2, 249 | textSizes[1] + this.buttonSpacing * 2 250 | ]; 251 | 252 | var color; 253 | var isButtonClick = false; 254 | 255 | // we can only hover or click, when are not interacting with some other widget. 256 | if ((this.activeWidgetId == null || this.activeWidgetId == widgetId ) && _inBox(buttonPosition, buttonSizes, this.io.mousePositionCur)) { 257 | 258 | if (this.io.mouseLeftDownPrev && !this.io.mouseLeftDownCur) { 259 | 260 | isButtonClick = true; 261 | color = this.clickButtonColor; 262 | } else if (this.io.mouseLeftDownCur) { 263 | 264 | color = this.clickButtonColor; 265 | this.activeWidgetId = widgetId; 266 | } else { 267 | color = this.hoverButtonColor; 268 | } 269 | 270 | 271 | } else { 272 | color = this.buttonColor 273 | } 274 | 275 | this._box( 276 | buttonPosition, 277 | buttonSizes, color) 278 | 279 | // Render button text. 280 | this._text([buttonPosition[0] + this.buttonSpacing, 281 | buttonPosition[1] + buttonSizes[1] - this.buttonSpacing], str); 282 | 283 | 284 | this.prevWidgetSizes = (buttonSizes); 285 | 286 | 287 | /* 288 | BUTTON IO 289 | If button is pressed, return true; 290 | Otherwise, return false. 291 | */ 292 | 293 | 294 | if (isButtonClick) { 295 | return true; // button press! 296 | } 297 | 298 | return false; 299 | } 300 | 301 | 302 | GUI.prototype.separator = function () { 303 | this._moveWindowCaret(); 304 | 305 | var separatorPosition = this.windowCaret; 306 | // the separator should fill out the windows size. 307 | var separatorSizes = [ 308 | this.windowSizes[0] - 2 * this.windowSpacing, 309 | this._getTextSizes("0")[1] * this.separatorHeightRatio]; 310 | 311 | this._box(separatorPosition, separatorSizes, [0.4, 0.4, 0.4]); 312 | 313 | this.prevWidgetSizes = (separatorSizes); 314 | } 315 | 316 | 317 | GUI.prototype.textLine = function (str) { 318 | this._moveWindowCaret(); 319 | 320 | var textLinePosition = this.windowCaret; 321 | var textSizes = this._getTextSizes(str); 322 | 323 | // Render button text. 324 | this._textCenter(textLinePosition, textSizes, str); 325 | 326 | this.prevWidgetSizes = textSizes; 327 | }; 328 | 329 | GUI.prototype.begin = function (io, windowTitle) { 330 | 331 | 332 | // sanity checking. 333 | if(typeof io == 'undefined') { 334 | throw new Error("argument 'io' missing "); 335 | } else { 336 | 337 | if(typeof io.mousePositionCur == 'undefined') { 338 | throw new Error("property 'io.mousePositionCur' missing "); 339 | } 340 | if(typeof io.mousePositionPrev == 'undefined') { 341 | throw new Error("property 'io.mousePositionPrev' missing "); 342 | } 343 | if(typeof io.mouseLeftDownCur == 'undefined') { 344 | throw new Error("property 'io.mouseLeftDownCur' missing "); 345 | } 346 | if(typeof io.mouseLeftDownPrev == 'undefined') { 347 | throw new Error("property 'io.mouseLeftDownPrev' missing "); 348 | } 349 | } 350 | 351 | // default value. 352 | if(typeof windowTitle == 'undefined') { 353 | windowTitle = "Window"; 354 | } 355 | 356 | this.windowTitle = windowTitle; 357 | 358 | /* 359 | Setup geometry buffers. 360 | */ 361 | this.indexBuffer = []; 362 | this.positionBuffer = []; 363 | this.colorBuffer = []; 364 | this.uvBuffer = []; 365 | 366 | this.indexBufferIndex = 0; 367 | this.positionBufferIndex = 0; 368 | this.colorBufferIndex = 0; 369 | this.uvBufferIndex = 0; 370 | 371 | this.io = io; 372 | 373 | // render window. 374 | this._window(); 375 | }; 376 | 377 | 378 | GUI.prototype.end = function (gl, canvasWidth, canvasHeight) { 379 | 380 | if(typeof gl == 'undefined') { 381 | throw new Error("argument 'gl' missing "); 382 | } 383 | if(!(typeof canvasWidth == 'number')) { 384 | throw new Error("argument 'canvasWidth' must be a number "); 385 | } 386 | if(!(typeof canvasHeight == 'number')) { 387 | throw new Error("argument 'canvasHeight' must be a number "); 388 | } 389 | 390 | 391 | /* 392 | If a VAO is already bound, we need to unbound it. Otherwise, we will write into a VAO created by the user of the library 393 | when calling vertexAttribPointer, which means that we would effectively corrupt the user data! 394 | */ 395 | var VAO_ext = gl.getExtension('OES_vertex_array_object'); 396 | if (VAO_ext) 397 | VAO_ext.bindVertexArrayOES(null); 398 | 399 | /* 400 | We are changing some GL states when rendering the GUI. So before rendering we backup these states, 401 | and after rendering we restore these states. This is so that the end-user does not involuntarily have his 402 | GL-states messed with. 403 | */ 404 | this._backupGLState(gl); 405 | 406 | 407 | this.positionBufferObject.update(this.positionBuffer); 408 | gl.enableVertexAttribArray(this.shader.attributes.aPosition.location); 409 | gl.vertexAttribPointer(this.shader.attributes.aPosition.location, 2, gl.FLOAT, false, 0, 0); 410 | this.positionBufferObject.unbind(); 411 | 412 | 413 | this.colorBufferObject.update(this.colorBuffer); 414 | gl.enableVertexAttribArray(this.shader.attributes.aColor.location); 415 | gl.vertexAttribPointer(this.shader.attributes.aColor.location, 4, gl.FLOAT, false, 0, 0); 416 | this.colorBufferObject.unbind(); 417 | 418 | this.uvBufferObject.update(this.uvBuffer); 419 | gl.enableVertexAttribArray(this.shader.attributes.aUv.location); 420 | gl.vertexAttribPointer(this.shader.attributes.aUv.location, 2, gl.FLOAT, false, 0, 0); 421 | this.uvBufferObject.unbind(); 422 | 423 | this.indexBufferObject.update(this.indexBuffer); 424 | 425 | 426 | /* 427 | Setup matrices. 428 | */ 429 | var projection = mat4.create() 430 | mat4.ortho(projection, 0, canvasWidth, canvasHeight, 0, -1.0, 1.0) 431 | 432 | this.shader.bind() 433 | 434 | this.shader.uniforms.uProj = projection; 435 | this.shader.uniforms.uFontAtlas = this.fontAtlasTexture.bind() 436 | 437 | gl.disable(gl.DEPTH_TEST) // no depth testing; we handle this by manually placing out 438 | // widgets in the order we wish them to be rendered. 439 | 440 | 441 | // for text rendering, enable alpha blending. 442 | gl.enable(gl.BLEND) 443 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 444 | 445 | gl.drawElements(gl.TRIANGLES, (this.indexBufferIndex), gl.UNSIGNED_SHORT, 0); 446 | 447 | 448 | /* 449 | Make sure to always reset the active widget id, if mouse is released. 450 | This makes sure that every widget does not explicitly have to reset this value 451 | by themselves, which is a bit error-prone. 452 | */ 453 | if (this.activeWidgetId != null && this.io.mouseLeftDownCur == false) { 454 | this.activeWidgetId = null; 455 | } 456 | 457 | 458 | this._restoreGLState(gl); 459 | 460 | }; 461 | 462 | 463 | /* 464 | Mouse has focus EITHER when the mouse is inside the window, OR 465 | it is outside the window, but is interacting with a widget. 466 | */ 467 | GUI.prototype.hasMouseFocus = function () { 468 | return this.mouseInWindow || this.activeWidgetId != null; 469 | } 470 | 471 | 472 | 473 | GUI.prototype._setupDefaultSettings = function (char) { 474 | 475 | /* 476 | window settings 477 | */ 478 | 479 | // distance from window-borders to the widgets. 480 | this.windowSpacing = 14; 481 | 482 | // the vertical and horizontal spacing between the widgets. 483 | this.widgetSpacing = 11; 484 | 485 | // position of the window. 486 | this.windowPosition = [20, 20]; 487 | // size of the window. 488 | this.windowSizes = [360, 500]; 489 | // color of the window. 490 | this.windowColor = [0.1, 0.1, 0.1]; 491 | // the transparency of the window. 492 | this.windowAlpha = 0.9; 493 | 494 | // the title bar height. 495 | this.titleBarHeight = 21; 496 | // spacing between the title bars border, and the window title. 497 | this.titleBarVerticalSpacing = 6; 498 | // the title bar color. 499 | this.titleBarColor = [0.2, 0.4, 0.6]; 500 | 501 | 502 | /* 503 | button settings 504 | */ 505 | 506 | // the horizontal and vertical spacing between the button border and its text label. 507 | this.buttonSpacing = 3; 508 | // normal button color. 509 | this.buttonColor = [0.35, 0.1, 0.1]; 510 | // button button color when mouse hover 511 | this.hoverButtonColor = [0.40, 0.1, 0.1]; 512 | // button color when mouse click. 513 | this.clickButtonColor = [0.50, 0.1, 0.1]; 514 | 515 | /* 516 | slider settings 517 | */ 518 | 519 | // the vertical space between the number(inside the slider) and the border of the slider box. 520 | this.sliderVerticalSpacing = 4; 521 | // the color of the slider background. 522 | this.sliderBackgroundColor = [0.16, 0.16, 0.16]; 523 | // the color of the bar in the slider. 524 | this.sliderFillColor = [0.0, 0.3, 0.6]; 525 | // the color of the slider background when mouse hover, 526 | this.sliderBackgroundColorHover = [0.19, 0.19, 0.19]; 527 | // the color of the bar in the slider when mouse hover. 528 | this.sliderFillColorHover = [0.0, 0.3, 0.70]; 529 | 530 | /* 531 | dragger settings 532 | */ 533 | 534 | // the horizontal spacing between the subdragger widgets in a dragger widget. 535 | this.draggerWidgetHorizontalSpacing = 3; 536 | // the vertical spacing between the top and bottom borders and the text in draggers. 537 | this.draggerVerticalSpacing = 5; 538 | 539 | // The colors of the three subdraggers in the rgbDragger widget. 540 | // "Hover", refers to the color when the dragger is hovered. 541 | this.draggerRgbRedColor = [0.3, 0.0, 0.0]; 542 | this.draggerRgbRedColorHover = [0.35, 0.0, 0.0]; 543 | this.draggerRgbGreenColor = [0.0, 0.3, 0.0]; 544 | this.draggerRgbGreenColorHover = [0.0, 0.35, 0.0]; 545 | this.draggerRgbBlueColor = [0.0, 0.0, 0.3]; 546 | this.draggerRgbBlueColorHover = [0.0, 0.0, 0.38]; 547 | //The colors of the subdraggers in the draggerFloat widgets. 548 | // "Hover", refers to the color when the dragger is hovered. 549 | this.draggerFloatColor = [0.30, 0.30, 0.30]; 550 | this.draggerFloatColorHover = [0.32, 0.32, 0.32]; 551 | 552 | /* 553 | checkbox settings 554 | */ 555 | 556 | 557 | // the outer color is the color of the outer box of the checkbox, 558 | // and the inner color is the color of the inner box 559 | this.checkboxOuterColor = [0.3, 0.3, 0.3]; 560 | this.checkboxInnerColor = [0.15, 0.15, 0.15]; 561 | this.checkboxOuterColorHover = [0.33, 0.33, 0.33]; 562 | this.checkboxInnerColorHover = [0.18, 0.18, 0.18]; 563 | // size of inner box will be (height of "0")* checkBoxInnerSizeRatio 564 | this.checkBoxInnerSizeRatio = 1.4; 565 | // size of outer box will be (height of "0")* checkBoxOuterSizeRatio 566 | this.checkBoxOuterSizeRatio = 2.0; 567 | 568 | 569 | /* 570 | radioButton settings 571 | */ 572 | 573 | 574 | // the outer color is the color of the outer circle of the radioButton, 575 | // and the inner color is the color of the inner circle 576 | this.radioButtonOuterColor = [0.3, 0.3, 0.3]; 577 | this.radioButtonInnerColor = [0.15, 0.15, 0.15]; 578 | this.radioButtonOuterColorHover = [0.33, 0.33, 0.33]; 579 | this.radioButtonInnerColorHover = [0.18, 0.18, 0.18]; 580 | // in order to render the radio button, we must triangulate the circles into triangle segments 581 | // this number is the number of triangle segments. 582 | this.radioButtonCircleSegments = 9; 583 | 584 | // radius of the inner circle will be (height of "0")* innerRadiusRatio 585 | this.radioButtonInnerRadius = 0.6; 586 | // radius of the outer circle will be (height of "0")* outerRadiusRatio 587 | this.radioButtonOuterRadius = 1.0; 588 | 589 | 590 | /* 591 | separator settings. 592 | */ 593 | 594 | // the color of a separator. 595 | this.separatorColor = [0.4, 0.4, 0.4]; 596 | // the height of a separator (height of "0") * this.separatorHeightRatio 597 | this.separatorHeightRatio = 0.2; 598 | 599 | /* 600 | general settings 601 | */ 602 | 603 | // Some widgets render a label in addition to themselves(such as the sliders and draggers) 604 | // this value is the horizontal spacing between the label and the widget for those widgets. 605 | this.widgetLabelHorizontalSpacing = 4; 606 | 607 | // Some widgets will grow horizontally as the window size increases. They are grown to occupy this 608 | // ratio of the total window width. 609 | this.widgetHorizontalGrowRatio = 0.6; 610 | }; 611 | 612 | GUI.prototype._getCharDesc = function (char) { 613 | return fontInfo.chars[char.charCodeAt(0) - 32]; 614 | }; 615 | 616 | GUI.prototype._addIndex = function (index) { 617 | this.indexBuffer[this.indexBufferIndex++] = index; 618 | }; 619 | 620 | GUI.prototype._addPosition = function (position) { 621 | this.positionBuffer[this.positionBufferIndex++] = position[0]; 622 | this.positionBuffer[this.positionBufferIndex++] = position[1]; 623 | }; 624 | 625 | GUI.prototype._addColor = function (color) { 626 | this.colorBuffer[this.colorBufferIndex++] = color[0]; 627 | this.colorBuffer[this.colorBufferIndex++] = color[1]; 628 | this.colorBuffer[this.colorBufferIndex++] = color[2]; 629 | this.colorBuffer[this.colorBufferIndex++] = color[3]; 630 | }; 631 | 632 | GUI.prototype._addUv = function (uv) { 633 | this.uvBuffer[this.uvBufferIndex++] = uv[0]; 634 | this.uvBuffer[this.uvBufferIndex++] = uv[1]; 635 | }; 636 | 637 | /* 638 | Get width and height of a text string. 639 | */ 640 | GUI.prototype._getTextSizes = function (str) { 641 | 642 | var width = 0; 643 | var height = 0; // the height of the highest character. 644 | 645 | for (var i = 0; i < str.length; ++i) { 646 | var ch = str[i]; 647 | var cd = this._getCharDesc(ch); 648 | 649 | width += (cd.xadvance) * this.textScale; 650 | 651 | var y0 = (cd.yoff) * this.textScale; 652 | var y1 = (cd.yoff2) * this.textScale; 653 | var h = y1 - y0; 654 | 655 | if (height < h) { 656 | height = h; 657 | } 658 | 659 | } 660 | 661 | return [width, height]; 662 | } 663 | 664 | 665 | /* 666 | render text 667 | */ 668 | GUI.prototype._text = function (position, str) { 669 | 670 | /* 671 | Make sure to round the position to integer. Otherwise, anti-aliasing causes the text to get blurry, 672 | it seems 673 | */ 674 | var x = Math.round(position[0]); 675 | var y = Math.round(position[1]); 676 | 677 | /* 678 | Width of a single pixel in the font atlas. 679 | */ 680 | var ipw = 1.0 / 256; 681 | var iph = 1.0 / 256; 682 | 683 | for (var i = 0; i < str.length; ++i) { 684 | 685 | var ch = str[i]; 686 | 687 | // char desc 688 | var cd = this._getCharDesc(ch); 689 | 690 | /* 691 | We will render a single character as a quad. 692 | First we gather all information needed to render the quad: 693 | */ 694 | 695 | var x0 = (x + cd.xoff) * this.textScale; 696 | var y0 = (y + cd.yoff) * this.textScale; 697 | var x1 = (x + cd.xoff2) * this.textScale; 698 | var y1 = (y + cd.yoff2) * this.textScale; 699 | 700 | 701 | var s0 = (cd.x0 * ipw); 702 | var t0 = (cd.y0 * iph); 703 | var s1 = (cd.x1 * ipw); 704 | var t1 = (cd.y1 * iph); 705 | 706 | // render text as white. 707 | var whiteColor = [1, 1, 1, 1] 708 | 709 | 710 | /* 711 | Now we have all the information. Now render the quad as two triangles: 712 | */ 713 | 714 | var baseIndex = this.positionBufferIndex / 2; 715 | 716 | // top left 717 | this._addPosition([x0, y0]); 718 | this._addColor(whiteColor); 719 | this._addUv([s0, t0]); 720 | 721 | // bottom left 722 | this._addPosition([x0, y1]); 723 | this._addColor(whiteColor); 724 | this._addUv([s0, t1]); 725 | 726 | // top right 727 | this._addPosition([x1, y0]); 728 | this._addColor(whiteColor); 729 | this._addUv([s1, t0]); 730 | 731 | 732 | // bottom right 733 | this._addPosition([x1, y1]); 734 | this._addColor(whiteColor); 735 | this._addUv([s1, t1]); 736 | 737 | // triangle 1 738 | this._addIndex(baseIndex + 0); 739 | this._addIndex(baseIndex + 1); 740 | this._addIndex(baseIndex + 2); 741 | 742 | // triangle 2 743 | this._addIndex(baseIndex + 3); 744 | this._addIndex(baseIndex + 2); 745 | this._addIndex(baseIndex + 1); 746 | 747 | // finally, advance the x-coord, in preparation of rendering the next character. 748 | x += (cd.xadvance) * this.textScale; 749 | } 750 | } 751 | 752 | /* 753 | Render text centered in a box with position `p`, width `s[0]`, height `[1]`, 754 | */ 755 | GUI.prototype._textCenter = function (p, s, str) { 756 | var strSizes = this._getTextSizes(str); 757 | 758 | // we must round, otherwise the text may end up between pixels(say at 1.5, or 1.6, or something ), 759 | // and this makes it blurry 760 | var strPosition = [ 761 | Math.round(0.5 * (p[0] + (p[0] + s[0]) - strSizes[0] )), 762 | Math.round(0.5 * (p[1] + (p[1] + s[1]) + strSizes[1] )), 763 | ]; 764 | 765 | this._text(strPosition, str); 766 | } 767 | 768 | /* 769 | Add vertex that only has one color, and does not use a texture. 770 | */ 771 | GUI.prototype._coloredVertex = function (position, color) { 772 | // at this uv-coordinate, the font atlas is entirely white. 773 | var whiteUv = [0.95, 0.95]; 774 | 775 | this._addPosition(position); 776 | this._addColor(color); 777 | this._addUv(whiteUv); 778 | }; 779 | 780 | /* 781 | Render a box. 782 | 783 | `color` is a RGB-triplet. 784 | the optional `alpha` argument specifies the transparency of the box. 785 | default value of `alpha` is 1.0 786 | */ 787 | GUI.prototype._box = function (position, size, color, alpha) { 788 | 789 | 790 | if (typeof alpha === 'undefined') { 791 | alpha = 1.0; // default to 1.0 792 | } 793 | 794 | // top-left, bottom-left, top-right, bottom-right corners 795 | var tl = position; 796 | var bl = [position[0], position[1] + size[1]]; 797 | var tr = [position[0] + size[0], position[1]]; 798 | var br = [position[0] + size[0], position[1] + size[1]]; 799 | 800 | var baseIndex = this.positionBufferIndex / 2; 801 | 802 | var c = [color[0], color[1], color[2], alpha]; 803 | 804 | // vertex 1 805 | this._coloredVertex(tl, c); 806 | 807 | // vertex 2 808 | this._coloredVertex(bl, c); 809 | 810 | // vertex 3 811 | this._coloredVertex(tr, c); 812 | 813 | // vertex 4 814 | this._coloredVertex(br, c); 815 | 816 | 817 | // triangle 1 818 | this._addIndex(baseIndex + 0); 819 | this._addIndex(baseIndex + 1); 820 | this._addIndex(baseIndex + 2); 821 | 822 | // triangle 2 823 | this._addIndex(baseIndex + 3); 824 | this._addIndex(baseIndex + 2); 825 | this._addIndex(baseIndex + 1); 826 | 827 | }; 828 | 829 | GUI.prototype._unitCircle = function (position, theta, radius) { 830 | return [position[0] + radius * Math.cos(theta), position[1] + radius * Math.sin(theta)]; 831 | }; 832 | 833 | /* 834 | Render a circle, where the top-left corner of the circle is `position` 835 | Where `segments` is how many triangle segments the triangle is rendered with. 836 | */ 837 | GUI.prototype._circle = function (position, sizes, color, segments) { 838 | 839 | var centerPosition = [ 840 | position[0] + 0.5 * sizes[0], 841 | position[1] + 0.5 * sizes[1] 842 | ]; 843 | var radius = sizes[0] / 2; 844 | 845 | var baseIndex = this.positionBufferIndex / 2; 846 | 847 | var c = [color[0], color[1], color[2], 1.0]; 848 | 849 | // add center vertex. 850 | this._coloredVertex(centerPosition, c); 851 | var centerVertexIndex = baseIndex + 0; 852 | 853 | 854 | var stepSize = (2 * Math.PI) / segments; 855 | var curIndex = baseIndex + 1; 856 | for (var theta = 0; theta <= 2 * Math.PI + 0.1; theta += stepSize, ++curIndex) { 857 | 858 | // for first frame, we only create one vertex, and no triangles 859 | if (theta == 0) { 860 | var p = this._unitCircle(centerPosition, theta, radius); 861 | this._coloredVertex(p, c); 862 | } else { 863 | var p = this._unitCircle(centerPosition, theta, radius); 864 | this._coloredVertex(p, c); 865 | 866 | this._addIndex(curIndex + 0); 867 | this._addIndex(curIndex - 1); 868 | this._addIndex(centerVertexIndex); 869 | } 870 | } 871 | }; 872 | 873 | function _inCircle(p, s, x) { 874 | 875 | // circle center 876 | var cp = [ 877 | p[0] + 0.5 * s[0], 878 | p[1] + 0.5 * s[1] 879 | 880 | ]; 881 | var radius = s[0] * 0.5; 882 | 883 | // distance from `x` to circle center. 884 | var dist = Math.sqrt((x[0] - cp[0]) * (x[0] - cp[0]) + (x[1] - cp[1]) * (x[1] - cp[1])); 885 | 886 | return (dist <= radius); 887 | } 888 | 889 | 890 | /* 891 | Given a box with position `p`, width `s[0]`, height `[1]`, 892 | return whether the point with the position `x` is inside the box. 893 | */ 894 | function _inBox(p, s, x) { 895 | var minX = p[0]; 896 | var minY = p[1]; 897 | 898 | var maxX = p[0] + s[0]; 899 | var maxY = p[1] + s[1]; 900 | 901 | return ( 902 | minX <= x[0] && x[0] <= maxX && 903 | minY <= x[1] && x[1] <= maxY 904 | ); 905 | } 906 | 907 | 908 | /* 909 | Before adding a widget, move the window caret to the right of the previous widget if this.sameLineActive, 910 | ELSE start a line. 911 | you should ALWAYS call this function before adding a new widget. 912 | */ 913 | GUI.prototype._moveWindowCaret = function () { 914 | 915 | if (this.prevWidgetSizes == null) { 916 | // we have not yet laid out the first widget. Do nothing. 917 | return; 918 | } 919 | 920 | if (this.sameLineActive) { 921 | this.windowCaret = [this.windowCaret[0] + this.widgetSpacing + this.prevWidgetSizes[0], this.windowCaret[1]]; 922 | } else { 923 | this.windowCaret = [this.windowSpacing + this.windowPosition[0], this.windowCaret[1] + this.widgetSpacing + this.prevWidgetSizes[1]]; 924 | } 925 | 926 | // the user have to explicitly call sameLine() again if we he wants samLineActive again. 927 | this.sameLineActive = false; 928 | 929 | }; 930 | 931 | GUI.prototype._draggerFloat = function (widgetId, labelStr, value, color, colorHover, width, position, minVal, maxVal) { 932 | 933 | /* 934 | DRAGGER IO 935 | */ 936 | 937 | var draggerPosition = position; 938 | var draggerSizes = [ 939 | width, 940 | this._getTextSizes("0")[1] + 2 * this.draggerVerticalSpacing 941 | ]; 942 | 943 | var mouseCollision = _inBox(draggerPosition, draggerSizes, this.io.mousePositionCur); 944 | if ( 945 | mouseCollision && 946 | this.io.mouseLeftDownCur == true && this.io.mouseLeftDownPrev == false) { 947 | // if slider is clicked, it becomes active. 948 | this.activeWidgetId = widgetId; 949 | } 950 | 951 | if (this.activeWidgetId == widgetId) { 952 | value.val += 0.01 * (this.io.mousePositionCur[0] - this.io.mousePositionPrev[0]); 953 | value.val = clamp(value.val, minVal, maxVal); 954 | 955 | this.activeWidgetId = widgetId; 956 | 957 | } 958 | 959 | /* 960 | DRAGGER RENDERING 961 | */ 962 | var sliderValueNumDecimalDigits = 2; // hardcode this value for now. 963 | var sliderValueStr = labelStr + value.val.toFixed(sliderValueNumDecimalDigits); 964 | 965 | 966 | /* 967 | If either widget is active, OR we are hovering but not clicking, 968 | switch to hover color. 969 | */ 970 | var isHover = (this.activeWidgetId == widgetId) || (mouseCollision && !this.io.mouseLeftDownCur ); 971 | 972 | this._box( 973 | draggerPosition, 974 | draggerSizes, isHover ? colorHover : color); 975 | 976 | 977 | var sliderValueStrSizes = this._getTextSizes(sliderValueStr); 978 | 979 | 980 | // render text in slider 981 | this._textCenter(draggerPosition, draggerSizes, sliderValueStr); 982 | 983 | // return top right corner, and bottom right corner of the dragger. 984 | return { 985 | topRight: [draggerPosition[0] + draggerSizes[0], draggerPosition[1]], 986 | bottomRight: [draggerPosition[0] + draggerSizes[0], draggerPosition[1] + draggerSizes[1]], 987 | }; 988 | }; 989 | 990 | /* 991 | sublabels, 992 | min max, for all n. 993 | hover color, for all three. 994 | */ 995 | GUI.prototype._draggerFloatN = function (labelStr, value, N, minMaxValues, subLabels, colors) { 996 | this._moveWindowCaret(); 997 | 998 | if (!minMaxValues) 999 | minMaxValues = []; 1000 | 1001 | if (!subLabels) 1002 | subLabels = []; 1003 | 1004 | if (!colors) 1005 | colors = []; 1006 | 1007 | 1008 | // if minMaxValues is only a single min-max pair(a n array on the form [min,max]), 1009 | // then that pair becomes the value of the rest 1010 | // of the min-max pairs. 1011 | if (minMaxValues.length == 2 && typeof minMaxValues[0][0] == "undefined" ) { 1012 | var defaultValue = [minMaxValues[0], minMaxValues[1] ]; 1013 | for (var i = 0; i < N; ++i) { 1014 | minMaxValues[i] = defaultValue; 1015 | } 1016 | 1017 | 1018 | } 1019 | 1020 | 1021 | /* 1022 | Set default values of arguments 1023 | */ 1024 | for (var i = 0; i < N; ++i) { 1025 | if (!subLabels[i]) { 1026 | subLabels[i] = ""; 1027 | } 1028 | 1029 | if (!minMaxValues[i]) { 1030 | minMaxValues[i] = [-1, 1]; 1031 | } 1032 | 1033 | if (!colors[i]) { 1034 | colors[i] = [this.draggerFloatColor, this.draggerFloatColorHover]; 1035 | 1036 | } 1037 | } 1038 | 1039 | // width of a single subdragger. 1040 | var draggerWidth = 1041 | (((this.windowSizes[0] - 2 * this.windowSpacing) * (this.widgetHorizontalGrowRatio)) - (N - 1) * this.draggerWidgetHorizontalSpacing) / (N); 1042 | 1043 | var nDraggerPosition = this.windowCaret; 1044 | var formerDraggerPosition = {topRight: nDraggerPosition}; 1045 | 1046 | for (var iDragger = 0; iDragger < N; ++iDragger) { 1047 | var v = {val: value[iDragger]}; 1048 | 1049 | // first dragger has no spacing in front. 1050 | var hasFrontSpacing = (iDragger == 0) ? false : true; 1051 | 1052 | var position = [ 1053 | formerDraggerPosition.topRight[0] + (hasFrontSpacing ? this.draggerWidgetHorizontalSpacing : 0), 1054 | formerDraggerPosition.topRight[1]]; 1055 | 1056 | // make sure each subdragger has an unique widget-ID. 1057 | var draggerWidgetId = hashString(labelStr + (iDragger + "")); 1058 | 1059 | formerDraggerPosition = this._draggerFloat(draggerWidgetId, subLabels[iDragger], v, 1060 | colors[iDragger][0], 1061 | colors[iDragger][1], draggerWidth, position, minMaxValues[iDragger][0], minMaxValues[iDragger][1]); 1062 | 1063 | // update value 1064 | value[iDragger] = v.val; 1065 | } 1066 | 1067 | // the total size of all the N draggers. 1068 | var draggerSizes = [ 1069 | formerDraggerPosition.bottomRight[0] - 1070 | nDraggerPosition[0], 1071 | formerDraggerPosition.bottomRight[1] - 1072 | nDraggerPosition[1]]; 1073 | 1074 | // finally, we place a label after all the draggers. 1075 | var draggerLabelPosition = [nDraggerPosition[0] + draggerSizes[0] + this.widgetLabelHorizontalSpacing, nDraggerPosition[1]] 1076 | var draggerLabelStrSizes = [this._getTextSizes(labelStr)[0], draggerSizes[1]]; 1077 | this._textCenter(draggerLabelPosition, draggerLabelStrSizes, labelStr); 1078 | 1079 | this.prevWidgetSizes = [ 1080 | 1081 | draggerSizes[0] + this.widgetLabelHorizontalSpacing + draggerLabelStrSizes[0], 1082 | draggerSizes[1]]; 1083 | } 1084 | 1085 | GUI.prototype._slider = function (labelStr, value, min, max, doRounding, numDecimalDigits) { 1086 | 1087 | this._moveWindowCaret(); 1088 | 1089 | /* 1090 | SLIDER IO 1091 | */ 1092 | 1093 | var sliderPosition = this.windowCaret; 1094 | var widgetId = hashString(labelStr); 1095 | 1096 | // * if we use the height of a single digit, we know that the slider will always be high enough. 1097 | // (since all digits have equal height in our font). 1098 | // * also, we dynamically determine the slider width, based on the window width. 1099 | var sliderSizes = [ 1100 | (this.windowSizes[0] - 2 * this.windowSpacing) * this.widgetHorizontalGrowRatio, 1101 | this._getTextSizes("0")[1] + 2 * this.sliderVerticalSpacing 1102 | ]; 1103 | 1104 | var mouseCollision = _inBox(sliderPosition, sliderSizes, this.io.mousePositionCur); 1105 | 1106 | if ( 1107 | mouseCollision && 1108 | this.io.mouseLeftDownCur == true && this.io.mouseLeftDownPrev == false) { 1109 | // if slider is clicked, it becomes active. 1110 | this.activeWidgetId = widgetId; 1111 | } 1112 | 1113 | if (this.activeWidgetId == widgetId) { 1114 | // if the mouse is clicking on the slider, we modify `value.val` based 1115 | // on the x-position of the mouse. 1116 | 1117 | var xMax = sliderPosition[0] + sliderSizes[0]; 1118 | var xMin = sliderPosition[0]; 1119 | 1120 | /* 1121 | Values larger than xMin and xMax should not overflow or underflow the slider. 1122 | */ 1123 | var mouseX = clamp(this.io.mousePositionCur[0], xMin, xMax); 1124 | 1125 | value.val = (max - min) * ((mouseX - xMin) / (xMax - xMin)) + min; 1126 | 1127 | if (doRounding) 1128 | value.val = Math.round(value.val); 1129 | 1130 | this.activeWidgetId = widgetId; 1131 | 1132 | } 1133 | 1134 | /* 1135 | If either widget is active, OR we are hovering but not clicking, 1136 | switch to hover color. 1137 | */ 1138 | var isHover = (this.activeWidgetId == widgetId) || (mouseCollision && !this.io.mouseLeftDownCur ); 1139 | 1140 | 1141 | /* 1142 | SLIDER RENDERING 1143 | */ 1144 | 1145 | /* 1146 | Compute slider fill. Measures how much of the slider is filled. 1147 | In range [0,1] 1148 | */ 1149 | var sliderFill = (value.val - min) / (max - min); 1150 | 1151 | var sliderValueStr = value.val.toFixed(doRounding ? 0 : numDecimalDigits); 1152 | 1153 | this._box( 1154 | sliderPosition, 1155 | sliderSizes, isHover ? this.sliderBackgroundColorHover : this.sliderBackgroundColor); 1156 | 1157 | /* 1158 | Now fill the slider based on `sliderFill` 1159 | */ 1160 | this._box( 1161 | sliderPosition, 1162 | [sliderSizes[0] * sliderFill, sliderSizes[1]], 1163 | isHover ? this.sliderFillColorHover : this.sliderFillColor); 1164 | 1165 | var sliderValueStrSizes = this._getTextSizes(sliderValueStr); 1166 | 1167 | 1168 | // render text in slider 1169 | this._textCenter(sliderPosition, sliderSizes, sliderValueStr); 1170 | 1171 | // now render slider label. 1172 | var sliderLabelPosition = [sliderPosition[0] + sliderSizes[0] + this.widgetLabelHorizontalSpacing, sliderPosition[1]] 1173 | var sliderLabelStrSizes = [this._getTextSizes(labelStr)[0], sliderSizes[1]]; 1174 | this._textCenter(sliderLabelPosition, sliderLabelStrSizes, labelStr); 1175 | 1176 | this.prevWidgetSizes = [sliderSizes[0] + sliderLabelStrSizes[0], sliderSizes[1]]; 1177 | 1178 | } 1179 | 1180 | 1181 | GUI.prototype._window = function () { 1182 | 1183 | var widgetId = hashString(this.windowTitle); 1184 | 1185 | /* 1186 | WINDOW IO(move window when dragging the title-bar using the left mouse button) 1187 | */ 1188 | 1189 | var titleBarPosition = this.windowPosition; 1190 | var titleBarSizes = [this.windowSizes[0], this.titleBarHeight]; 1191 | 1192 | if ( 1193 | _inBox(titleBarPosition, titleBarSizes, this.io.mousePositionCur) && 1194 | this.io.mouseLeftDownCur == true && this.io.mouseLeftDownPrev == false) { 1195 | this.activeWidgetId = widgetId; 1196 | } 1197 | 1198 | if (this.activeWidgetId == widgetId) { 1199 | 1200 | if (_inBox(titleBarPosition, titleBarSizes, this.io.mousePositionCur)) { 1201 | // if mouse in title bar, just use the mouse position delta to adjust the window pos. 1202 | 1203 | this.windowPosition = [ 1204 | this.windowPosition[0] + (this.io.mousePositionCur[0] - this.io.mousePositionPrev[0]), 1205 | this.windowPosition[1] + (this.io.mousePositionCur[1] - this.io.mousePositionPrev[1]) 1206 | ]; 1207 | 1208 | // the mouse position relative to the top-left corner of the window. 1209 | this.relativeMousePosition = [ 1210 | (this.windowPosition[0] - this.io.mousePositionCur[0]), 1211 | (this.windowPosition[1] - this.io.mousePositionCur[1]) 1212 | ]; 1213 | 1214 | } else { 1215 | 1216 | /* 1217 | If the window cannot keep up with the mouse, we must use the relative mouse position to approximate 1218 | the change in (x,y) 1219 | */ 1220 | 1221 | this.windowPosition = [ 1222 | this.relativeMousePosition[0] + (this.io.mousePositionCur[0]), 1223 | this.relativeMousePosition[1] + (this.io.mousePositionCur[1]) 1224 | ]; 1225 | } 1226 | 1227 | // update title bar position. 1228 | titleBarPosition = this.windowPosition; 1229 | } 1230 | 1231 | /* 1232 | WINDOW RENDERING. 1233 | */ 1234 | 1235 | // draw title bar 1236 | this._box(titleBarPosition, titleBarSizes, this.titleBarColor); 1237 | 1238 | // draw title bar text 1239 | this._textCenter( 1240 | [this.windowPosition[0] + this.titleBarVerticalSpacing, this.windowPosition[1]], 1241 | [this._getTextSizes(this.windowTitle)[0], this.titleBarHeight], 1242 | this.windowTitle); 1243 | 1244 | // draw the actual window. 1245 | this._box([this.windowPosition[0], this.windowPosition[1] + this.titleBarHeight], this.windowSizes, 1246 | this.windowColor, this.windowAlpha); 1247 | 1248 | // setup the window-caret. The window-caret is where we will place the next widget in the window. 1249 | this.windowCaret = [ 1250 | this.windowPosition[0] + this.windowSpacing, 1251 | this.windowPosition[1] + this.windowSpacing + this.titleBarHeight]; 1252 | this.prevWidgetSizes = null; // should be null at the beginning. 1253 | 1254 | 1255 | /* 1256 | Determine whether the mouse is inside the window. We need this in some places. 1257 | */ 1258 | this.mouseInWindow = _inBox(titleBarPosition, 1259 | [this.windowSizes[0], this.titleBarHeight + this.windowSizes[1]], 1260 | this.io.mousePositionCur); 1261 | } 1262 | 1263 | 1264 | GUI.prototype._restoreGLState = function (gl) { 1265 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.lastElementArrayBuffer); 1266 | gl.bindBuffer(gl.ARRAY_BUFFER, this.lastArrayBuffer); 1267 | gl.useProgram(this.lastProgram); 1268 | gl.bindTexture(gl.TEXTURE_2D, this.lastTexture); 1269 | 1270 | if (this.lastEnableDepthTest) gl.enable(gl.DEPTH_TEST); else gl.disable(gl.DEPTH_TEST); 1271 | if (this.lastEnableBlend) gl.enable(gl.BLEND); else gl.disable(gl.BLEND); 1272 | } 1273 | 1274 | 1275 | GUI.prototype._backupGLState = function (gl) { 1276 | 1277 | this.lastProgram = gl.getParameter(gl.CURRENT_PROGRAM); 1278 | this.lastElementArrayBuffer = gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING); 1279 | this.lastArrayBuffer = gl.getParameter(gl.ARRAY_BUFFER_BINDING); 1280 | this.lastTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); 1281 | this.lastEnableDepthTest = gl.isEnabled(gl.DEPTH_TEST); 1282 | this.lastEnableBlend = gl.isEnabled(gl.BLEND); 1283 | 1284 | /* 1285 | TODO: figure out how to back up `blendFunc`. 1286 | */ 1287 | 1288 | } 1289 | 1290 | module.exports = GUI; 1291 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pnp-gui", 3 | "version": "0.0.4", 4 | "description": "A Immediate Mode WebGL GUI that aims to be easy to use and intergrate into any project.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo --verbose example/index.js -t glslify --live --open", 8 | "test": "standard" 9 | }, 10 | "keywords": [ 11 | "stackgl", 12 | "glsl", 13 | "gl", 14 | "webgl", 15 | "gui", 16 | "Immediate Mode GUI", 17 | "text", 18 | "font" 19 | ], 20 | "author": "Eric Arnebäck (https://github.com/Erkaman)", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "bunny": "^1.0.1", 24 | "geo-3d-box": "^2.0.2", 25 | "geo-3d-transform-mat4": "^1.0.0", 26 | "gl-camera-pos-from-view-matrix": "^1.0.1", 27 | "gl-geometry": "^3.1.0", 28 | "gl-now": "^1.4.0", 29 | "gl-vec3": "^1.0.3", 30 | "glsl-noise": "0.0.0", 31 | "glslify": "^5.0.2", 32 | "normals": "^1.0.1", 33 | "orbit-camera": "^1.0.0", 34 | "random-array": "0.0.2", 35 | "stanford-dragon": "^1.1.1", 36 | "vertices-bounding-box": "^1.0.0" 37 | }, 38 | "dependencies": { 39 | "clamp": "^1.0.1", 40 | "gl-buffer": "^2.1.2", 41 | "gl-mat4": "^1.1.4", 42 | "gl-shader": "^4.2.0", 43 | "gl-texture2d": "^2.0.10", 44 | "gl-vec2": "^1.0.0", 45 | "hash-string": "^1.0.0", 46 | "ndpack-image": "^2.0.0" 47 | }, 48 | "directories": { 49 | "example": "example" 50 | }, 51 | "browserify": { 52 | "transform": [ 53 | "glslify" 54 | ] 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/Erkaman/pnp-gui.git" 59 | }, 60 | "homepage": "https://github.com/Erkaman/pnp-gui#readme" 61 | } 62 | -------------------------------------------------------------------------------- /shaders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eric on 23/04/16. 3 | */ 4 | 5 | module.exports.vert = ` 6 | precision mediump float; 7 | 8 | attribute vec2 aPosition; 9 | attribute vec4 aColor; 10 | attribute vec2 aUv; 11 | 12 | uniform mat4 uProj; 13 | 14 | varying vec4 vColor; 15 | varying vec2 vUv; 16 | 17 | void main() { 18 | gl_Position = uProj * vec4(aPosition, 0, 1); 19 | vColor = aColor; 20 | vUv = aUv; 21 | } 22 | ` 23 | 24 | module.exports.frag = ` 25 | precision mediump float; 26 | 27 | varying vec4 vColor; 28 | varying vec2 vUv; 29 | 30 | uniform sampler2D uFontAtlas; 31 | 32 | void main() { 33 | vec4 sample = texture2D(uFontAtlas, vUv); 34 | gl_FragColor = vec4(vColor.xyz * sample.xyz, sample.x * vColor.a ); 35 | } 36 | ` --------------------------------------------------------------------------------