├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── data.jpg ├── default.json ├── index.html ├── lib ├── OK.js ├── dat.gui.js └── gl-matrix.js ├── sharevol.js └── src ├── index.html ├── main.js ├── shaders ├── lineShaderWEBGL.frag ├── lineShaderWEBGL.vert ├── textureShaderWEBGL.frag ├── textureShaderWEBGL.vert ├── volumeShaderWEBGL.frag └── volumeShaderWEBGL.vert ├── slicer.js └── volume.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #Requires google closure compiler 2 | VERSION = 0.1 3 | #COMP = java -jar compiler-latest/compiler.jar --jscomp_warning internetExplorerChecks --js= 4 | #FLAGS = --js_output_file= 5 | COMP = cp 6 | FLAGS = 7 | 8 | #Sources 9 | SCRIPTS = src/main.js src/slicer.js src/volume.js 10 | LIBS = lib/gl-matrix-min.js lib/dat.gui.min.js lib/OK-min.js lib/sharevol-min.js 11 | 12 | all: sharevol.js 13 | #Build the shaders into release index.html 14 | sed -e "/Volume vertex shader/ r src/shaders/volumeShaderWEBGL.vert" \ 15 | -e "/Volume fragment shader/ r src/shaders/volumeShaderWEBGL.frag" \ 16 | -e "/Texture vertex shader/ r src/shaders/textureShaderWEBGL.vert" \ 17 | -e "/Texture fragment shader/ r src/shaders/textureShaderWEBGL.frag" \ 18 | -e "/Line vertex shader/ r src/shaders/lineShaderWEBGL.vert" \ 19 | -e "/Line fragment shader/ r src/shaders/lineShaderWEBGL.frag" < src/index.html > index.html 20 | 21 | .PHONY : clean 22 | clean: 23 | -rm lib/*min.js 24 | -rm sharevol.js 25 | 26 | sharevol.js: $(LIBS) 27 | #Combine into final bundle 28 | cat $(LIBS) > sharevol.js 29 | 30 | lib/sharevol-min.js: $(SCRIPTS) 31 | cat $(SCRIPTS) > lib/sharevol-all.js 32 | $(COMP)lib/sharevol-all.js $(FLAGS)lib/sharevol-min.js 33 | 34 | lib/OK-min.js: lib/OK.js 35 | $(COMP)lib/OK.js $(FLAGS)lib/OK-min.js 36 | 37 | lib/gl-matrix-min.js: lib/gl-matrix.js 38 | $(COMP)lib/gl-matrix.js $(FLAGS)lib/gl-matrix-min.js 39 | 40 | lib/dat.gui.min.js: lib/gl-matrix.js 41 | $(COMP)lib/dat.gui.js $(FLAGS)lib/dat.gui.min.js 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ShareVol 2 | ======== 3 | Lightweight volume viewer in WebGL http://okaluza.github.io/sharevol 4 | 5 | Copyright (c) 2014, Monash University. All rights reserved. 6 | Author: Owen Kaluza - owen.kaluza ( at ) monash.edu 7 | 8 | Licensed under the GNU Lesser General Public License 9 | https://www.gnu.org/licenses/lgpl.html 10 | (If this doesn't suit your usage requirements, please contact - I'm open to releasing under other licenses) 11 | 12 | Now WebGL is well supported I wanted a tool to show some volume data with emphasis on simplicity, loading speed and high-quality rendering. Looking around, XTK and VolumeRC are the contenders, both seem to be part of larger projects which didn't suit my needs. 13 | 14 | I hope this code can become an easy to understand and lightweight base for sharing volume data on the web or developing volume rendering based tools. The aim is to keep this a small and manageable project and avoid turning it into a full featured rendering library. 15 | 16 | How to use it: 17 | -------------- 18 | The core files are 19 | 20 | - **index.html** (main html page including shaders) 21 | - **sharevol.js** (minified javascript code including library dependencies) 22 | 23 | The default action after loading is to attempt to read a parameter file ("default.json" if not otherwise specified). 24 | This should contain a reference to the image data url for the data to visualise and other vis settings. 25 | 26 | An example data set is provided in the file "data.jpg" 27 | (256x256x256, 1:1:1 converted to tiled 2d image) 28 | Courtesy of http://volvis.org/ Rotational C-arm x-ray scan of a human foot. Tissue and bone are present in the dataset, by Philips Research, Hamburg, Germany. 29 | 30 | Other parameter files can be specified by passing a url with the *data* parameter, eg: *index.html?data=myparams.json* (TODO: specify other URL options) 31 | 32 | The simplest way to view your own data set is fork this project and edit/replace "data.jpg" and "default.json". 33 | (If you merge changes into the gh-pages branch, your data should then be viewable at http://username.github.io/sharevol) 34 | 35 | TODO: describe parameters in json config. 36 | 37 | - To enable the volume renderer, ensure the property "volume" exists. 38 | - To enable the slice viewer, ensure the property "slicer" exists. 39 | 40 | TODO: Describe UI options and features. 41 | 42 | Acknowlegements: 43 | ---------------- 44 | 45 | The starting point of this code was Philip Rideout's excellent public domain tutorial on single pass raycasting... 46 | http://prideout.net/blog/?p=64 47 | 48 | I applied the concept of using 2D texture atlases from Vicomtech's work http://volumerc.org/demos/volren/ as WebGL doesn't support 3D textures. 49 | 50 | I also found these articles useful: 51 | http://sizecoding.blogspot.com.au/2008/08/isosurfaces-in-glsl.html 52 | http://graphicsrunner.blogspot.com.au/2009/01/volume-rendering-101.html 53 | 54 | Dependencies: 55 | ------------- 56 | DAT.GUI https://github.com/dataarts/dat.gui (Apache licensed) 57 | glMatrix: http://glmatrix.net/ 58 | OK.js (my simple utility library) 59 | 60 | Copies of all dependencies are provided, build minifies and combines all into sharevol.js. 61 | 62 | Other projects of interest: 63 | --------------------------- 64 | 65 | https://github.com/xtk/X 66 | https://github.com/VolumeRC 67 | 68 | VolumeRC provides some conversion scripts are particularly useful for creating tiled images compatible with sharevol from various data formats: 69 | https://github.com/VolumeRC/AtlasConversionScripts 70 | 71 | XTK seems to contain lots of conversion and loading goodies that might be of use if you want to get this code to load data sets in other formats. 72 | 73 | Tri-cubic filtering: 74 | -------------------- 75 | The optional tri-cubic filtering was ripped from Danny Ruijters 76 | http://www.dannyruijters.nl/cubicinterpolation/ 77 | 78 | Please cite their paper if you use the tricubic interpolation feature in any published work. 79 | See: http://www.dannyruijters.nl/cubicinterpolation/license.txt 80 | 81 | (If you don't wish to use it, just delete the interpolate_tricubic_fast() function from index.html) 82 | 83 | -------------------------------------------------------------------------------- /data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluza/sharevol/a54fb01a063d590fa948aa08ff12863bcc71ad1e/data.jpg -------------------------------------------------------------------------------- /default.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "nogui": false, 4 | "background": "rgba(81,77,74,1.00)" 5 | }, 6 | "colourmaps": [ 7 | { 8 | "colours": [ 9 | { 10 | "position": 0, 11 | "colour": "rgba(0,0,0,0.00)" 12 | }, 13 | { 14 | "position": 0.023438, 15 | "colour": "rgba(60,60,60,1.00)" 16 | }, 17 | { 18 | "position": 0.046875, 19 | "colour": "rgba(18,15,0,1.00)" 20 | }, 21 | { 22 | "position": 0.066641, 23 | "colour": "rgba(248,144,87,0.38)" 24 | }, 25 | { 26 | "position": 0.103047, 27 | "colour": "rgba(252,224,166,1.00)" 28 | }, 29 | { 30 | "position": 0.146016, 31 | "colour": "rgba(255,81,0,1.00)" 32 | }, 33 | { 34 | "position": 0.200703, 35 | "colour": "rgba(72,0,20,1.00)" 36 | }, 37 | { 38 | "position": 0.236084, 39 | "colour": "rgba(246,245,122,1.00)" 40 | }, 41 | { 42 | "position": 0.310078, 43 | "colour": "rgba(255,0,0,1.00)" 44 | }, 45 | { 46 | "position": 0.355, 47 | "colour": "rgba(255,255,255,0.00)" 48 | }, 49 | { 50 | "position": 0.894062, 51 | "colour": "rgba(255,255,255,0.00)" 52 | }, 53 | { 54 | "position": 1, 55 | "colour": "rgba(255,255,255,1.00)" 56 | } 57 | ] 58 | } 59 | ], 60 | "views": [ 61 | { 62 | "axes": false, 63 | "border": false, 64 | "translate": [ 65 | -0.05715767664977255, 66 | -0.031176914536239782, 67 | -1.6454482671904334 68 | ], 69 | "rotate": [ 70 | 0.5536984801292419, 71 | 0.1137750968337059, 72 | -0.7585440278053284, 73 | 0.32404208183288574 74 | ] 75 | } 76 | ], 77 | "objects": [ 78 | { 79 | "name": "volume", 80 | "samples": 256, 81 | "isovalue": 0.45, 82 | "isowalls": true, 83 | "isoalpha": 1, 84 | "isosmooth": 0.5045783843331688, 85 | "colour": [ 86 | 255, 87 | 245.46299999999997, 88 | 226.95000000000002 89 | ], 90 | "density": 5, 91 | "power": 1, 92 | "colourmap": 0, 93 | "tricubicfilter": false, 94 | "zmin": 0, 95 | "zmax": 1, 96 | "ymin": 0, 97 | "ymax": 0.9723324001313833, 98 | "xmin": 0, 99 | "xmax": 1, 100 | "brightness": -0.11780890196982163, 101 | "contrast": 1, 102 | "volume": { 103 | "url": "data.jpg", 104 | "res": [ 105 | 256, 106 | 256, 107 | 256 108 | ], 109 | "scale": [ 110 | 1, 111 | 1, 112 | 1 113 | ] 114 | }, 115 | "slices": { 116 | "properties": { 117 | "show": true, 118 | "X": 93, 119 | "Y": 111, 120 | "Z": 144, 121 | "brightness": 0, 122 | "contrast": 1, 123 | "power": 1, 124 | "usecolourmap": false, 125 | "layout": "x|y|z", 126 | "zoom": 0.6683351069050206 127 | } 128 | } 129 | } 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebGL Volume Viewer 7 | 8 | 9 | 20 | 21 | 397 | 398 | 415 | 416 | 510 | 511 | 539 | 540 | 552 | 553 | 603 | 604 | 605 | 606 | 607 | 608 | 611 | 612 |
613 |

Loading...

614 |
615 | 616 |
617 |
×
618 |

Colourmaps:

619 |
620 | 629 |
630 | 631 | 632 |
633 |
634 |
635 | 636 |
637 | 638 | 639 | 640 | 641 | -------------------------------------------------------------------------------- /lib/OK.js: -------------------------------------------------------------------------------- 1 | /** @preserve Javascript graphics utility library 2 | * Helper functions, WebGL classes, Mouse input, Colours and Gradients UI 3 | * Copyright (c) 2014, Owen Kaluza 4 | * Released into public domain: 5 | * This program is free software. It comes without any warranty, to 6 | * the extent permitted by applicable law. You can redistribute it 7 | * and/or modify it as long as this header remains intact 8 | */ 9 | //Miscellaneous javascript helper functions 10 | //Module definition, TODO: finish module 11 | var OK = (function () { 12 | var ok = {}; 13 | 14 | ok.debug_on = false; 15 | ok.debug = function(str) { 16 | if (!ok.debug_on) return; 17 | var uconsole = document.getElementById('console'); 18 | if (uconsole) 19 | uconsole.innerHTML = "
" + str + "
" + uconsole.innerHTML; 20 | else 21 | console.log(str); 22 | }; 23 | 24 | ok.clear = function consoleClear() { 25 | var uconsole = document.getElementById('console'); 26 | if (uconsole) uconsole.innerHTML = ''; 27 | }; 28 | 29 | return ok; 30 | }()); 31 | 32 | function getSearchVariable(variable, defaultVal) { 33 | var query = window.location.search.substring(1); 34 | var vars = query.split("&"); 35 | for (var i=0;i 0) 91 | element.removeChild(element.firstChild); 92 | } 93 | } 94 | 95 | //Browser specific animation frame request 96 | if ( !window.requestAnimationFrame ) { 97 | window.requestAnimationFrame = ( function() { 98 | return window.webkitRequestAnimationFrame || 99 | window.mozRequestAnimationFrame || 100 | window.oRequestAnimationFrame || 101 | window.msRequestAnimationFrame; 102 | } )(); 103 | } 104 | 105 | //Browser specific full screen request 106 | function requestFullScreen(id) { 107 | var element = document.getElementById(id); 108 | if (element.requestFullscreen) 109 | element.requestFullscreen(); 110 | else if (element.mozRequestFullScreen) 111 | element.mozRequestFullScreen(); 112 | else if (element.webkitRequestFullScreen) 113 | element.webkitRequestFullScreen(); 114 | } 115 | 116 | function typeOf(value) { 117 | var s = typeof value; 118 | if (s === 'object') { 119 | if (value) { 120 | if (typeof value.length === 'number' && 121 | !(value.propertyIsEnumerable('length')) && 122 | typeof value.splice === 'function') { 123 | s = 'array'; 124 | } 125 | } else { 126 | s = 'null'; 127 | } 128 | } 129 | return s; 130 | } 131 | 132 | function isEmpty(o) { 133 | var i, v; 134 | if (typeOf(o) === 'object') { 135 | for (i in o) { 136 | v = o[i]; 137 | if (v !== undefined && typeOf(v) !== 'function') { 138 | return false; 139 | } 140 | } 141 | } 142 | return true; 143 | } 144 | 145 | //AJAX 146 | //Reads a file from server, responds when done with file data + passed name to callback function 147 | function ajaxReadFile(filename, callback, nocache, progress, headers) 148 | { 149 | var http = new XMLHttpRequest(); 150 | var total = 0; 151 | if (progress != undefined) { 152 | if (typeof(progress) == 'number') 153 | total = progress; 154 | else 155 | http.onprogress = progress; 156 | } 157 | 158 | http.onreadystatechange = function() 159 | { 160 | if (total > 0 && http.readyState > 2) { 161 | //Passed size progress 162 | var recvd = parseInt(http.responseText.length); 163 | //total = parseInt(http.getResponseHeader('Content-length')) 164 | if (progress) setProgress(recvd / total * 100); 165 | } 166 | 167 | if (http.readyState == 4) { 168 | if (http.status == 200) { 169 | if (progress) setProgress(100); 170 | OK.debug("RECEIVED: " + filename); 171 | if (callback) 172 | callback(http.responseText, filename); 173 | } else { 174 | if (callback) 175 | callback("Error: " + http.status + " : " + filename); //Error callback 176 | else 177 | OK.debug("Ajax Read File Error: returned status code " + http.status + " " + http.statusText); 178 | } 179 | } 180 | } 181 | 182 | //Add date to url to prevent caching 183 | if (nocache) 184 | { 185 | var d = new Date(); 186 | http.open("GET", filename + "?d=" + d.getTime(), true); 187 | } 188 | else 189 | http.open("GET", filename, true); 190 | 191 | //Custom headers 192 | for (var key in headers) 193 | http.setRequestHeader(key, headers[key]); 194 | 195 | http.send(null); 196 | } 197 | 198 | function readURL(url, nocache, progress) { 199 | //Read url (synchronous) 200 | var http = new XMLHttpRequest(); 201 | var total = 0; 202 | if (progress != undefined) { 203 | if (typeof(progress) == 'number') 204 | total = progress; 205 | else 206 | http.onprogress = progress; 207 | } 208 | 209 | http.onreadystatechange = function() 210 | { 211 | if (total > 0 && http.readyState > 2) { 212 | //Passed size progress 213 | var recvd = parseInt(http.responseText.length); 214 | //total = parseInt(http.getResponseHeader('Content-length')) 215 | if (progress) setProgress(recvd / total * 100); 216 | } 217 | } 218 | 219 | //Add date to url to prevent caching 220 | if (nocache) 221 | { 222 | var d = new Date(); 223 | http.open("GET", url + "?d=" + d.getTime(), false); 224 | } else 225 | http.open('GET', url, false); 226 | http.overrideMimeType('text/plain; charset=x-user-defined'); 227 | http.send(null); 228 | if (http.status != 200) return ''; 229 | if (progress) setProgress(100); 230 | return http.responseText; 231 | } 232 | 233 | function updateProgress(evt) 234 | { 235 | //evt.loaded: bytes browser received/sent 236 | //evt.total: total bytes set in header by server (for download) or from client (upload) 237 | if (evt.lengthComputable) { 238 | setProgress(evt.loaded / evt.total * 100); 239 | OK.debug(evt.loaded + " / " + evt.total); 240 | } 241 | } 242 | 243 | function setProgress(percentage) 244 | { 245 | var val = Math.round(percentage); 246 | $S('progressbar').width = (3 * val) + "px"; 247 | $('progressstatus').innerHTML = val + "%"; 248 | } 249 | 250 | //Posts request to server, responds when done with response data to callback function 251 | function ajaxPost(url, params, callback, progress, headers) 252 | { 253 | var http = new XMLHttpRequest(); 254 | if (progress != undefined) http.upload.onprogress = progress; 255 | 256 | http.onreadystatechange = function() 257 | { 258 | if (http.readyState == 4) { 259 | if (http.status == 200) { 260 | if (progress) setProgress(100); 261 | OK.debug("POST: " + url); 262 | if (callback) 263 | callback(http.responseText); 264 | } else { 265 | if (callback) 266 | callback("Error, status:" + http.status); //Error callback 267 | else 268 | OK.debug("Ajax Post Error: returned status code " + http.status + " " + http.statusText); 269 | } 270 | } 271 | } 272 | 273 | http.open("POST", url, true); 274 | 275 | //Send the proper header information along with the request 276 | if (typeof(params) == 'string') { 277 | http.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 278 | http.setRequestHeader("Content-length", params.length); 279 | } 280 | 281 | //Custom headers 282 | if (headers) { 283 | for (key in headers) 284 | //alert(key + " : " + headers[key]); 285 | http.setRequestHeader(key, headers[key]); 286 | } 287 | 288 | http.send(params); 289 | } 290 | 291 | 292 | var defaultMouse; 293 | var dragMouse; //Global drag tracking 294 | 295 | //Handler class from passed functions 296 | /** 297 | * @constructor 298 | */ 299 | function MouseEventHandler(click, wheel, move, down, up, leave, pinch) { 300 | //All these functions should take (event, mouse) 301 | this.click = click; 302 | this.wheel = wheel; 303 | this.move = move; 304 | this.down = down; 305 | this.up = up; 306 | this.leave = leave; 307 | this.pinch = pinch; 308 | } 309 | 310 | /** 311 | * @constructor 312 | */ 313 | function Mouse(element, handler, enableContext) { 314 | this.element = element; 315 | //Custom handler for mouse actions... 316 | //requires members: click(event, mouse), move(event, mouse) and wheel(event, mouse) 317 | this.handler = handler; 318 | 319 | this.disabled = false; 320 | this.isdown = false; 321 | this.button = null; 322 | this.dragged = false; 323 | this.x = 0; 324 | this.x = 0; 325 | this.absoluteX = 0; 326 | this.absoluteY = 0; 327 | this.lastX = 0; 328 | this.lastY = 0; 329 | this.slider = null; 330 | this.spin = 0; 331 | //Option settings... 332 | this.moveUpdate = false; //Save mouse move origin once on mousedown or every move 333 | this.enableContext = enableContext ? true : false; 334 | 335 | element.addEventListener("onwheel" in document ? "wheel" : "mousewheel", handleMouseWheel, false); 336 | element.onmousedown = handleMouseDown; 337 | element.onmouseout = handleMouseLeave; 338 | document.onmouseup = handleMouseUp; 339 | document.onmousemove = handleMouseMove; 340 | //Touch events! testing... 341 | element.addEventListener("touchstart", touchHandler, true); 342 | element.addEventListener("touchmove", touchHandler, true); 343 | element.addEventListener("touchend", touchHandler, true); 344 | //To disable context menu 345 | element.oncontextmenu = function() {return this.mouse.enableContext;} 346 | } 347 | 348 | Mouse.prototype.setDefault = function() { 349 | //Sets up this mouse as the default for the document 350 | //Multiple mouse handlers can be created for elements but only 351 | //one should be set to handle document events 352 | defaultMouse = document.mouse = this; 353 | } 354 | 355 | Mouse.prototype.update = function(e) { 356 | // Get the mouse position relative to the document. 357 | if (!e) var e = window.event; 358 | var coord = mousePageCoord(e); 359 | this.x = coord[0]; 360 | this.y = coord[1]; 361 | 362 | //Save doc relative coords 363 | this.absoluteX = this.x; 364 | this.absoluteY = this.y; 365 | //Get element offset in document 366 | var offset = findElementPos(this.element); 367 | //Convert coords to position relative to element 368 | this.x -= offset[0]; 369 | this.y -= offset[1]; 370 | //Save position without scrolling, only checked in ff5 & chrome12 371 | this.clientx = e.clientX - offset[0]; 372 | this.clienty = e.clientY - offset[1]; 373 | } 374 | 375 | function mousePageCoord(event) { 376 | //Note: screen relative coords are only that are consistent (e.screenX/Y) 377 | var x,y; 378 | if (event.pageX || event.pageY) { 379 | x = event.pageX; 380 | y = event.pageY; 381 | } 382 | else { 383 | x = event.clientX + document.body.scrollLeft + 384 | document.documentElement.scrollLeft; 385 | y = event.clientY + document.body.scrollTop + 386 | document.documentElement.scrollTop; 387 | } 388 | return [x,y]; 389 | } 390 | 391 | function elementRelativeCoord(element, coord) { 392 | var offset = findElementPos(element); 393 | coord[0] -= offset[0]; 394 | coord[1] -= offset[1]; 395 | } 396 | 397 | 398 | // Get offset of element 399 | function findElementPos(obj) { 400 | var curleft = curtop = 0; 401 | //if (obj.offsetParent) { //Fix for chrome not getting actual object's offset here 402 | do { 403 | curleft += obj.offsetLeft; 404 | curtop += obj.offsetTop; 405 | } while (obj = obj.offsetParent); 406 | //} 407 | return [curleft,curtop]; 408 | } 409 | 410 | function getMouse(event) { 411 | if (!event) event = window.event; //access the global (window) event object 412 | var mouse = event.target.mouse; 413 | if (mouse) return mouse; 414 | //Attempt to find in parent nodes 415 | var target = event.target; 416 | var i = 0; 417 | while (target != document) { 418 | target = target.parentNode; 419 | if (target.mouse) return target.mouse; 420 | } 421 | 422 | return null; 423 | } 424 | 425 | function handleMouseDown(event) { 426 | //Event delegation details 427 | var mouse = getMouse(event); 428 | if (!mouse || mouse.disabled) return true; 429 | var e = event || window.event; 430 | mouse.target = e.target; 431 | //Clear dragged flag on mouse down 432 | mouse.dragged = false; 433 | 434 | mouse.update(event); 435 | if (!mouse.isdown) { 436 | mouse.lastX = mouse.absoluteX; 437 | mouse.lastY = mouse.absoluteY; 438 | } 439 | mouse.isdown = true; 440 | dragMouse = mouse; 441 | mouse.button = event.button; 442 | //Set document move & up event handlers to this.mouse object's 443 | document.mouse = mouse; 444 | 445 | //Handler for mouse down 446 | var action = true; 447 | if (mouse.handler.down) action = mouse.handler.down(event, mouse); 448 | //If handler returns false, prevent default action 449 | if (!action && event.preventDefault) event.preventDefault(); 450 | return action; 451 | } 452 | 453 | //Default handlers for up & down, call specific handlers on element 454 | function handleMouseUp(event) { 455 | var mouse = document.mouse; 456 | if (!mouse || mouse.disabled) return true; 457 | var action = true; 458 | if (mouse.isdown) 459 | { 460 | mouse.update(event); 461 | if (mouse.handler.click) action = mouse.handler.click(event, mouse); 462 | mouse.isdown = false; 463 | dragMouse = null; 464 | mouse.button = null; 465 | mouse.dragged = false; 466 | } 467 | if (mouse.handler.up) action = action && mouse.handler.up(event, mouse); 468 | //Restore default mouse on document 469 | document.mouse = defaultMouse; 470 | 471 | //If handler returns false, prevent default action 472 | if (!action && event.preventDefault) event.preventDefault(); 473 | return action; 474 | } 475 | 476 | function handleMouseMove(event) { 477 | //Use previous mouse if dragging 478 | var mouse = dragMouse ? dragMouse : getMouse(event); 479 | if (!mouse || mouse.disabled) return true; 480 | mouse.update(event); 481 | mouse.deltaX = mouse.absoluteX - mouse.lastX; 482 | mouse.deltaY = mouse.absoluteY - mouse.lastY; 483 | var action = true; 484 | 485 | //Set dragged flag if moved more than limit 486 | if (!mouse.dragged && mouse.isdown && Math.abs(mouse.deltaX) + Math.abs(mouse.deltaY) > 3) 487 | mouse.dragged = true; 488 | 489 | if (mouse.handler.move) 490 | action = mouse.handler.move(event, mouse); 491 | 492 | if (mouse.moveUpdate) { 493 | //Constant update of last position 494 | mouse.lastX = mouse.absoluteX; 495 | mouse.lastY = mouse.absoluteY; 496 | } 497 | 498 | //If handler returns false, prevent default action 499 | if (!action && event.preventDefault) event.preventDefault(); 500 | return action; 501 | } 502 | 503 | function handleMouseWheel(event) { 504 | var mouse = getMouse(event); 505 | if (!mouse || mouse.disabled) return true; 506 | mouse.update(event); 507 | var action = false; //Default action disabled 508 | 509 | var delta = event.deltaY ? -event.deltaY : event.wheelDelta; 510 | event.spin = delta > 0 ? 1 : -1; 511 | 512 | if (mouse.handler.wheel) action = mouse.handler.wheel(event, mouse); 513 | 514 | //If handler returns false, prevent default action 515 | if (!action && event.preventDefault) event.preventDefault(); 516 | return action; 517 | } 518 | 519 | function handleMouseLeave(event) { 520 | var mouse = getMouse(event); 521 | if (!mouse || mouse.disabled) return true; 522 | 523 | var action = true; 524 | if (mouse.handler.leave) action = mouse.handler.leave(event, mouse); 525 | 526 | //If handler returns false, prevent default action 527 | if (!action && event.preventDefault) event.preventDefault(); 528 | event.returnValue = action; //IE 529 | return action; 530 | } 531 | 532 | //Basic touch event handling 533 | //Based on: http://ross.posterous.com/2008/08/19/iphone-touch-events-in-javascript/ 534 | //Pinch handling all by OK 535 | function touchHandler(event) 536 | { 537 | var touches = event.changedTouches, 538 | first = touches[0], 539 | simulate = null, //Mouse event to simulate 540 | prevent = false, 541 | mouse = getMouse(event); 542 | 543 | switch(event.type) 544 | { 545 | case "touchstart": 546 | if (event.touches.length == 2) { 547 | mouse.isdown = false; //Ignore first pinch touchdown being processed as mousedown 548 | mouse.scaling = 0; 549 | } else 550 | simulate = "mousedown"; 551 | break; 552 | case "touchmove": 553 | if (mouse.scaling != null && event.touches.length == 2) { 554 | var dist = Math.sqrt( 555 | (event.touches[0].pageX-event.touches[1].pageX) * (event.touches[0].pageX-event.touches[1].pageX) + 556 | (event.touches[0].pageY-event.touches[1].pageY) * (event.touches[0].pageY-event.touches[1].pageY)); 557 | 558 | if (mouse.scaling > 0) { 559 | event.distance = (dist - mouse.scaling); 560 | if (mouse.handler.pinch) action = mouse.handler.pinch(event, mouse); 561 | //If handler returns false, prevent default action 562 | var action = true; 563 | if (!action && event.preventDefault) event.preventDefault(); // Firefox 564 | event.returnValue = action; //IE 565 | } else 566 | mouse.scaling = dist; 567 | } else 568 | simulate = "mousemove"; 569 | break; 570 | case "touchend": 571 | if (mouse.scaling != null) { 572 | //Pinch sends two touch start/end, 573 | //only turn off scaling after 2nd touchend 574 | if (mouse.scaling == 0) 575 | mouse.scaling = null; 576 | else 577 | mouse.scaling = 0; 578 | } else 579 | simulate = "mouseup"; 580 | break; 581 | default: 582 | return; 583 | } 584 | if (event.touches.length > 1) //Avoid processing multiple touch except pinch zoom 585 | simulate = null; 586 | 587 | //Passes other events on as simulated mouse events 588 | if (simulate) { 589 | //OK.debug(event.type + " - " + event.touches.length + " touches"); 590 | 591 | //initMouseEvent(type, canBubble, cancelable, view, clickCount, 592 | // screenX, screenY, clientX, clientY, ctrlKey, 593 | // altKey, shiftKey, metaKey, button, relatedTarget); 594 | var simulatedEvent = document.createEvent("MouseEvent"); 595 | simulatedEvent.initMouseEvent(simulate, true, true, window, 1, 596 | first.screenX, first.screenY, 597 | first.clientX, first.clientY, event.ctrlKey, 598 | event.altKey, event.shiftKey, event.metaKey, 0 /*left*/, null); 599 | 600 | //Prevent default where requested 601 | prevent = !first.target.dispatchEvent(simulatedEvent); 602 | event.preventDefault(); 603 | } 604 | 605 | //if (prevent || scaling) 606 | // event.preventDefault(); 607 | 608 | } 609 | 610 | 611 | /** 612 | * WebGL interface object 613 | * standard utilities for WebGL 614 | * Shader & matrix utilities for 3d & 2d 615 | * functions for 2d rendering / image processing 616 | * (c) Owen Kaluza 2012 617 | */ 618 | 619 | /** 620 | * @constructor 621 | */ 622 | function Viewport(x, y, width, height) { 623 | this.x = x; 624 | this.y = y; 625 | this.width = width; 626 | this.height = height; 627 | } 628 | 629 | /** 630 | * @constructor 631 | */ 632 | function WebGL(canvas, options) { 633 | this.program = null; 634 | this.modelView = new ViewMatrix(); 635 | this.perspective = new ViewMatrix(); 636 | this.textures = []; 637 | this.timer = null; 638 | 639 | if (!window.WebGLRenderingContext) throw "No browser WebGL support"; 640 | 641 | // Try to grab the standard context. If it fails, fallback to experimental. 642 | try { 643 | this.gl = canvas.getContext("webgl", options) || canvas.getContext("experimental-webgl", options); 644 | } catch (e) { 645 | OK.debug("detectGL exception: " + e); 646 | throw "No context" 647 | } 648 | this.viewport = new Viewport(0, 0, canvas.width, canvas.height); 649 | if (!this.gl) throw "Failed to get context"; 650 | 651 | } 652 | 653 | WebGL.prototype.setMatrices = function() { 654 | //Model view matrix 655 | this.gl.uniformMatrix4fv(this.program.mvMatrixUniform, false, this.modelView.matrix); 656 | //Perspective matrix 657 | this.gl.uniformMatrix4fv(this.program.pMatrixUniform, false, this.perspective.matrix); 658 | //Normal matrix 659 | if (this.program.nMatrixUniform) { 660 | var nMatrix = mat4.create(this.modelView.matrix); 661 | mat4.inverse(nMatrix); 662 | mat4.transpose(nMatrix); 663 | this.gl.uniformMatrix4fv(this.program.nMatrixUniform, false, nMatrix); 664 | } 665 | } 666 | 667 | WebGL.prototype.initDraw2d = function() { 668 | this.gl.viewport(this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height); 669 | 670 | this.gl.enableVertexAttribArray(this.program.attributes["aVertexPosition"]); 671 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexPositionBuffer); 672 | this.gl.vertexAttribPointer(this.program.attributes["aVertexPosition"], this.vertexPositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 673 | 674 | if (this.program.attributes["aTextureCoord"]) { 675 | this.gl.enableVertexAttribArray(this.program.attributes["aTextureCoord"]); 676 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.textureCoordBuffer); 677 | this.gl.vertexAttribPointer(this.program.attributes["aTextureCoord"], this.textureCoordBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 678 | } 679 | 680 | this.setMatrices(); 681 | } 682 | 683 | WebGL.prototype.updateTexture = function(texture, image, unit) { 684 | //Set default texture unit if not provided 685 | if (unit == undefined) unit = this.gl.TEXTURE0; 686 | this.gl.activeTexture(unit); 687 | this.gl.bindTexture(this.gl.TEXTURE_2D, texture); 688 | this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image); 689 | this.gl.bindTexture(this.gl.TEXTURE_2D, null); 690 | } 691 | 692 | WebGL.prototype.init2dBuffers = function(unit) { 693 | //Set default texture unit if not provided 694 | if (unit == undefined) unit = this.gl.TEXTURE0; 695 | //All output drawn onto a single 2x2 quad 696 | this.vertexPositionBuffer = this.gl.createBuffer(); 697 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexPositionBuffer); 698 | var vertexPositions = [1.0,1.0, -1.0,1.0, 1.0,-1.0, -1.0,-1.0]; 699 | this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(vertexPositions), this.gl.STATIC_DRAW); 700 | this.vertexPositionBuffer.itemSize = 2; 701 | this.vertexPositionBuffer.numItems = 4; 702 | 703 | //Gradient texture 704 | this.gl.activeTexture(unit); 705 | this.gradientTexture = this.gl.createTexture(); 706 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.gradientTexture); 707 | 708 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST); 709 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST); 710 | 711 | //Texture coords 712 | this.textureCoordBuffer = this.gl.createBuffer(); 713 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.textureCoordBuffer); 714 | var textureCoords = [1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0]; 715 | this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(textureCoords), this.gl.STATIC_DRAW); 716 | this.textureCoordBuffer.itemSize = 2; 717 | this.textureCoordBuffer.numItems = 4; 718 | } 719 | 720 | WebGL.prototype.loadTexture = function(image, filter) { 721 | if (filter == undefined) filter = this.gl.NEAREST; 722 | this.texid = this.textures.length; 723 | this.textures.push(this.gl.createTexture()); 724 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[this.texid]); 725 | //this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, true); 726 | //(Ability to set texture type?) 727 | this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.LUMINANCE, this.gl.LUMINANCE, this.gl.UNSIGNED_BYTE, image); 728 | //this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image); 729 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, filter); 730 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, filter); 731 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); 732 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); 733 | this.gl.bindTexture(this.gl.TEXTURE_2D, null); 734 | return this.textures[this.texid]; 735 | } 736 | 737 | WebGL.prototype.setPerspective = function(fovy, aspect, znear, zfar) { 738 | this.perspective.matrix = mat4.perspective(fovy, aspect, znear, zfar); 739 | } 740 | 741 | WebGL.prototype.use = function(program) { 742 | this.program = program; 743 | if (this.program.program) 744 | this.gl.useProgram(this.program.program); 745 | } 746 | 747 | /** 748 | * @constructor 749 | */ 750 | //Program object 751 | function WebGLProgram(gl, vs, fs) { 752 | //Can be passed source directly or script tag 753 | this.program = null; 754 | if (vs.indexOf("main") < 0) vs = getSourceFromElement(vs); 755 | if (fs.indexOf("main") < 0) fs = getSourceFromElement(fs); 756 | //Pass in vertex shader, fragment shaders... 757 | this.gl = gl; 758 | if (this.program && this.gl.isProgram(this.program)) 759 | { 760 | //Clean up previous shader set 761 | if (this.gl.isShader(this.vshader)) 762 | { 763 | this.gl.detachShader(this.program, this.vshader); 764 | this.gl.deleteShader(this.vshader); 765 | } 766 | if (this.gl.isShader(this.fshader)) 767 | { 768 | this.gl.detachShader(this.program, this.fshader); 769 | this.gl.deleteShader(this.fshader); 770 | } 771 | this.gl.deleteProgram(this.program); //Required for chrome, doesn't like re-using this.program object 772 | } 773 | 774 | this.program = this.gl.createProgram(); 775 | 776 | this.vshader = this.compileShader(vs, this.gl.VERTEX_SHADER); 777 | this.fshader = this.compileShader(fs, this.gl.FRAGMENT_SHADER); 778 | 779 | this.gl.attachShader(this.program, this.vshader); 780 | this.gl.attachShader(this.program, this.fshader); 781 | 782 | this.gl.linkProgram(this.program); 783 | 784 | if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) { 785 | throw "Could not initialise shaders: " + this.gl.getProgramInfoLog(this.program); 786 | } 787 | } 788 | 789 | WebGLProgram.prototype.compileShader = function(source, type) { 790 | //alert("Compiling " + type + " Source == " + source); 791 | var shader = this.gl.createShader(type); 792 | this.gl.shaderSource(shader, source); 793 | this.gl.compileShader(shader); 794 | if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) 795 | throw this.gl.getShaderInfoLog(shader); 796 | return shader; 797 | } 798 | 799 | //Setup and load uniforms 800 | WebGLProgram.prototype.setup = function(attributes, uniforms, noenable) { 801 | if (!this.program) return; 802 | if (attributes == undefined) attributes = ["aVertexPosition", "aTextureCoord"]; 803 | this.attributes = {}; 804 | var i; 805 | for (i in attributes) { 806 | this.attributes[attributes[i]] = this.gl.getAttribLocation(this.program, attributes[i]); 807 | if (!noenable) this.gl.enableVertexAttribArray(this.attributes[attributes[i]]); 808 | } 809 | 810 | this.uniforms = {}; 811 | for (i in uniforms) 812 | this.uniforms[uniforms[i]] = this.gl.getUniformLocation(this.program, uniforms[i]); 813 | this.mvMatrixUniform = this.gl.getUniformLocation(this.program, "uMVMatrix"); 814 | this.pMatrixUniform = this.gl.getUniformLocation(this.program, "uPMatrix"); 815 | this.nMatrixUniform = this.gl.getUniformLocation(this.program, "uNMatrix"); 816 | } 817 | 818 | /** 819 | * @constructor 820 | */ 821 | function ViewMatrix() { 822 | this.matrix = mat4.create(); 823 | mat4.identity(this.matrix); 824 | this.stack = []; 825 | } 826 | 827 | ViewMatrix.prototype.toString = function() { 828 | return JSON.stringify(this.toArray()); 829 | } 830 | 831 | ViewMatrix.prototype.toArray = function() { 832 | return JSON.parse(mat4.str(this.matrix)); 833 | } 834 | 835 | ViewMatrix.prototype.push = function(m) { 836 | if (m) { 837 | this.stack.push(mat4.create(m)); 838 | this.matrix = mat4.create(m); 839 | } else { 840 | this.stack.push(mat4.create(this.matrix)); 841 | } 842 | } 843 | 844 | ViewMatrix.prototype.pop = function() { 845 | if (this.stack.length == 0) { 846 | throw "Matrix stack underflow"; 847 | } 848 | this.matrix = this.stack.pop(); 849 | return this.matrix; 850 | } 851 | 852 | ViewMatrix.prototype.mult = function(m) { 853 | mat4.multiply(this.matrix, m); 854 | } 855 | 856 | ViewMatrix.prototype.identity = function() { 857 | mat4.identity(this.matrix); 858 | } 859 | 860 | ViewMatrix.prototype.scale = function(v) { 861 | mat4.scale(this.matrix, v); 862 | } 863 | 864 | ViewMatrix.prototype.translate = function(v) { 865 | mat4.translate(this.matrix, v); 866 | } 867 | 868 | ViewMatrix.prototype.rotate = function(angle,v) { 869 | var arad = angle * Math.PI / 180.0; 870 | mat4.rotate(this.matrix, arad, v); 871 | } 872 | 873 | /** 874 | * @constructor 875 | */ 876 | function Palette(source, premultiply) { 877 | this.premultiply = premultiply; 878 | //Default transparent black background 879 | this.background = new Colour("rgba(0,0,0,0)"); 880 | //Colour palette array 881 | this.colours = []; 882 | this.slider = new Image(); 883 | this.slider.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAPCAYAAAA2yOUNAAAAj0lEQVQokWNIjHT8/+zZs//Pnj37/+TJk/9XLp/+f+bEwf9HDm79v2Prqv9aKrz/GUYVEaeoMDMQryJXayWIoi0bFmFV1NWS+z/E1/Q/AwMDA0NVcez/LRsWoSia2luOUAADVcWx/xfO6/1/5fLp/1N7y//HhlmhKoCBgoyA/w3Vyf8jgyyxK4CBUF8zDAUAAJRXY0G1eRgAAAAASUVORK5CYII="; 884 | 885 | if (!source) { 886 | //Default greyscale 887 | this.colours.push(new ColourPos("rgba(255,255,255,1)", 0)); 888 | this.colours.push(new ColourPos("rgba(0,0,0,1)", 1.0)); 889 | return; 890 | } 891 | 892 | var calcPositions = false; 893 | 894 | if (typeof(source) == 'string') { 895 | //Palette string data parser 896 | var lines = source.split(/[\n;]/); // split on newlines and/or semi-colons 897 | var position; 898 | for (var i = 0; i < lines.length; i++) { 899 | var line = lines[i].trim(); 900 | if (!line) continue; 901 | 902 | //Palette: parse into attrib=value pairs 903 | var pair = line.split("="); 904 | if (pair[0] == "Background") 905 | this.background = new Colour(pair[1]); 906 | else if (pair[0][0] == "P") //Very old format: PositionX= 907 | position = parseFloat(pair[1]); 908 | else if (pair[0][0] == "C") { //Very old format: ColourX= 909 | //Colour constructor handles html format colours, if no # or rgb/rgba assumes integer format 910 | this.colours.push(new ColourPos(pair[1], position)); 911 | //Some old palettes had extra colours at end which screws things up so check end position 912 | if (position == 1.0) break; 913 | } else if (pair.length == 2) { 914 | //New style: position=value 915 | this.colours.push(new ColourPos(pair[1], pair[0])); 916 | } else { 917 | //Interpret as colour only, calculate positions 918 | calcPositions = true; 919 | this.colours.push(new ColourPos(line)); 920 | } 921 | } 922 | } else { 923 | //JSON colour/position list data 924 | for (var j=0; j 0) opaque = true; 949 | //Fix alpha=255 950 | if (this.colours[c].colour.alpha > 1.0) 951 | this.colours[c].colour.alpha = 1.0; 952 | } 953 | if (!opaque) { 954 | for (var c = 0; c < this.colours.length; c++) 955 | this.colours[c].colour.alpha = 1.0; 956 | } 957 | } 958 | 959 | Palette.prototype.sort = function() { 960 | this.colours.sort(function(a,b){return a.position - b.position}); 961 | } 962 | 963 | Palette.prototype.newColour = function(position, colour) { 964 | var col = new ColourPos(colour, position); 965 | this.colours.push(col); 966 | this.sort(); 967 | for (var i = 1; i < this.colours.length-1; i++) 968 | if (this.colours[i].position == position) return i; 969 | return -1; 970 | } 971 | 972 | Palette.prototype.inRange = function(pos, range, length) { 973 | for (var i = 0; i < this.colours.length; i++) 974 | { 975 | var x = this.colours[i].position * length; 976 | if (pos == x || (range > 1 && pos >= x - range / 2 && pos <= x + range / 2)) 977 | return i; 978 | } 979 | return -1; 980 | } 981 | 982 | Palette.prototype.inDragRange = function(pos, range, length) { 983 | for (var i = 1; i < this.colours.length-1; i++) 984 | { 985 | var x = this.colours[i].position * length; 986 | if (pos == x || (range > 1 && pos >= x - range / 2 && pos <= x + range / 2)) 987 | return i; 988 | } 989 | return 0; 990 | } 991 | 992 | Palette.prototype.remove = function(i) { 993 | this.colours.splice(i,1); 994 | } 995 | 996 | Palette.prototype.toString = function() { 997 | var paletteData = 'Background=' + this.background.html(); 998 | for (var i = 0; i < this.colours.length; i++) 999 | paletteData += '\n' + this.colours[i].position.toFixed(6) + '=' + this.colours[i].colour.html(); 1000 | return paletteData; 1001 | } 1002 | 1003 | Palette.prototype.get = function() { 1004 | var obj = {}; 1005 | obj.background = this.background.html(); 1006 | obj.colours = []; 1007 | for (var i = 0; i < this.colours.length; i++) 1008 | obj.colours.push({'position' : this.colours[i].position, 'colour' : this.colours[i].colour.html()}); 1009 | return obj; 1010 | } 1011 | 1012 | Palette.prototype.toJSON = function() { 1013 | return JSON.stringify(this.get()); 1014 | } 1015 | 1016 | //Palette draw to canvas 1017 | Palette.prototype.draw = function(canvas, ui) { 1018 | //Slider image not yet loaded? 1019 | if (!this.slider.width && ui) { 1020 | var _this = this; 1021 | setTimeout(function() { _this.draw(canvas, ui); }, 150); 1022 | return; 1023 | } 1024 | 1025 | // Figure out if a webkit browser is being used 1026 | if (!canvas) {alert("Invalid canvas!"); return;} 1027 | var webkit = /webkit/.test(navigator.userAgent.toLowerCase()); 1028 | 1029 | if (this.colours.length == 0) { 1030 | this.background = new Colour("#ffffff"); 1031 | this.colours.push(new ColourPos("#000000", 0)); 1032 | this.colours.push(new ColourPos("#ffffff", 1)); 1033 | } 1034 | 1035 | //Colours might be out of order (especially during editing) 1036 | //so save a (shallow) copy and sort it 1037 | list = this.colours.slice(0); 1038 | list.sort(function(a,b){return a.position - b.position}); 1039 | 1040 | if (canvas.getContext) { 1041 | //Draw the gradient(s) 1042 | var width = canvas.width; 1043 | var height = canvas.height; 1044 | var context = canvas.getContext('2d'); 1045 | context.clearRect(0, 0, width, height); 1046 | 1047 | if (webkit) { 1048 | //Split up into sections or webkit draws a fucking awful gradient with banding 1049 | var x0 = 0; 1050 | for (var i = 1; i < list.length; i++) { 1051 | var x1 = Math.round(width * list[i].position); 1052 | context.fillStyle = context.createLinearGradient(x0, 0, x1, 0); 1053 | var colour1 = list[i-1].colour; 1054 | var colour2 = list[i].colour; 1055 | //Pre-blend with background unless in UI mode 1056 | if (this.premultiply && !ui) { 1057 | colour1 = this.background.blend(colour1); 1058 | colour2 = this.background.blend(colour2); 1059 | } 1060 | context.fillStyle.addColorStop(0.0, colour1.html()); 1061 | context.fillStyle.addColorStop(1.0, colour2.html()); 1062 | context.fillRect(x0, 0, x1-x0, height); 1063 | x0 = x1; 1064 | } 1065 | } else { 1066 | //Single gradient 1067 | context.fillStyle = context.createLinearGradient(0, 0, width, 0); 1068 | for (var i = 0; i < list.length; i++) { 1069 | var colour = list[i].colour; 1070 | //Pre-blend with background unless in UI mode 1071 | if (this.premultiply && !ui) 1072 | colour = this.background.blend(colour); 1073 | context.fillStyle.addColorStop(list[i].position, colour.html()); 1074 | } 1075 | context.fillRect(0, 0, width, height); 1076 | } 1077 | 1078 | /* Posterise mode (no gradients) 1079 | var x0 = 0; 1080 | for (var i = 1; i < list.length; i++) { 1081 | var x1 = Math.round(width * list[i].position); 1082 | //Pre-blend with background unless in UI mode 1083 | var colour2 = ui ? list[i].colour : this.background.blend(list[i].colour); 1084 | context.fillStyle = colour2.html(); 1085 | context.fillRect(x0, 0, x1-x0, height); 1086 | x0 = x1; 1087 | } 1088 | */ 1089 | 1090 | //Background colour 1091 | var bg = document.getElementById('backgroundCUR'); 1092 | if (bg) bg.style.background = this.background.html(); 1093 | 1094 | //User interface controls 1095 | if (!ui) return; //Skip drawing slider interface 1096 | for (var i = 1; i < list.length-1; i++) 1097 | { 1098 | var x = Math.floor(width * list[i].position) + 0.5; 1099 | var HSV = list[i].colour.HSV(); 1100 | if (HSV.V > 50) 1101 | context.strokeStyle = "black"; 1102 | else 1103 | context.strokeStyle = "white"; 1104 | context.beginPath(); 1105 | context.moveTo(x, 0); 1106 | context.lineTo(x, canvas.height); 1107 | context.closePath(); 1108 | context.stroke(); 1109 | x -= (this.slider.width / 2); 1110 | context.drawImage(this.slider, x, 0); 1111 | } 1112 | } else alert("getContext failed!"); 1113 | } 1114 | 1115 | 1116 | /** 1117 | * @constructor 1118 | */ 1119 | function ColourPos(colour, pos) { 1120 | //Stores colour as rgba and position as real [0,1] 1121 | if (pos == undefined) 1122 | this.position = 0.0; 1123 | else 1124 | this.position = parseFloat(pos); 1125 | //Detect out of range... 1126 | if (this.position >= 0 && this.position <= 1) { 1127 | if (colour) { 1128 | if (typeof(colour) == 'object') 1129 | this.colour = colour; 1130 | else 1131 | this.colour = new Colour(colour); 1132 | } else { 1133 | this.colour = new Colour("#000000"); 1134 | } 1135 | } else { 1136 | throw( "Invalid Colour Position: " + pos); 1137 | } 1138 | } 1139 | 1140 | /** 1141 | * @constructor 1142 | */ 1143 | function Colour(colour) { 1144 | //Construct... stores colour as r,g,b,a values 1145 | //Can pass in html colour string, HSV object, Colour object or integer rgba 1146 | if (typeof colour == "undefined") 1147 | this.set("#ffffff") 1148 | else if (typeof(colour) == 'string') 1149 | this.set(colour); 1150 | else if (typeof(colour) == 'object') { 1151 | //Determine passed type, Colour, RGBA or HSV 1152 | if (typeof colour.H != "undefined") 1153 | //HSV 1154 | this.setHSV(colour); 1155 | else if (typeof colour.red != "undefined") { 1156 | //Another Colour object 1157 | this.red = colour.red; 1158 | this.green = colour.green; 1159 | this.blue = colour.blue; 1160 | this.alpha = colour.alpha; 1161 | } else if (colour.R) { 1162 | //RGBA 1163 | this.red = colour.R; 1164 | this.green = colour.G; 1165 | this.blue = colour.B; 1166 | this.alpha = typeof colour.A == "undefined" ? 1.0 : colour.A; 1167 | } else { 1168 | //Assume array 1169 | this.red = colour[0]; 1170 | this.green = colour[1]; 1171 | this.blue = colour[2]; 1172 | //Convert float components to [0-255] 1173 | //NOTE: This was commented, not sure where the problem was 1174 | //Needed for parsing JSON array [0,1] colours 1175 | if (this.red <= 1.0 && this.green <= 1.0 && this.blue <= 1.0) { 1176 | this.red = Math.round(this.red * 255); 1177 | this.green = Math.round(this.green * 255); 1178 | this.blue = Math.round(this.blue * 255); 1179 | } 1180 | this.alpha = typeof colour[3] == "undefined" ? 1.0 : colour[3]; 1181 | } 1182 | } else { 1183 | //Convert from integer AABBGGRR 1184 | this.fromInt(colour); 1185 | } 1186 | } 1187 | 1188 | Colour.prototype.set = function(val) { 1189 | if (!val) val = "#ffffff"; //alert("No Value provided!"); 1190 | var re = /^rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,?\s*(\d\.?\d*)?\)$/; 1191 | var bits = re.exec(val); 1192 | if (bits) 1193 | { 1194 | this.red = parseInt(bits[1]); 1195 | this.green = parseInt(bits[2]); 1196 | this.blue = parseInt(bits[3]); 1197 | this.alpha = typeof bits[4] == "undefined" ? 1.0 : parseFloat(bits[4]); 1198 | 1199 | } else if (val.charAt(0) == "#") { 1200 | var hex = val.substring(1,7); 1201 | this.alpha = 1.0; 1202 | this.red = parseInt(hex.substring(0,2),16); 1203 | this.green = parseInt(hex.substring(2,4),16); 1204 | this.blue = parseInt(hex.substring(4,6),16); 1205 | } else { 1206 | //Attempt to parse as integer 1207 | this.fromInt(parseInt(val)); 1208 | } 1209 | } 1210 | 1211 | Colour.prototype.fromInt = function(intcolour) { 1212 | //Convert from integer AABBGGRR 1213 | this.red = (intcolour&0x000000ff); 1214 | this.green = (intcolour&0x0000ff00) >>> 8; 1215 | this.blue = (intcolour&0x00ff0000) >>> 16; 1216 | this.alpha = ((intcolour&0xff000000) >>> 24) / 255.0; 1217 | } 1218 | 1219 | Colour.prototype.toInt = function() { 1220 | //Convert to integer AABBGGRR 1221 | var result = this.red; 1222 | result += (this.green << 8); 1223 | result += (this.blue << 16); 1224 | result += (Math.round(this.alpha * 255) << 24); 1225 | return result; 1226 | } 1227 | 1228 | Colour.prototype.toString = function() {return this.html();} 1229 | 1230 | Colour.prototype.html = function() { 1231 | return "rgba(" + this.red + "," + this.green + "," + this.blue + "," + this.alpha.toFixed(2) + ")"; 1232 | } 1233 | 1234 | Colour.prototype.rgbaGL = function() { 1235 | var arr = [this.red/255.0, this.green/255.0, this.blue/255.0, this.alpha]; 1236 | return new Float32Array(arr); 1237 | } 1238 | 1239 | Colour.prototype.rgbaGLSL = function() { 1240 | var c = this.rgbaGL(); 1241 | return "rgba(" + c[0].toFixed(4) + "," + c[1].toFixed(4) + "," + c[2].toFixed(4) + "," + c[3].toFixed(4) + ")"; 1242 | } 1243 | 1244 | Colour.prototype.rgba = function() { 1245 | var rgba = [this.red/255.0, this.green/255.0, this.blue/255.0, this.alpha]; 1246 | return rgba; 1247 | } 1248 | 1249 | Colour.prototype.rgbaObj = function() { 1250 | //OK.debug('R:' + this.red + ' G:' + this.green + ' B:' + this.blue + ' A:' + this.alpha); 1251 | return({'R':this.red, 'G':this.green, 'B':this.blue, 'A':this.alpha}); 1252 | } 1253 | 1254 | Colour.prototype.print = function() { 1255 | OK.debug(this.printString(true)); 1256 | } 1257 | 1258 | Colour.prototype.printString = function(alpha) { 1259 | return 'R:' + this.red + ' G:' + this.green + ' B:' + this.blue + (alpha ? ' A:' + this.alpha : ''); 1260 | } 1261 | 1262 | Colour.prototype.HEX = function(o) { 1263 | o = Math.round(Math.min(Math.max(0,o),255)); 1264 | return("0123456789ABCDEF".charAt((o-o%16)/16)+"0123456789ABCDEF".charAt(o%16)); 1265 | } 1266 | 1267 | Colour.prototype.htmlHex = function(o) { 1268 | return("#" + this.HEX(this.red) + this.HEX(this.green) + this.HEX(this.blue)); 1269 | }; 1270 | 1271 | Colour.prototype.hex = function(o) { 1272 | //hex RGBA in expected order 1273 | return(this.HEX(this.red) + this.HEX(this.green) + this.HEX(this.blue) + this.HEX(this.alpha*255)); 1274 | }; 1275 | 1276 | Colour.prototype.hexGL = function(o) { 1277 | //RGBA for openGL (stored ABGR internally on little endian) 1278 | return(this.HEX(this.alpha*255) + this.HEX(this.blue) + this.HEX(this.green) + this.HEX(this.red)); 1279 | }; 1280 | 1281 | Colour.prototype.setHSV = function(o) 1282 | { 1283 | var R, G, A, B, C, S=o.S/100, V=o.V/100, H=o.H/360; 1284 | 1285 | if(S>0) { 1286 | if(H>=1) H=0; 1287 | 1288 | H=6*H; F=H-Math.floor(H); 1289 | A=Math.round(255*V*(1-S)); 1290 | B=Math.round(255*V*(1-(S*F))); 1291 | C=Math.round(255*V*(1-(S*(1-F)))); 1292 | V=Math.round(255*V); 1293 | 1294 | switch(Math.floor(H)) { 1295 | case 0: R=V; G=C; B=A; break; 1296 | case 1: R=B; G=V; B=A; break; 1297 | case 2: R=A; G=V; B=C; break; 1298 | case 3: R=A; G=B; B=V; break; 1299 | case 4: R=C; G=A; B=V; break; 1300 | case 5: R=V; G=A; B=B; break; 1301 | } 1302 | 1303 | this.red = R ? R : 0; 1304 | this.green = G ? G : 0; 1305 | this.blue = B ? B : 0; 1306 | } else { 1307 | this.red = (V=Math.round(V*255)); 1308 | this.green = V; 1309 | this.blue = V; 1310 | } 1311 | this.alpha = typeof o.A == "undefined" ? 1.0 : o.A; 1312 | } 1313 | 1314 | Colour.prototype.HSV = function() { 1315 | var r = ( this.red / 255.0 ); //RGB values = 0 ÷ 255 1316 | var g = ( this.green / 255.0 ); 1317 | var b = ( this.blue / 255.0 ); 1318 | 1319 | var min = Math.min( r, g, b ); //Min. value of RGB 1320 | var max = Math.max( r, g, b ); //Max. value of RGB 1321 | deltaMax = max - min; //Delta RGB value 1322 | 1323 | var v = max; 1324 | var s, h; 1325 | var deltaRed, deltaGreen, deltaBlue; 1326 | 1327 | if ( deltaMax == 0 ) //This is a gray, no chroma... 1328 | { 1329 | h = 0; //HSV results = 0 ÷ 1 1330 | s = 0; 1331 | } 1332 | else //Chromatic data... 1333 | { 1334 | s = deltaMax / max; 1335 | 1336 | deltaRed = ( ( ( max - r ) / 6 ) + ( deltaMax / 2 ) ) / deltaMax; 1337 | deltaGreen = ( ( ( max - g ) / 6 ) + ( deltaMax / 2 ) ) / deltaMax; 1338 | deltaBlue = ( ( ( max - b ) / 6 ) + ( deltaMax / 2 ) ) / deltaMax; 1339 | 1340 | if ( r == max ) h = deltaBlue - deltaGreen; 1341 | else if ( g == max ) h = ( 1 / 3 ) + deltaRed - deltaBlue; 1342 | else if ( b == max ) h = ( 2 / 3 ) + deltaGreen - deltaRed; 1343 | 1344 | if ( h < 0 ) h += 1; 1345 | if ( h > 1 ) h -= 1; 1346 | } 1347 | 1348 | return({'H':360*h, 'S':100*s, 'V':v*100}); 1349 | } 1350 | 1351 | Colour.prototype.HSVA = function() { 1352 | var hsva = this.HSV(); 1353 | hsva.A = this.alpha; 1354 | return hsva; 1355 | } 1356 | 1357 | Colour.prototype.interpolate = function(other, lambda) { 1358 | //Interpolate between this colour and another by lambda 1359 | this.red = Math.round(this.red + lambda * (other.red - this.red)); 1360 | this.green = Math.round(this.green + lambda * (other.green - this.green)); 1361 | this.blue = Math.round(this.blue + lambda * (other.blue - this.blue)); 1362 | this.alpha = Math.round(this.alpha + lambda * (other.alpha - this.alpha)); 1363 | } 1364 | 1365 | Colour.prototype.blend = function(src) { 1366 | //Blend this colour with another and return result (uses src alpha from other colour) 1367 | return new Colour([ 1368 | Math.round((1.0 - src.alpha) * this.red + src.alpha * src.red), 1369 | Math.round((1.0 - src.alpha) * this.green + src.alpha * src.green), 1370 | Math.round((1.0 - src.alpha) * this.blue + src.alpha * src.blue), 1371 | (1.0 - src.alpha) * this.alpha + src.alpha * src.alpha 1372 | ]); 1373 | } 1374 | 1375 | /* JavaScript colour picker with opacity, (c) Owen Kaluza, Public Domain 1376 | * Depends on: utils.js, colours.js 1377 | * */ 1378 | 1379 | /** 1380 | * Draggable window class * 1381 | * @constructor 1382 | */ 1383 | function MoveWindow(id) { 1384 | //Mouse processing: 1385 | if (!id) return; 1386 | this.element = $(id); 1387 | if (!this.element) {alert("No such element: " + id); return null;} 1388 | this.mouse = new Mouse(this.element, this); 1389 | this.mouse.moveUpdate = true; 1390 | this.element.mouse = this.mouse; 1391 | } 1392 | 1393 | MoveWindow.prototype.open = function(x, y) { 1394 | //Show the window 1395 | var style = this.element.style; 1396 | 1397 | if (x<0) x=0; 1398 | if (y<0) y=0; 1399 | if (x != undefined) style.left = x + "px"; 1400 | if (y != undefined) style.top = y + "px"; 1401 | style.display = 'block'; 1402 | 1403 | //Correct if outside window width/height 1404 | var w = this.element.offsetWidth, 1405 | h = this.element.offsetHeight; 1406 | if (x + w > window.innerWidth - 20) 1407 | style.left=(window.innerWidth - w - 20) + 'px'; 1408 | if (y + h > window.innerHeight - 20) 1409 | style.top=(window.innerHeight - h - 20) + 'px'; 1410 | //console.log("Open " + this.element.id + " " + style.left + "," + style.top + " : " + style.display); 1411 | } 1412 | 1413 | MoveWindow.prototype.close = function() { 1414 | this.element.style.display = 'none'; 1415 | } 1416 | 1417 | MoveWindow.prototype.move = function(e, mouse) { 1418 | //console.log("Move: " + mouse.isdown); 1419 | if (!mouse.isdown) return; 1420 | if (mouse.button > 0) return; //Process left drag only 1421 | //Drag position 1422 | var style = mouse.element.style; 1423 | style.left = parseInt(style.left) + mouse.deltaX + 'px'; 1424 | style.top = parseInt(style.top) + mouse.deltaY + 'px'; 1425 | } 1426 | 1427 | MoveWindow.prototype.down = function(e, mouse) { 1428 | //Prevents drag/selection 1429 | return false; 1430 | } 1431 | 1432 | function scale(val, range, min, max) {return clamp(max * val / range, min, max);} 1433 | function clamp(val, min, max) {return Math.max(min, Math.min(max, val));} 1434 | 1435 | /** 1436 | * @constructor 1437 | */ 1438 | function ColourPicker(savefn, abortfn) { 1439 | // Originally based on : 1440 | // DHTML Color Picker, Programming by Ulyses, ColorJack.com (Creative Commons License) 1441 | // http://www.dynamicdrive.com/dynamicindex11/colorjack/index.htm 1442 | // (Stripped down, clean class based interface no IE6 support for HTML5 browsers only) 1443 | 1444 | function createDiv(id, inner, styles) { 1445 | var div = document.createElement("div"); 1446 | div.id = id; 1447 | if (inner) div.innerHTML = inner; 1448 | if (styles) div.style.cssText = styles; 1449 | 1450 | return div; 1451 | } 1452 | 1453 | var parentElement = document.body; 1454 | //Images 1455 | var checkimg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAIElEQVQ4jWP4TwAcOHAAL2YYNWBYGEBIASEwasCwMAAALvidroqDalkAAAAASUVORK5CYII=" 1456 | var slideimg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAFCAYAAAC5Fuf5AAAAKklEQVQokWP4////fwY6gv////9n+A8F9LIQxVJaW4xiz4D5lB4WIlsMAPjER7mTpG/OAAAAAElFTkSuQmCC" 1457 | var pickimg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAALUlEQVQYlWNgQAX/kTBW8B8ZYFMIk0ARQFaIoQCbQuopIspNRPsOrpABSzgBAFHzU61KjdKlAAAAAElFTkSuQmCC"; 1458 | var svimg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAEG0lEQVQ4jQEQBO/7APz8/Pz7+/vx+/v75Pr6+tb6+vrF+Pj4tPf396H4+PiO9/f3e/X19Wfz8/NU8PDwQuvr6zLi4uIjzs7OFZmZmQoA8PDw/O/v7/Ht7e3l7Ozs2Ozs7Mjq6uq35ubmpeXl5ZLf39+A3NzcbtXV1VvMzMxLvr6+O6ioqCyEhIQfQEBAFADk5OT84eHh8uDg4Obe3t7Z3Nzcy9nZ2brV1dWq0NDQmcrKyofCwsJ2uLi4ZKqqqlSYmJhFfX19N1lZWSsnJychANPT0/zT09Pz0NDQ6c3NzdzKysrNx8fHv8DAwK+6urqfsrKyj6mpqX+cnJxvjIyMX3l5eVBeXl5EPz8/ORsbGy8Aw8PD/MHBwfS+vr7qurq63ra2ttKxsbHErKystaOjo6eampqXj4+PiYODg3lycnJrXl5eX0hISFIuLi5IEBAQPwCwsLD9r6+v9aysrOynp6fioqKi1p2dncmVlZW8jo6OroODg6F5eXmUa2trhl1dXXlLS0ttNzc3YiIiIlkNDQ1RAJ6env2bm5v2l5eX7pSUlOWPj4/aiIiIz4GBgcN5eXm3cHBwq2RkZJ5XV1eSSkpKhzk5OX0qKipzGBgYawgICGMAioqK/YeHh/eDg4PvgICA6Hp6et90dHTVbW1ty2VlZcBcXFy1UVFRqkZGRqA6OjqWLS0tjSEhIYQSEhJ9BgYGdwB2dnb+c3Nz+HFxcfJra2vrZmZm42JiYttaWlrRUlJSyUtLS79CQkK2Nzc3rS0tLaQiIiKdGBgYlQ4ODo8EBASKAGNjY/5gYGD5XV1d9FpaWu5VVVXnTk5O4UlJSdlCQkLRPDw8yTQ0NMEqKiq7IiIisxkZGa0RERGmCgoKoQMDA5wAUFBQ/k9PT/pKSkr3R0dH8kNDQ+w+Pj7mOTk54DMzM9otLS3TJycnzSAgIMgZGRnBExMTvA0NDbcHBweyAwMDrwA9PT3+PDw8+zo6Ovg2Njb0MzMz8DAwMOwqKirnJSUl4iEhId4cHBzYFxcX1BISEtAODg7KCQkJxwQEBMQBAQHBAC0tLf4rKyv9Kioq+iYmJvclJSX0ISEh8R4eHu4aGhrqFhYW5xMTE+MQEBDgDQ0N3AgICNkGBgbWBAQE0wAAANEAHh4e/h0dHf0bGxv7Ghoa+hgYGPcWFhb2FBQU8xEREfEPDw/uDAwM7AoKCuoICAjoBgYG5gMDA+MBAQHiAAAA4QARERH+EBAQ/g8PD/0NDQ38DQ0N+wsLC/kKCgr4CAgI9wcHB/YFBQX0BAQE8wICAvIBAQHwAQEB7wAAAO8AAADuAAUFBf4FBQX+BAQE/gQEBP4DAwP+AwMD/QMDA/0CAgL8AQEB/AEBAfsAAAD7AAAA+wAAAPoAAAD6AAAA+QAAAPmq2NbsCl2m4wAAAABJRU5ErkJggg==" 1459 | 1460 | var checked = 'background-image: url("' + checkimg + '");'; 1461 | var slider = 'cursor: crosshair; float: left; height: 170px; position: relative; width: 19px; padding: 0;' + checked; 1462 | var sliderControl = 'top: 0px; left: -5px; background: url("' + slideimg + '"); height: 5px; width: 29px; position: absolute; '; 1463 | var sliderBG = 'position: relative;'; 1464 | 1465 | this.element = createDiv("picker", null, "display:none; top: 58px; z-index: 20; background: #0d0d0d; color: #aaa; cursor: move; font-family: arial; font-size: 11px; padding: 7px 10px 11px 10px; position: fixed; width: 229px; border-radius: 5px; border: 1px solid #444;"); 1466 | var bg = createDiv("pickCURBG", null, checked + " float: left; width: 12px; height: 12px; margin-right: 3px;"); 1467 | bg.appendChild(createDiv("pickCUR", null, "float: left; width: 12px; height: 12px; background: #fff; margin-right: 3px;")); 1468 | this.element.appendChild(bg); 1469 | var rgb = createDiv("pickRGB", "R: 255 G: 255 B: 255", "float: left; position: relative; top: -1px;"); 1470 | rgb.onclick = "colours.picker.updateString()"; 1471 | this.element.appendChild(rgb); 1472 | this.element.appendChild(createDiv("pickCLOSE", "X", "float: right; cursor: pointer; margin: 0 8px 3px;")); 1473 | this.element.appendChild(createDiv("pickOK", "OK", "float: right; cursor: pointer; margin: 0 8px 3px;")); 1474 | var sv = createDiv("SV", null, "position: relative; cursor: crosshair; float: left; height: 170px; width: 170px; margin-right: 10px; background: url('" + svimg +"') no-repeat; background-size: 100%;"); 1475 | sv.appendChild(createDiv("SVslide", null, "background: url('" + pickimg +"'); height: 9px; width: 9px; position: absolute; cursor: crosshair")); 1476 | this.element.appendChild(sv); 1477 | var h = createDiv("H", null, slider); 1478 | h.appendChild(createDiv("Hmodel", null, sliderBG)); 1479 | h.appendChild(createDiv("Hslide", null, sliderControl)); 1480 | this.element.appendChild(h); 1481 | var o = createDiv("O", null, slider + "border: 1px solid #888; left: 9px;"); 1482 | o.appendChild(createDiv("Omodel", null, sliderBG)); 1483 | o.appendChild(createDiv("Oslide", null, sliderControl)); 1484 | this.element.appendChild(o); 1485 | parentElement.appendChild(this.element); 1486 | 1487 | /* Hover rules require appending to stylesheet */ 1488 | var css = '#pickRGB:hover {color: #FFD000;} #pickCLOSE:hover {color: #FFD000;} #pickOK:hover {color: #FFD000;}'; 1489 | var style = document.createElement('style'); 1490 | if (style.styleSheet) 1491 | style.styleSheet.cssText = css; 1492 | else 1493 | style.appendChild(document.createTextNode(css)); 1494 | document.getElementsByTagName('head')[0].appendChild(style); 1495 | 1496 | // call base class constructor 1497 | MoveWindow.call(this, "picker"); 1498 | 1499 | this.savefn = savefn; 1500 | this.abortfn = abortfn; 1501 | this.size = 170.0; //H,S & V range in pixels 1502 | this.sv = 5; //Half size of SV selector 1503 | this.oh = 2; //Half size of H & O selectors 1504 | this.picked = {H:360, S:100, V:100, A:1.0}; 1505 | this.max = {'H':360,'S':100,'V':100, 'A':1.0}; 1506 | this.colour = new Colour(); 1507 | 1508 | //Load hue strip 1509 | var i, html='', bgcol, opac; 1510 | for(i=0; i<=this.size; i++) { 1511 | bgcol = new Colour({H:Math.round((360/this.size)*i), S:100, V:100, A:1.0}); 1512 | html += "
<\/div>"; 1513 | } 1514 | $('Hmodel').innerHTML = html; 1515 | 1516 | //Load alpha strip 1517 | html=''; 1518 | for(i=0; i<=this.size; i++) { 1519 | opac=1.0-i/this.size; 1520 | html += "
<\/div>"; 1521 | } 1522 | $('Omodel').innerHTML = html; 1523 | } 1524 | 1525 | //Inherits from MoveWindow 1526 | ColourPicker.prototype = new MoveWindow; 1527 | ColourPicker.prototype.constructor = MoveWindow; 1528 | 1529 | ColourPicker.prototype.pick = function(colour, x, y) { 1530 | //Show the picker, with selected colour 1531 | this.update(colour.HSVA()); 1532 | if (this.element.style.display == 'block') return; 1533 | MoveWindow.prototype.open.call(this, x, y); 1534 | } 1535 | 1536 | ColourPicker.prototype.select = function(element, x, y) { 1537 | if (!x || !y) { 1538 | var offset = findElementPos(element); //Requires: mouse.js 1539 | x = x ? x : offset[0]+32; 1540 | y = y ? y : offset[1]+32; 1541 | } 1542 | var colour = new Colour(element.style.backgroundColor); 1543 | //Show the picker, with selected colour 1544 | this.update(colour.HSVA()); 1545 | if (this.element.style.display == 'block') return; 1546 | MoveWindow.prototype.open.call(this, x, y); 1547 | this.target = element; 1548 | } 1549 | 1550 | //Mouse event handling 1551 | ColourPicker.prototype.click = function(e, mouse) { 1552 | if (mouse.target.id == "pickCLOSE") { 1553 | if (this.abortfn) this.abortfn(); 1554 | toggle('picker'); 1555 | } else if (mouse.target.id == "pickOK") { 1556 | if (this.savefn) 1557 | this.savefn(this.picked); 1558 | 1559 | //Set element background 1560 | if (this.target) { 1561 | var colour = new Colour(this.picked); 1562 | this.target.style.backgroundColor = colour.html(); 1563 | } 1564 | 1565 | toggle('picker'); 1566 | } else if (mouse.target.id == 'SV') 1567 | this.setSV(mouse); 1568 | else if (mouse.target.id == 'Hslide' || mouse.target.className == 'hue') 1569 | this.setHue(mouse); 1570 | else if (mouse.target.id == 'Oslide' || mouse.target.className == 'opacity') 1571 | this.setOpacity(mouse); 1572 | } 1573 | 1574 | ColourPicker.prototype.move = function(e, mouse) { 1575 | //Process left drag 1576 | if (mouse.isdown && mouse.button == 0) { 1577 | if (mouse.target.id == 'picker' || mouse.target.id == 'pickCUR' || mouse.target.id == 'pickRGB') { 1578 | //Call base class function 1579 | MoveWindow.prototype.move.call(this, e, mouse); 1580 | } else if (mouse.target) { 1581 | //Drag on H/O slider acts as click 1582 | this.click(e, mouse); 1583 | } 1584 | } 1585 | } 1586 | 1587 | ColourPicker.prototype.wheel = function(e, mouse) { 1588 | this.incHue(-e.spin); 1589 | } 1590 | 1591 | ColourPicker.prototype.setSV = function(mouse) { 1592 | var X = mouse.clientx - parseInt($('SV').offsetLeft), 1593 | Y = mouse.clienty - parseInt($('SV').offsetTop); 1594 | //Saturation & brightness adjust 1595 | this.picked.S = scale(X, this.size, 0, this.max['S']); 1596 | this.picked.V = this.max['V'] - scale(Y, this.size, 0, this.max['V']); 1597 | this.update(this.picked); 1598 | } 1599 | 1600 | ColourPicker.prototype.setHue = function(mouse) { 1601 | var X = mouse.clientx - parseInt($('H').offsetLeft), 1602 | Y = mouse.clienty - parseInt($('H').offsetTop); 1603 | //Hue adjust 1604 | this.picked.H = scale(Y, this.size, 0, this.max['H']); 1605 | this.update(this.picked); 1606 | } 1607 | 1608 | ColourPicker.prototype.incHue = function(inc) { 1609 | //Hue adjust incrementally 1610 | this.picked.H += inc; 1611 | this.picked.H = clamp(this.picked.H, 0, this.max['H']); 1612 | this.update(this.picked); 1613 | } 1614 | 1615 | ColourPicker.prototype.setOpacity = function(mouse) { 1616 | var X = mouse.clientx - parseInt($('O').offsetLeft), 1617 | Y = mouse.clienty - parseInt($('O').offsetTop); 1618 | //Alpha adjust 1619 | this.picked.A = 1.0 - clamp(Y / this.size, 0, 1); 1620 | this.update(this.picked); 1621 | } 1622 | 1623 | ColourPicker.prototype.updateString = function(str) { 1624 | if (!str) str = prompt('Edit colour:', this.colour.html()); 1625 | if (!str) return; 1626 | this.colour = new Colour(str); 1627 | this.update(this.colour.HSV()); 1628 | } 1629 | 1630 | ColourPicker.prototype.update = function(HSV) { 1631 | this.picked = HSV; 1632 | this.colour = new Colour(HSV), 1633 | rgba = this.colour.rgbaObj(), 1634 | rgbaStr = this.colour.html(), 1635 | bgcol = new Colour({H:HSV.H, S:100, V:100, A:255}); 1636 | 1637 | $('pickRGB').innerHTML=this.colour.printString(); 1638 | $S('pickCUR').background=rgbaStr; 1639 | $S('pickCUR').backgroundColour=rgbaStr; 1640 | $S('SV').backgroundColor=bgcol.htmlHex(); 1641 | 1642 | //Hue adjust 1643 | $S('Hslide').top = this.size * (HSV.H/360.0) - this.oh + 'px'; 1644 | //SV adjust 1645 | $S('SVslide').top = Math.round(this.size - this.size*(HSV.V/100.0) - this.sv) + 'px'; 1646 | $S('SVslide').left = Math.round(this.size*(HSV.S/100.0) - this.sv) + 'px'; 1647 | //Alpha adjust 1648 | $S('Oslide').top = this.size * (1.0-HSV.A) - this.oh - 1 + 'px'; 1649 | }; 1650 | 1651 | 1652 | 1653 | /** 1654 | * @constructor 1655 | */ 1656 | function GradientEditor(canvas, callback, premultiply, nopicker, scrollable) { 1657 | this.canvas = canvas; 1658 | this.callback = callback; 1659 | this.premultiply = premultiply; 1660 | this.changed = true; 1661 | this.inserting = false; 1662 | this.editing = null; 1663 | this.element = null; 1664 | this.spin = 0; 1665 | this.scrollable = scrollable; 1666 | var self = this; 1667 | function saveColour(val) {self.save(val);} 1668 | function abortColour() {self.cancel();} 1669 | if (!nopicker) 1670 | this.picker = new ColourPicker(this.save.bind(this), this.cancel.bind(this)); 1671 | 1672 | //Create default palette object (enable premultiply if required) 1673 | this.palette = new Palette(null, premultiply); 1674 | //Event handling for palette 1675 | this.canvas.mouse = new Mouse(this.canvas, this); 1676 | this.canvas.oncontextmenu="return false;"; 1677 | this.canvas.oncontextmenu = function() { return false; } 1678 | 1679 | //this.update(); 1680 | } 1681 | 1682 | //Palette management 1683 | GradientEditor.prototype.read = function(source) { 1684 | //Read a new palette from source data 1685 | this.palette = new Palette(source, this.premultiply); 1686 | this.reset(); 1687 | this.update(true); 1688 | } 1689 | 1690 | GradientEditor.prototype.update = function(nocallback) { 1691 | //Redraw and flag change 1692 | this.changed = true; 1693 | this.palette.draw(this.canvas, true); 1694 | //Trigger callback if any 1695 | if (!nocallback && this.callback) this.callback(this); 1696 | } 1697 | 1698 | //Draw gradient to passed canvas if data has changed 1699 | //If no changes, return false 1700 | GradientEditor.prototype.get = function(canvas, cache) { 1701 | if (cache && !this.changed) return false; 1702 | this.changed = false; 1703 | //Update passed canvas 1704 | this.palette.draw(canvas, false); 1705 | return true; 1706 | } 1707 | 1708 | GradientEditor.prototype.insert = function(position, x, y) { 1709 | //Flag unsaved new colour 1710 | this.inserting = true; 1711 | var col = new Colour(); 1712 | this.editing = this.palette.newColour(position, col) 1713 | this.update(); 1714 | //Edit new colour 1715 | this.picker.pick(col, x, y); 1716 | } 1717 | 1718 | GradientEditor.prototype.editBackground = function(element) { 1719 | this.editing = -1; 1720 | var offset = findElementPos(element); //From mouse.js 1721 | this.element = element; 1722 | this.picker.pick(this.palette.background, offset[0]+32, offset[1]+32); 1723 | } 1724 | 1725 | GradientEditor.prototype.edit = function(val, x, y) { 1726 | if (typeof(val) == 'number') { 1727 | this.editing = val; 1728 | this.picker.pick(this.palette.colours[val].colour, x, y); 1729 | } else if (typeof(val) == 'object') { 1730 | //Edit element 1731 | this.cancel(); //Abort any current edit first 1732 | this.element = val; 1733 | var col = new Colour(val.style.backgroundColor) 1734 | var offset = findElementPos(val); //From mouse.js 1735 | this.picker.pick(col, offset[0]+32, offset[1]+32); 1736 | } 1737 | this.update(); 1738 | } 1739 | 1740 | GradientEditor.prototype.save = function(val) { 1741 | if (this.editing != null) { 1742 | if (this.editing >= 0) 1743 | //Update colour with selected 1744 | this.palette.colours[this.editing].colour.setHSV(val); 1745 | else 1746 | //Update background colour with selected 1747 | this.palette.background.setHSV(val); 1748 | } 1749 | if (this.element) { 1750 | var col = new Colour(0); 1751 | col.setHSV(val); 1752 | this.element.style.backgroundColor = col.html(); 1753 | if (this.element.onchange) this.element.onchange(); //Call change function 1754 | } 1755 | this.reset(); 1756 | this.update(); 1757 | } 1758 | 1759 | GradientEditor.prototype.cancel = function() { 1760 | //If aborting a new colour add, delete it 1761 | if (this.editing >= 0 && this.inserting) 1762 | this.palette.remove(this.editing); 1763 | this.reset(); 1764 | this.update(); 1765 | } 1766 | 1767 | GradientEditor.prototype.reset = function() { 1768 | //Reset editing data 1769 | this.inserting = false; 1770 | this.editing = null; 1771 | this.element = null; 1772 | } 1773 | 1774 | //Mouse event handling 1775 | GradientEditor.prototype.click = function(event, mouse) { 1776 | //this.changed = true; 1777 | if (event.ctrlKey) { 1778 | //Flip 1779 | for (var i = 0; i < this.palette.colours.length; i++) 1780 | this.palette.colours[i].position = 1.0 - this.palette.colours[i].position; 1781 | this.update(); 1782 | return false; 1783 | } 1784 | 1785 | //Use non-scrolling position 1786 | if (!this.scrollable) mouse.x = mouse.clientx; 1787 | 1788 | if (mouse.slider != null) 1789 | { 1790 | //Slider moved, update texture 1791 | mouse.slider = null; 1792 | this.palette.sort(); //Fix any out of order colours 1793 | this.update(); 1794 | return false; 1795 | } 1796 | var pal = this.canvas; 1797 | if (pal.getContext){ 1798 | this.cancel(); //Abort any current edit first 1799 | var context = pal.getContext('2d'); 1800 | var ypos = findElementPos(pal)[1]+30; 1801 | 1802 | //Get selected colour 1803 | //In range of a colour pos +/- 0.5*slider width? 1804 | var i = this.palette.inRange(mouse.x, this.palette.slider.width, pal.width); 1805 | if (i >= 0) { 1806 | if (event.button == 0) { 1807 | //Edit colour on left click 1808 | this.edit(i, event.clientX-128, ypos); 1809 | } else if (event.button == 2) { 1810 | //Delete on right click 1811 | this.palette.remove(i); 1812 | this.update(); 1813 | } 1814 | } else { 1815 | //Clicked elsewhere, add new colour 1816 | this.insert(mouse.x / pal.width, event.clientX-128, ypos); 1817 | } 1818 | } 1819 | return false; 1820 | } 1821 | 1822 | GradientEditor.prototype.down = function(event, mouse) { 1823 | return false; 1824 | } 1825 | 1826 | GradientEditor.prototype.move = function(event, mouse) { 1827 | if (!mouse.isdown) return true; 1828 | 1829 | //Use non-scrolling position 1830 | if (!this.scrollable) mouse.x = mouse.clientx; 1831 | 1832 | if (mouse.slider == null) { 1833 | //Colour slider dragged on? 1834 | var i = this.palette.inDragRange(mouse.x, this.palette.slider.width, this.canvas.width); 1835 | if (i>0) mouse.slider = i; 1836 | } 1837 | 1838 | if (mouse.slider == null) 1839 | mouse.isdown = false; //Abort action if not on slider 1840 | else { 1841 | if (mouse.x < 1) mouse.x = 1; 1842 | if (mouse.x > this.canvas.width-1) mouse.x = this.canvas.width-1; 1843 | //Move to adjusted position and redraw 1844 | this.palette.colours[mouse.slider].position = mouse.x / this.canvas.width; 1845 | this.update(true); 1846 | } 1847 | } 1848 | 1849 | GradientEditor.prototype.wheel = function(event, mouse) { 1850 | if (this.timer) 1851 | clearTimeout(this.timer); 1852 | else 1853 | this.canvas.style.cursor = "wait"; 1854 | this.spin += 0.01 * event.spin; 1855 | //this.cycle(0.01 * event.spin); 1856 | var this_ = this; 1857 | this.timer = setTimeout(function() {this_.cycle(this_.spin); this_.spin = 0;}, 150); 1858 | } 1859 | 1860 | GradientEditor.prototype.leave = function(event, mouse) { 1861 | } 1862 | 1863 | GradientEditor.prototype.cycle = function(inc) { 1864 | this.canvas.style.cursor = "default"; 1865 | this.timer = null; 1866 | //Shift all colours cyclically 1867 | for (var i = 1; i < this.palette.colours.length-1; i++) 1868 | { 1869 | var x = this.palette.colours[i].position; 1870 | x += inc; 1871 | if (x <= 0) x += 1.0; 1872 | if (x >= 1.0) x -= 1.0; 1873 | this.palette.colours[i].position = x; 1874 | } 1875 | this.palette.sort(); //Fix any out of order colours 1876 | this.update(); 1877 | } 1878 | 1879 | 1880 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebGL Volume Viewer 7 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | 33 | 34 | 38 | 39 | 89 | 90 | 91 | 92 | 93 | 94 | 97 | 98 |
99 |

Loading...

100 |
101 | 102 |
103 |
×
104 |

Colourmaps:

105 |
106 | 115 |
116 | 117 | 118 |
119 |
120 |
121 | 122 |
123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** @preserve 2 | * ShareVol 3 | * Lightweight WebGL volume viewer/slicer 4 | * 5 | * Copyright (c) 2014, Monash University. All rights reserved. 6 | * Author: Owen Kaluza - owen.kaluza ( at ) monash.edu 7 | * 8 | * Licensed under the GNU Lesser General Public License 9 | * https://www.gnu.org/licenses/lgpl.html 10 | * 11 | */ 12 | //TODO: colourmaps per slicer/volume not shared (global shared list of selectable maps?) 13 | var volume; 14 | var slicer; 15 | var colours; 16 | //Windows... 17 | var info, colourmaps; 18 | var state = {}; 19 | var reset; 20 | var filename; 21 | var mobile; 22 | 23 | function initPage() { 24 | window.onresize = autoResize; 25 | 26 | //Create tool windows 27 | info = new Popup("info"); 28 | info.show(); 29 | colourmaps = new Popup("colourmap", 400, 200); 30 | 31 | try { 32 | if (!window.WebGLRenderingContext) 33 | throw "No browser WebGL support"; 34 | var canvas = document.createElement('canvas'); 35 | var ctx = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 36 | if (!ctx) 37 | throw "No WebGL context available"; 38 | canvas = ctx = null; 39 | } catch (e) { 40 | $('status').innerHTML = "Sorry, ShareVol requires a WebGL capable browser!"; 41 | return; 42 | } 43 | 44 | //Yes it's user agent sniffing, but we need to attempt to detect mobile devices so we don't over-stress their gpu... 45 | mobile = (screen.width <= 760 || /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)); 46 | 47 | //Colour editing and palette management 48 | colours = new GradientEditor($('palette'), updateColourmap); 49 | 50 | //Load json data? 51 | var json = getSearchVariable("data"); 52 | //Attempt to load default.json 53 | if (!json) json = "default.json"; 54 | 55 | $('status').innerHTML = "Loading params..."; 56 | ajaxReadFile(decodeURI(json), loadData, true); 57 | } 58 | 59 | function loadStoredData(key) { 60 | if (localStorage[key]) { 61 | try { 62 | var parsed = JSON.parse(localStorage[key]); 63 | state = parsed; 64 | } catch (e) { 65 | //if erroneous data in local storage, delete 66 | //console.log("parse error: " + e.message); 67 | alert("parse error: " + e.message); 68 | localStorage[key] = null; 69 | } 70 | } 71 | } 72 | 73 | function loadData(src, fn) { 74 | var parsed = JSON.parse(src); 75 | if (parsed.volume) { 76 | //Old data format 77 | state = {} 78 | state.properties = {}; 79 | state.colourmaps = [{}]; 80 | object = {}; 81 | view = {}; 82 | state.views = [view]; 83 | state.objects = [object]; 84 | //Copy fields to their new locations 85 | //Objects 86 | object.name = "volume"; 87 | object.samples = parsed.volume.properties.samples; 88 | object.isovalue = parsed.volume.properties.isovalue; 89 | object.isowalls = parsed.volume.properties.drawWalls; 90 | object.isoalpha = parsed.volume.properties.isoalpha; 91 | object.isosmooth = parsed.volume.properties.isosmooth; 92 | object.colour = parsed.volume.properties.isocolour; 93 | object.density = parsed.volume.properties.density; 94 | object.power = parsed.volume.properties.power; 95 | if (parsed.volume.properties.usecolourmap) object.colourmap = 0; 96 | object.tricubicfilter = parsed.volume.properties.tricubicFilter; 97 | object.zmin = parsed.volume.properties.Zmin; 98 | object.zmax = parsed.volume.properties.Zmax; 99 | object.ymin = parsed.volume.properties.Ymin; 100 | object.ymax = parsed.volume.properties.Ymax; 101 | object.xmin = parsed.volume.properties.Xmin; 102 | object.xmax = parsed.volume.properties.Xmax; 103 | object.brightness = parsed.volume.properties.brightness; 104 | object.contrast = parsed.volume.properties.contrast; 105 | //The volume data sub-object 106 | object.volume = {}; 107 | object.volume.url = parsed.url; 108 | object.volume.res = parsed.res; 109 | object.volume.scale = parsed.scale; 110 | //The slicer properties 111 | object.slices = parsed.slicer; 112 | //Properties - global rendering properties 113 | state.properties.nogui = parsed.nogui; 114 | //Views - single only in old data 115 | view.axes = parsed.volume.properties.axes; 116 | view.border = parsed.volume.properties.border; 117 | view.translate = parsed.volume.translate; 118 | view.rotate = parsed.volume.rotate; 119 | view.focus = parsed.volume.focus; 120 | 121 | //Colourmap 122 | colours.read(parsed.volume.colourmap); 123 | colours.update(); 124 | state.colourmaps = [colours.palette.get()]; 125 | delete state.colourmaps[0].background; 126 | state.properties.background = colours.palette.background.html(); 127 | } else { 128 | //New format - LavaVu compatible 129 | state = parsed; 130 | } 131 | 132 | reset = state; //Store orig for reset 133 | //Storage reset? 134 | if (getSearchVariable("reset")) {localStorage.removeItem(fn); console.log("Storage cleared");} 135 | /* LOCALSTORAGE DISABLED 136 | //Load any stored presets for this file 137 | filename = fn; 138 | loadStoredData(fn); 139 | */ 140 | 141 | //Setup default props from original data... 142 | //state.objects = reset.objects; 143 | if (!state.objects[0].volume.res) state.objects[0].volume.res = [256, 256, 256]; 144 | if (!state.objects[0].volume.scale) state.objects[0].volume.scale = [1.0, 1.0, 1.0]; 145 | 146 | //Load the image 147 | loadTexture(); 148 | } 149 | 150 | function saveData() { 151 | try { 152 | localStorage[filename] = getData(); 153 | } catch(e) { 154 | //data wasn’t successfully saved due to quota exceed so throw an error 155 | console.log('LocalStorage Error: Quota exceeded? ' + e); 156 | } 157 | } 158 | 159 | function getData(compact, matrix) { 160 | if (volume) { 161 | var vdat = volume.get(matrix); 162 | var object = state.objects[0]; 163 | object.saturation = vdat.properties.saturation; 164 | object.brightness = vdat.properties.brightness; 165 | object.contrast = vdat.properties.contrast; 166 | object.zmin = vdat.properties.zmin; 167 | object.zmax = vdat.properties.zmax; 168 | object.ymin = vdat.properties.ymin; 169 | object.ymax = vdat.properties.ymax; 170 | object.xmin = vdat.properties.xmin; 171 | object.xmax = vdat.properties.xmax; 172 | //object.volume.res = parsed.res; 173 | //object.volume.scale = parsed.scale; 174 | object.samples = vdat.properties.samples; 175 | object.isovalue = vdat.properties.isovalue; 176 | object.isowalls = vdat.properties.isowalls 177 | object.isoalpha = vdat.properties.isoalpha; 178 | object.isosmooth = vdat.properties.isosmooth; 179 | object.colour = vdat.properties.colour; 180 | object.density = vdat.properties.density; 181 | object.power = vdat.properties.power; 182 | object.minclip = parsed.volume.properties.minclip;; 183 | object.maxclip = parsed.volume.properties.maxclip;; 184 | object.tricubicfilter = vdat.properties.tricubicFilter; 185 | if (vdat.properties.usecolourmap) 186 | object.colourmap = 0; 187 | else 188 | delete object.colourmap; 189 | 190 | //Views - single only in old data 191 | state.views[0].axes = vdat.properties.axes; 192 | state.views[0].border = vdat.properties.border; 193 | state.views[0].translate = vdat.translate; 194 | state.views[0].rotate = vdat.rotate; 195 | 196 | if (slicer) 197 | state.objects[0].slices = slicer.get(); 198 | 199 | //Colourmap 200 | state.colourmaps = [colours.palette.get()]; 201 | delete state.colourmaps[0].background; 202 | state.properties.background = colours.palette.background.html(); 203 | } 204 | 205 | //Return compact json string 206 | console.log(JSON.stringify(state, null, 2)); 207 | if (compact) return JSON.stringify(state); 208 | //Otherwise return indented json string 209 | return JSON.stringify(state, null, 2); 210 | } 211 | 212 | function exportData() { 213 | window.open('data:text/json;base64,' + window.btoa(getData())); 214 | } 215 | 216 | function resetFromData(src) { 217 | //Restore data from saved props 218 | if (src.objects[0].volume && volume) { 219 | volume.load(src.objects[0]); 220 | volume.draw(); 221 | } 222 | 223 | if (src.objects[0].slices && slicer) { 224 | slicer.load(src.objects[0].slices); 225 | slicer.draw(); 226 | } 227 | } 228 | 229 | function loadTexture() { 230 | $('status').innerHTML = "Loading image data... "; 231 | var image; 232 | 233 | loadImage(state.objects[0].volume.url, function () { 234 | image = new Image(); 235 | 236 | var headers = request.getAllResponseHeaders(); 237 | var match = headers.match( /^Content-Type\:\s*(.*?)$/mi ); 238 | var mimeType = match[1] || 'image/png'; 239 | var blob = new Blob([request.response], {type: mimeType} ); 240 | image.src = window.URL.createObjectURL(blob); 241 | var imageElement = document.createElement("img"); 242 | 243 | image.onload = function () { 244 | console.log("Loaded image: " + image.width + " x " + image.height); 245 | imageLoaded(image); 246 | } 247 | } 248 | ); 249 | } 250 | 251 | function imageLoaded(image) { 252 | //Create the slicer 253 | if (state.objects[0].slices) { 254 | if (mobile) state.objects[0].slices.show = false; //Start hidden on small screen 255 | slicer = new Slicer(state.objects[0], image, "linear"); 256 | } 257 | 258 | //Create the volume viewer 259 | if (state.objects[0].volume) { 260 | interactive = true; 261 | if (mobile || state.properties.interactive == false) interactive = false; 262 | volume = new Volume(state.objects[0], image, interactive); 263 | volume.slicer = slicer; //For axis position 264 | } 265 | 266 | //Volume draw on mouseup to apply changes from other controls (including slicer) 267 | document.addEventListener("mouseup", function(ev) {if (volume) volume.delayedRender(250, true);}, false); 268 | document.addEventListener("wheel", function(ev) {if (volume) volume.delayedRender(250, true);}, false); 269 | 270 | //Update colours (and draw objects) 271 | colours.read(state.colourmaps[0].colours); 272 | //Copy the global background colour 273 | colours.palette.background = new Colour(state.properties.background); 274 | colours.update(); 275 | 276 | info.hide(); //Status 277 | 278 | /*/Draw speed test 279 | frames = 0; 280 | testtime = new Date().getTime(); 281 | info.show(); 282 | volume.draw(false, true);*/ 283 | 284 | if (!state.properties.nogui) { 285 | var gui = new dat.GUI(); 286 | if (state.properties.server) 287 | gui.add({"Update" : function() {ajaxPost(state.properties.server + "/update", "data=" + encodeURIComponent(getData(true, true)));}}, 'Update'); 288 | /* LOCALSTORAGE DISABLED 289 | gui.add({"Reset" : function() {resetFromData(reset);}}, 'Reset');*/ 290 | gui.add({"Restore" : function() {resetFromData(state);}}, 'Restore'); 291 | gui.add({"Export" : function() {exportData();}}, 'Export'); 292 | //gui.add({"loadFile" : function() {document.getElementById('fileupload').click();}}, 'loadFile'). name('Load Image file'); 293 | gui.add({"ColourMaps" : function() {window.colourmaps.toggle();}}, 'ColourMaps'); 294 | 295 | var f = gui.addFolder('Views'); 296 | var ir2 = 1.0 / Math.sqrt(2.0); 297 | f.add({"XY" : function() {volume.rotate = quat4.create([0, 0, 0, 1]);}}, 'XY'); 298 | f.add({"YX" : function() {volume.rotate = quat4.create([0, 1, 0, 0]);}}, 'YX'); 299 | f.add({"XZ" : function() {volume.rotate = quat4.create([ir2, 0, 0, -ir2]);}}, 'XZ'); 300 | f.add({"ZX" : function() {volume.rotate = quat4.create([ir2, 0, 0, ir2]);}}, 'ZX'); 301 | f.add({"YZ" : function() {volume.rotate = quat4.create([0, -ir2, 0, -ir2]);}}, 'YZ'); 302 | f.add({"ZY" : function() {volume.rotate = quat4.create([0, -ir2, 0, ir2]);}}, 'ZY'); 303 | 304 | if (volume) volume.addGUI(gui); 305 | if (slicer) slicer.addGUI(gui); 306 | } 307 | 308 | //Save props on exit 309 | window.onbeforeunload = saveData; 310 | } 311 | 312 | ///////////////////////////////////////////////////////////////////////// 313 | function autoResize() { 314 | if (volume) { 315 | volume.width = 0; //volume.canvas.width = window.innerWidth; 316 | volume.height = 0; //volume.canvas.height = window.innerHeight; 317 | volume.draw(); 318 | } 319 | } 320 | 321 | function updateColourmap() { 322 | if (!colours) return; 323 | var gradient = $('gradient'); 324 | colours.palette.draw(gradient, false); 325 | 326 | if (volume && volume.webgl) { 327 | volume.webgl.updateTexture(volume.webgl.gradientTexture, gradient, volume.gl.TEXTURE1); //Use 2nd texture unit 328 | volume.applyBackground(colours.palette.background.html()); 329 | volume.draw(); 330 | } 331 | 332 | if (slicer) { 333 | slicer.updateColourmap(); 334 | slicer.draw(); 335 | } 336 | } 337 | 338 | var request, progressBar; 339 | 340 | function loadImage(imageURI, callback) 341 | { 342 | request = new XMLHttpRequest(); 343 | request.onloadstart = showProgressBar; 344 | request.onprogress = updateProgressBar; 345 | request.onload = callback; 346 | request.onloadend = hideProgressBar; 347 | request.open("GET", imageURI, true); 348 | request.responseType = 'arraybuffer'; 349 | request.send(null); 350 | } 351 | 352 | function showProgressBar() 353 | { 354 | progressBar = document.createElement("progress"); 355 | progressBar.value = 0; 356 | progressBar.max = 100; 357 | progressBar.removeAttribute("value"); 358 | document.getElementById('status').appendChild(progressBar); 359 | } 360 | 361 | function updateProgressBar(e) 362 | { 363 | if (e.lengthComputable) 364 | progressBar.value = e.loaded / e.total * 100; 365 | else 366 | progressBar.removeAttribute("value"); 367 | } 368 | 369 | function hideProgressBar() 370 | { 371 | document.getElementById('status').removeChild(progressBar); 372 | } 373 | 374 | /** 375 | * @constructor 376 | */ 377 | function Popup(id, x, y) { 378 | this.el = $(id); 379 | this.style = $S(id); 380 | if (x && y) { 381 | this.style.left = x + 'px'; 382 | this.style.top = y + 'px'; 383 | } else { 384 | this.style.left = ((window.innerWidth - this.el.offsetWidth) * 0.5) + 'px'; 385 | this.style.top = ((window.innerHeight - this.el.offsetHeight) * 0.5) + 'px'; 386 | } 387 | this.drag = false; 388 | } 389 | 390 | Popup.prototype.toggle = function() { 391 | if (this.style.visibility == 'visible') 392 | this.hide(); 393 | else 394 | this.show(); 395 | } 396 | 397 | Popup.prototype.show = function() { 398 | this.style.visibility = 'visible'; 399 | } 400 | 401 | Popup.prototype.hide = function() { 402 | this.style.visibility = 'hidden'; 403 | } 404 | 405 | -------------------------------------------------------------------------------- /src/shaders/lineShaderWEBGL.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | varying vec4 vColour; 3 | 4 | void main(void) 5 | { 6 | gl_FragColor = vColour; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/shaders/lineShaderWEBGL.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 aVertexPosition; 4 | attribute vec4 aVertexColour; 5 | 6 | uniform mat4 uMVMatrix; 7 | uniform mat4 uPMatrix; 8 | 9 | uniform vec4 uColour; 10 | uniform float uAlpha; 11 | 12 | varying vec4 vColour; 13 | 14 | void main(void) 15 | { 16 | vec4 mvPosition = uMVMatrix * vec4(aVertexPosition, 1.0); 17 | gl_Position = uPMatrix * mvPosition; 18 | vec4 colour = aVertexColour; 19 | float alpha = 1.0; 20 | if (uColour.a > 0.01) colour = uColour; 21 | if (uAlpha > 0.01) alpha = uAlpha; 22 | vColour = vec4(colour.rgb, colour.a * alpha); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/shaders/textureShaderWEBGL.frag: -------------------------------------------------------------------------------- 1 | //Texture fragment shader 2 | precision mediump float; 3 | #define rgba vec4 4 | 5 | //Palette lookup mu = [0,1] 6 | #define gradient(mu) texture2D(palette, vec2(mu, 0.0)) 7 | 8 | //Uniform data 9 | uniform sampler2D palette; 10 | uniform sampler2D texture; 11 | 12 | uniform int colourmap; 13 | uniform float bright; 14 | uniform float cont; 15 | uniform float power; 16 | 17 | uniform int axis; 18 | uniform vec3 slice; 19 | uniform ivec3 res; 20 | uniform vec2 dim; 21 | 22 | uniform ivec2 select; 23 | 24 | //Current coordinate 25 | varying vec2 vCoord; 26 | 27 | void main() 28 | { 29 | bool invert = false; 30 | vec2 coord; 31 | float z; 32 | 33 | if (int(gl_FragCoord.x) == select.x) invert = true; 34 | if (int(gl_FragCoord.y) == select.y) invert = true; 35 | 36 | if (axis==0) 37 | { 38 | //x-axis slice 39 | //slice offset coords from vCoord.x, inside coords from (slice,vCoord.y) 40 | z = vCoord.x * float(res.z); 41 | coord = vec2(clamp(slice.x, 0.0, 0.999), vCoord.y); 42 | } 43 | else if (axis==1) 44 | { 45 | //y-axis slice 46 | //slice offset coords from vCoord.y, inside coords from (vCoord.x,slice) 47 | z = vCoord.y * float(res.z); 48 | coord = vec2(vCoord.x, clamp(slice.y, 0.0, 0.999)); 49 | } 50 | else if (axis==2) 51 | { 52 | //z-axis slice 53 | //slice offset coords from slice.z, inside coords unchanged (vCoord.xy) 54 | z = slice.z * float(res.z); 55 | coord = vCoord; 56 | } 57 | 58 | //Get offsets to selected slice 59 | float xy = z/dim.x; 60 | int row = int(xy); 61 | //mod() function doesn't work properly on safari, use fract() instead 62 | //int col = int(fract(xy) * dim.x); 63 | int col = int(fract(xy) * dim.x); 64 | coord += vec2(float(col), float(row)); 65 | //Rescale to texture coords [0,1] 66 | coord /= dim; 67 | 68 | //Get texture value at coord and calculate final colour 69 | vec4 tex = texture2D(texture, coord); 70 | float lum = tex.r; //0.3 * tex.r + 0.59 * tex.g + 0.11 * tex.b; 71 | lum = pow(lum, power); 72 | vec4 pixelColor; 73 | if (colourmap == 1) 74 | { 75 | pixelColor = gradient(lum); 76 | } 77 | else 78 | { 79 | pixelColor = vec4(lum, lum, lum, 1.0); 80 | } 81 | pixelColor.rgb = ((pixelColor.rgb - 0.5) * max(cont, 0.0)) + 0.5; 82 | pixelColor.rgb += bright; 83 | if (invert) 84 | { 85 | pixelColor.rgb = vec3(1.0) - pixelColor.rgb; 86 | pixelColor.a = 1.0; 87 | } 88 | gl_FragColor = pixelColor; 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/shaders/textureShaderWEBGL.vert: -------------------------------------------------------------------------------- 1 | //A simple vertex shader for 2d image processing 2 | //Pass the vertex coords to fragment shader in vCoord 3 | precision highp float; 4 | attribute vec3 aVertexPosition; 5 | uniform mat4 uMVMatrix; 6 | varying vec2 vCoord; 7 | void main(void) { 8 | gl_Position = vec4(aVertexPosition, 1.0); 9 | //Apply translation, rotation & scaling matrix to vertices to get coords 10 | vec4 coords = uMVMatrix * vec4(aVertexPosition.xy, 0.0, 1.0); 11 | vCoord = coords.xy; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/shaders/volumeShaderWEBGL.frag: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014, Monash University. All rights reserved. 3 | * Author: Owen Kaluza - owen.kaluza ( at ) monash.edu 4 | * 5 | * Licensed under the GNU Lesser General Public License 6 | * https://www.gnu.org/licenses/lgpl.html 7 | */ 8 | precision highp float; 9 | 10 | //Defined dynamically before compile... 11 | //const vec2 slices = vec2(16.0,16.0); 12 | //const int maxSamples = 256; 13 | 14 | uniform sampler2D uVolume; 15 | uniform sampler2D uTransferFunction; 16 | 17 | uniform vec3 uBBMin; 18 | uniform vec3 uBBMax; 19 | uniform vec3 uResolution; 20 | 21 | uniform bool uEnableColour; 22 | 23 | uniform float uBrightness; 24 | uniform float uContrast; 25 | uniform float uSaturation; 26 | uniform float uPower; 27 | 28 | uniform mat4 uPMatrix; 29 | uniform mat4 uInvPMatrix; 30 | uniform mat4 uMVMatrix; 31 | uniform mat4 uNMatrix; 32 | uniform vec4 uViewport; 33 | uniform int uSamples; 34 | uniform float uDensityFactor; 35 | uniform float uIsoValue; 36 | uniform vec4 uIsoColour; 37 | uniform float uIsoSmooth; 38 | uniform int uIsoWalls; 39 | uniform int uFilter; 40 | uniform vec2 uRange; 41 | uniform vec2 uDenMinMax; 42 | 43 | //#define tex3D(pos) interpolate_tricubic_fast(pos) 44 | //#define tex3D(pos) texture3Dfrom2D(pos).x 45 | 46 | vec2 islices = vec2(1.0 / slices.x, 1.0 / slices.y); 47 | 48 | vec4 texture3Dfrom2D(vec3 pos) 49 | { 50 | //Get z slice index and position between two slices 51 | float Z = pos.z * slices.x * slices.y; 52 | int slice = int(Z); //Index of first slice 53 | 54 | //X & Y coords of sample scaled to slice size 55 | vec2 sampleOffset = pos.xy * islices; 56 | //Offsets in 2D texture of given slice indices 57 | //(add offsets to scaled position within slice to get sample positions) 58 | float A = float(slice) * islices.x; 59 | float B = float(slice+1) * islices.x; 60 | vec2 z1offset = vec2(fract(A), floor(A) / slices.y) + sampleOffset; 61 | vec2 z2offset = vec2(fract(B), floor(B) / slices.y) + sampleOffset; 62 | 63 | //Interpolate the final value by position between slices [0,1] 64 | return mix(texture2D(uVolume, z1offset), texture2D(uVolume, z2offset), fract(Z)); 65 | } 66 | 67 | float interpolate_tricubic_fast(vec3 coord); 68 | 69 | float tex3D(vec3 pos) 70 | { 71 | if (uFilter > 0) 72 | return interpolate_tricubic_fast(pos); 73 | return texture3Dfrom2D(pos).x; 74 | } 75 | 76 | // It seems WebGL has no transpose 77 | mat4 transpose(in mat4 m) 78 | { 79 | return mat4( 80 | vec4(m[0].x, m[1].x, m[2].x, m[3].x), 81 | vec4(m[0].y, m[1].y, m[2].y, m[3].y), 82 | vec4(m[0].z, m[1].z, m[2].z, m[3].z), 83 | vec4(m[0].w, m[1].w, m[2].w, m[3].w) 84 | ); 85 | } 86 | 87 | //Light moves with camera 88 | const vec3 lightPos = vec3(0.5, 0.5, 5.0); 89 | const float ambient = 0.2; 90 | const float diffuse = 0.8; 91 | const vec3 diffColour = vec3(1.0, 1.0, 1.0); //Colour of diffuse light 92 | const vec3 ambColour = vec3(0.2, 0.2, 0.2); //Colour of ambient light 93 | 94 | void lighting(in vec3 pos, in vec3 normal, inout vec3 colour) 95 | { 96 | vec4 vertPos = uMVMatrix * vec4(pos, 1.0); 97 | vec3 lightDir = normalize(lightPos - vertPos.xyz); 98 | vec3 lightWeighting = ambColour + diffColour * diffuse * clamp(abs(dot(normal, lightDir)), 0.1, 1.0); 99 | 100 | colour *= lightWeighting; 101 | } 102 | 103 | vec3 isoNormal(in vec3 pos, in vec3 shift, in float density) 104 | { 105 | vec3 shiftpos = vec3(pos.x + shift.x, pos.y + shift.y, pos.z + shift.z); 106 | vec3 shiftx = vec3(shiftpos.x, pos.y, pos.z); 107 | vec3 shifty = vec3(pos.x, shiftpos.y, pos.z); 108 | vec3 shiftz = vec3(pos.x, pos.y, shiftpos.z); 109 | 110 | //Detect bounding box hit (walls) 111 | if (uIsoWalls > 0) 112 | { 113 | if (pos.x <= uBBMin.x) return vec3(-1.0, 0.0, 0.0); 114 | if (pos.x >= uBBMax.x) return vec3(1.0, 0.0, 0.0); 115 | if (pos.y <= uBBMin.y) return vec3(0.0, -1.0, 0.0); 116 | if (pos.y >= uBBMax.y) return vec3(0.0, 1.0, 0.0); 117 | if (pos.z <= uBBMin.z) return vec3(0.0, 0.0, -1.0); 118 | if (pos.z >= uBBMax.z) return vec3(0.0, 0.0, 1.0); 119 | } 120 | 121 | //Calculate normal 122 | return vec3(density) - vec3(tex3D(shiftx), tex3D(shifty), tex3D(shiftz)); 123 | } 124 | 125 | vec2 rayIntersectBox(vec3 rayDirection, vec3 rayOrigin) 126 | { 127 | //Intersect ray with bounding box 128 | vec3 rayInvDirection = 1.0 / rayDirection; 129 | vec3 bbMinDiff = (uBBMin - rayOrigin) * rayInvDirection; 130 | vec3 bbMaxDiff = (uBBMax - rayOrigin) * rayInvDirection; 131 | vec3 imax = max(bbMaxDiff, bbMinDiff); 132 | vec3 imin = min(bbMaxDiff, bbMinDiff); 133 | float back = min(imax.x, min(imax.y, imax.z)); 134 | float front = max(max(imin.x, 0.0), max(imin.y, imin.z)); 135 | return vec2(back, front); 136 | } 137 | 138 | void main() 139 | { 140 | //Compute eye space coord from window space to get the ray direction 141 | mat4 invMVMatrix = transpose(uMVMatrix); 142 | //ObjectSpace *[MV] = EyeSpace *[P] = Clip /w = Normalised device coords ->VP-> Window 143 | //Window ->[VP^]-> NDC ->[/w]-> Clip ->[P^]-> EyeSpace ->[MV^]-> ObjectSpace 144 | vec4 ndcPos; 145 | ndcPos.xy = ((2.0 * gl_FragCoord.xy) - (2.0 * uViewport.xy)) / (uViewport.zw) - 1.0; 146 | ndcPos.z = (2.0 * gl_FragCoord.z - gl_DepthRange.near - gl_DepthRange.far) / 147 | (gl_DepthRange.far - gl_DepthRange.near); 148 | ndcPos.w = 1.0; 149 | vec4 clipPos = ndcPos / gl_FragCoord.w; 150 | //vec4 eyeSpacePos = uInvPMatrix * clipPos; 151 | vec3 rayDirection = normalize((invMVMatrix * uInvPMatrix * clipPos).xyz); 152 | 153 | //Ray origin from the camera position 154 | vec4 camPos = -vec4(uMVMatrix[3]); //4th column of modelview 155 | vec3 rayOrigin = (invMVMatrix * camPos).xyz; 156 | 157 | //Calc step 158 | float stepSize = 1.732 / float(uSamples); //diagonal of [0,1] normalised coord cube = sqrt(3) 159 | 160 | //Intersect ray with bounding box 161 | vec2 intersection = rayIntersectBox(rayDirection, rayOrigin); 162 | //Subtract small increment to avoid errors on front boundary 163 | intersection.y -= 0.000001; 164 | //Discard points outside the box (no intersection) 165 | if (intersection.x <= intersection.y) discard; 166 | 167 | vec3 rayStart = rayOrigin + rayDirection * intersection.y; 168 | vec3 rayStop = rayOrigin + rayDirection * intersection.x; 169 | 170 | vec3 step = normalize(rayStop-rayStart) * stepSize; 171 | vec3 pos = rayStart; 172 | 173 | float T = 1.0; 174 | vec3 colour = vec3(0.0); 175 | bool inside = false; 176 | vec3 shift = uIsoSmooth / uResolution; 177 | //Number of samples to take along this ray before we pass out back of volume... 178 | float travel = distance(rayStop, rayStart) / stepSize; 179 | int samples = int(ceil(travel)); 180 | float range = uRange.y - uRange.x; 181 | if (range <= 0.0) range = 1.0; 182 | //Scale isoValue 183 | float isoValue = uRange.x + uIsoValue * range; 184 | 185 | //Raymarch, front to back 186 | for (int i=0; i < maxSamples; ++i) 187 | { 188 | //Render samples until we pass out back of cube or fully opaque 189 | #ifndef IE11 190 | if (i == samples || T < 0.01) break; 191 | #else 192 | //This is slower but allows IE 11 to render, break on non-uniform condition causes it to fail 193 | if (i == uSamples) break; 194 | if (all(greaterThanEqual(pos, uBBMin)) && all(lessThanEqual(pos, uBBMax))) 195 | #endif 196 | { 197 | //Get density 198 | float density = tex3D(pos); 199 | 200 | #define ISOSURFACE 201 | #ifdef ISOSURFACE 202 | //Passed through isosurface? 203 | if (isoValue > uRange.x && ((!inside && density >= isoValue) || (inside && density < isoValue))) 204 | { 205 | inside = !inside; 206 | //Find closer to exact position by iteration 207 | //http://sizecoding.blogspot.com.au/2008/08/isosurfaces-in-glsl.html 208 | float exact; 209 | float a = intersection.y + (float(i)*stepSize); 210 | float b = a - stepSize; 211 | for (int j = 0; j < 5; j++) 212 | { 213 | exact = (b + a) * 0.5; 214 | pos = rayDirection * exact + rayOrigin; 215 | density = tex3D(pos); 216 | if (density - isoValue < 0.0) 217 | b = exact; 218 | else 219 | a = exact; 220 | } 221 | 222 | //Skip edges unless flagged to draw 223 | if (uIsoWalls > 0 || all(greaterThanEqual(pos, uBBMin)) && all(lessThanEqual(pos, uBBMax))) 224 | { 225 | vec4 value = vec4(uIsoColour.rgb, 1.0); 226 | 227 | //normal = normalize(normal); 228 | //if (length(normal) < 1.0) normal = vec3(0.0, 1.0, 0.0); 229 | vec3 normal = normalize(mat3(uNMatrix) * isoNormal(pos, shift, density)); 230 | 231 | vec3 light = value.rgb; 232 | lighting(pos, normal, light); 233 | //Front-to-back blend equation 234 | colour += T * uIsoColour.a * light; 235 | T *= (1.0 - uIsoColour.a); 236 | } 237 | } 238 | #endif 239 | 240 | if (uDensityFactor > 0.0) 241 | { 242 | //Normalise the density over provided range 243 | density = (density - uRange.x) / range; 244 | density = clamp(density, 0.0, 1.0); 245 | if (density < uDenMinMax[0] || density > uDenMinMax[1]) 246 | { 247 | //Skip to next sample... 248 | pos += step; 249 | continue; 250 | } 251 | 252 | density = pow(density, uPower); //Apply power 253 | 254 | vec4 value; 255 | if (uEnableColour) 256 | value = texture2D(uTransferFunction, vec2(density, 0.5)); 257 | else 258 | value = vec4(density); 259 | 260 | value *= uDensityFactor * stepSize; 261 | 262 | //Color 263 | colour += T * value.rgb; 264 | //Alpha 265 | T *= 1.0 - value.a; 266 | } 267 | } 268 | 269 | //Next sample... 270 | pos += step; 271 | } 272 | 273 | //Apply brightness, saturation & contrast 274 | colour += uBrightness; 275 | const vec3 LumCoeff = vec3(0.2125, 0.7154, 0.0721); 276 | vec3 AvgLumin = vec3(0.5, 0.5, 0.5); 277 | vec3 intensity = vec3(dot(colour, LumCoeff)); 278 | colour = mix(intensity, colour, uSaturation); 279 | colour = mix(AvgLumin, colour, uContrast); 280 | 281 | if (T > 0.95) discard; 282 | gl_FragColor = vec4(colour, 1.0 - T); 283 | 284 | #ifdef WRITE_DEPTH 285 | /* Write the depth !Not supported in WebGL without extension */ 286 | vec4 clip_space_pos = uPMatrix * uMVMatrix * vec4(rayStart, 1.0); 287 | float ndc_depth = clip_space_pos.z / clip_space_pos.w; 288 | float depth = (((gl_DepthRange.far - gl_DepthRange.near) * ndc_depth) + 289 | gl_DepthRange.near + gl_DepthRange.far) / 2.0; 290 | gl_FragDepth = depth; 291 | #endif 292 | } 293 | 294 | float interpolate_tricubic_fast(vec3 coord) 295 | { 296 | /* License applicable to this function: 297 | Copyright (c) 2008-2013, Danny Ruijters. All rights reserved. 298 | 299 | Redistribution and use in source and binary forms, with or without 300 | modification, are permitted provided that the following conditions are met: 301 | * Redistributions of source code must retain the above copyright 302 | notice, this list of conditions and the following disclaimer. 303 | * Redistributions in binary form must reproduce the above copyright 304 | notice, this list of conditions and the following disclaimer in the 305 | documentation and/or other materials provided with the distribution. 306 | * Neither the name of the copyright holders nor the names of its 307 | contributors may be used to endorse or promote products derived from 308 | this software without specific prior written permission. 309 | 310 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 311 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 312 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 313 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 314 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 315 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 316 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 317 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 318 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 319 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 320 | POSSIBILITY OF SUCH DAMAGE. 321 | 322 | The views and conclusions contained in the software and documentation are 323 | those of the authors and should not be interpreted as representing official 324 | policies, either expressed or implied. 325 | 326 | When using this code in a scientific project, please cite one or all of the 327 | following papers: 328 | * Daniel Ruijters and Philippe Thévenaz, 329 | GPU Prefilter for Accurate Cubic B-Spline Interpolation, 330 | The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. 331 | * Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, 332 | Efficient GPU-Based Texture Interpolation using Uniform B-Splines, 333 | Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. 334 | */ 335 | // shift the coordinate from [0,1] to [-0.5, nrOfVoxels-0.5] 336 | vec3 nrOfVoxels = uResolution; //textureSize3D(tex, 0)); 337 | vec3 coord_grid = coord * nrOfVoxels - 0.5; 338 | vec3 index = floor(coord_grid); 339 | vec3 fraction = coord_grid - index; 340 | vec3 one_frac = 1.0 - fraction; 341 | 342 | vec3 w0 = 1.0/6.0 * one_frac*one_frac*one_frac; 343 | vec3 w1 = 2.0/3.0 - 0.5 * fraction*fraction*(2.0-fraction); 344 | vec3 w2 = 2.0/3.0 - 0.5 * one_frac*one_frac*(2.0-one_frac); 345 | vec3 w3 = 1.0/6.0 * fraction*fraction*fraction; 346 | 347 | vec3 g0 = w0 + w1; 348 | vec3 g1 = w2 + w3; 349 | vec3 mult = 1.0 / nrOfVoxels; 350 | vec3 h0 = mult * ((w1 / g0) - 0.5 + index); //h0 = w1/g0 - 1, move from [-0.5, nrOfVoxels-0.5] to [0,1] 351 | vec3 h1 = mult * ((w3 / g1) + 1.5 + index); //h1 = w3/g1 + 1, move from [-0.5, nrOfVoxels-0.5] to [0,1] 352 | 353 | // fetch the eight linear interpolations 354 | // weighting and fetching is interleaved for performance and stability reasons 355 | float tex000 = texture3Dfrom2D(h0).r; 356 | float tex100 = texture3Dfrom2D(vec3(h1.x, h0.y, h0.z)).r; 357 | tex000 = mix(tex100, tex000, g0.x); //weigh along the x-direction 358 | float tex010 = texture3Dfrom2D(vec3(h0.x, h1.y, h0.z)).r; 359 | float tex110 = texture3Dfrom2D(vec3(h1.x, h1.y, h0.z)).r; 360 | tex010 = mix(tex110, tex010, g0.x); //weigh along the x-direction 361 | tex000 = mix(tex010, tex000, g0.y); //weigh along the y-direction 362 | float tex001 = texture3Dfrom2D(vec3(h0.x, h0.y, h1.z)).r; 363 | float tex101 = texture3Dfrom2D(vec3(h1.x, h0.y, h1.z)).r; 364 | tex001 = mix(tex101, tex001, g0.x); //weigh along the x-direction 365 | float tex011 = texture3Dfrom2D(vec3(h0.x, h1.y, h1.z)).r; 366 | float tex111 = texture3Dfrom2D(h1).r; 367 | tex011 = mix(tex111, tex011, g0.x); //weigh along the x-direction 368 | tex001 = mix(tex011, tex001, g0.y); //weigh along the y-direction 369 | 370 | return mix(tex001, tex000, g0.z); //weigh along the z-direction 371 | } 372 | 373 | -------------------------------------------------------------------------------- /src/shaders/volumeShaderWEBGL.vert: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | attribute vec3 aVertexPosition; 3 | void main(void) 4 | { 5 | gl_Position = vec4(aVertexPosition, 1.0); 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/slicer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ShareVol 3 | * Lightweight WebGL volume viewer/slicer 4 | * 5 | * Copyright (c) 2014, Monash University. All rights reserved. 6 | * Author: Owen Kaluza - owen.kaluza ( at ) monash.edu 7 | * 8 | * Licensed under the GNU Lesser General Public License 9 | * https://www.gnu.org/licenses/lgpl.html 10 | * 11 | */ 12 | 13 | function Slicer(props, image, filter, parentEl) { 14 | this.image = image; 15 | this.res = props.volume.res; 16 | this.dims = [props.volume.res[0] * props.volume.scale[0], 17 | props.volume.res[1] * props.volume.scale[1], 18 | props.volume.res[2] * props.volume.scale[2]]; 19 | this.slices = [0.5, 0.5, 0.5]; 20 | 21 | // Set properties 22 | this.properties = {}; 23 | this.properties.show = true; 24 | this.properties.X = Math.round(this.res[0] / 2); 25 | this.properties.Y = Math.round(this.res[1] / 2); 26 | this.properties.Z = Math.round(this.res[2] / 2); 27 | this.properties.brightness = 0.0; 28 | this.properties.contrast = 1.0; 29 | this.properties.power = 1.0; 30 | this.properties.usecolourmap = false; 31 | this.properties.layout = "xyz"; 32 | this.flipY = false; 33 | this.properties.zoom = 1.0; 34 | 35 | this.container = document.createElement("div"); 36 | this.container.style.cssText = "position: absolute; bottom: 10px; left: 10px; margin: 0px; padding: 0px; pointer-events: none;"; 37 | if (!parentEl) parentEl = document.body; 38 | parentEl.appendChild(this.container); 39 | 40 | //Load from local storage or previously loaded file 41 | if (props.slices) this.load(props.slices); 42 | 43 | this.canvas = document.createElement("canvas"); 44 | this.canvas.style.cssText = "position: absolute; bottom: 0px; margin: 0px; padding: 0px; border: none; background: rgba(0,0,0,0); pointer-events: none;"; 45 | 46 | this.doLayout(); 47 | 48 | this.canvas.mouse = new Mouse(this.canvas, this); 49 | 50 | this.webgl = new WebGL(this.canvas); 51 | this.gl = this.webgl.gl; 52 | 53 | this.filter = this.gl.NEAREST; //Nearest-neighbour (default) 54 | if (filter == "linear") this.filter = this.gl.LINEAR; 55 | 56 | //Use the default buffers 57 | this.webgl.init2dBuffers(this.gl.TEXTURE2); 58 | 59 | //Compile the shaders 60 | this.program = new WebGLProgram(this.gl, 'texture-vs', 'texture-fs'); 61 | if (this.program.errors) OK.debug(this.program.errors); 62 | this.program.setup(["aVertexPosition"], ["palette", "texture", "colourmap", "cont", "bright", "power", "slice", "dim", "res", "axis", "select"]); 63 | 64 | this.gl.clearColor(0, 0, 0, 0); 65 | this.gl.enable(this.gl.BLEND); 66 | this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); 67 | this.gl.enable(this.gl.SCISSOR_TEST); 68 | 69 | //Load the textures 70 | this.loadImage(this.image); 71 | 72 | //Hidden? 73 | if (!this.properties.show) this.toggle(); 74 | } 75 | 76 | Slicer.prototype.toggle = function() { 77 | if (this.container.style.visibility == 'hidden') 78 | this.container.style.visibility = 'visible'; 79 | else 80 | this.container.style.visibility = 'hidden'; 81 | } 82 | 83 | Slicer.prototype.addGUI = function(gui) { 84 | this.gui = gui; 85 | var that = this; 86 | //Add folder 87 | var f1 = this.gui.addFolder('Slices'); 88 | f1.add(this.properties, 'show').onFinishChange(function(l) {that.toggle();}); 89 | //["hide/show"] = function() {}; 90 | f1.add(this.properties, 'layout').onFinishChange(function(l) {that.doLayout(); that.draw();}); 91 | //f1.add(this.properties, 'X', 0, this.res[0], 1).listen(); 92 | //f1.add(this.properties, 'Y', 0, this.res[1], 1).listen(); 93 | //f1.add(this.properties, 'Z', 0, this.res[2], 1).listen(); 94 | f1.add(this.properties, 'zoom', 0.01, 4.0, 0.1).onFinishChange(function(l) {that.doLayout(); that.draw();}); 95 | 96 | f1.add(this.properties, 'brightness', -1.0, 1.0, 0.01); 97 | f1.add(this.properties, 'contrast', 0.0, 3.0, 0.01); 98 | f1.add(this.properties, 'power', 0.01, 5.0, 0.01); 99 | f1.add(this.properties, 'usecolourmap'); 100 | f1.open(); 101 | 102 | var changefn = function(value) {that.draw();}; 103 | for (var i in f1.__controllers) 104 | f1.__controllers[i].onChange(changefn); 105 | } 106 | 107 | Slicer.prototype.get = function() { 108 | var data = {}; 109 | //data.colourmap = colours.palette.toString(); 110 | data.properties = this.properties; 111 | return data; 112 | } 113 | 114 | Slicer.prototype.load = function(src) { 115 | //colours.read(data.colourmap); 116 | //colours.update(); 117 | for (var key in src.properties) 118 | this.properties[key] = src.properties[key] 119 | } 120 | 121 | Slicer.prototype.setX = function(val) {this.properties.X = val * this.res[0]; this.draw();} 122 | 123 | 124 | Slicer.prototype.setY = function(val) {this.properties.Y = val * this.res[1]; this.draw();} 125 | Slicer.prototype.setZ = function(val) {this.properties.Z = val * this.res[2]; this.draw();} 126 | 127 | Slicer.prototype.doLayout = function() { 128 | this.viewers = []; 129 | 130 | var x = 0; 131 | var y = 0; 132 | var xmax = 0; 133 | var ymax = 0; 134 | var rotate = 0; 135 | var alignTop = true; 136 | 137 | removeChildren(this.container); 138 | 139 | var that = this; 140 | var buffer = ""; 141 | var rowHeight = 0, rowWidth = 0; 142 | var addViewer = function(idx) { 143 | var mag = 1.0; 144 | if (buffer) mag = parseFloat(buffer); 145 | var v = new SliceView(that, x, y, idx, rotate, mag); 146 | that.viewers.push(v); 147 | that.container.appendChild(v.div); 148 | 149 | // x += v.viewport.width + 5; //Offset by previous width 150 | // var h = v.viewport.height + 5; 151 | // if (h > rowHeight) rowHeight = h; 152 | // if (x > xmax) xmax = x; 153 | 154 | y += v.viewport.height + 5; //Offset by previous height 155 | var w = v.viewport.width + 5; 156 | if (w > rowWidth) rowWidth = w; 157 | if (y > ymax) ymax = y; 158 | } 159 | 160 | //Process based on layout 161 | this.flipY = false; 162 | for (var i=0; i 0) this.properties.samples -= this.properties.samples % 32; 217 | f.add(this.properties, 'samples', 32, 1024, 32); 218 | f.add(this.properties, 'density', 0.0, 50.0, 1.0); 219 | f.add(this.properties, 'brightness', -1.0, 1.0, 0.05); 220 | f.add(this.properties, 'contrast', 0.0, 2.0, 0.05); 221 | f.add(this.properties, 'saturation', 0.0, 2.0, 0.05); 222 | f.add(this.properties, 'power', 0.01, 5.0, 0.05); 223 | f.add(this.properties, 'minclip', 0.0, 1.0, 0.0); 224 | f.add(this.properties, 'maxclip', 0.0, 1.0, 1.0); 225 | f.add(this.properties, 'axes'); 226 | f.add(this.properties, 'border'); 227 | f.add(this.properties, 'tricubicFilter'); 228 | f.open(); 229 | //this.gui.__folders.f.controllers[1].updateDisplay(); //Update samples display 230 | 231 | //Clip planes folder 232 | var f0 = this.gui.addFolder('Clip planes'); 233 | f0.add(this.properties, 'xmin', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setX(l);}); 234 | f0.add(this.properties, 'xmax', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setX(l);}); 235 | f0.add(this.properties, 'ymin', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setY(l);}); 236 | f0.add(this.properties, 'ymax', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setY(l);}); 237 | f0.add(this.properties, 'zmin', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setZ(l);}); 238 | f0.add(this.properties, 'zmax', 0.0, 1.0, 0.01);//.onFinishChange(function(l) {if (slicer) slicer.setZ(l);}); 239 | //f0.open(); 240 | 241 | //Isosurfaces folder 242 | var f1 = this.gui.addFolder('Isosurface'); 243 | f1.add(this.properties, 'isovalue', 0.0, 1.0, 0.01); 244 | f1.add(this.properties, 'isowalls'); 245 | f1.add(this.properties, 'isoalpha', 0.0, 1.0, 0.01); 246 | f1.add(this.properties, 'isosmooth', 0.1, 3.0, 0.1); 247 | f1.addColor(this.properties, 'colour'); 248 | //f1.open(); 249 | 250 | // Iterate over all controllers and set change function 251 | var that = this; 252 | var changefn = function(value) {that.delayedRender(250);}; //Use delayed high quality render for faster interaction 253 | for (var i in f.__controllers) 254 | f.__controllers[i].onChange(changefn); 255 | for (var i in f0.__controllers) 256 | f0.__controllers[i].onChange(changefn); 257 | for (var i in f1.__controllers) 258 | f1.__controllers[i].onChange(changefn); 259 | } 260 | 261 | Volume.prototype.load = function(src) { 262 | for (var key in src) 263 | this.properties[key] = src[key] 264 | 265 | if (src.colourmap != undefined) this.properties.usecolourmap = true; 266 | this.properties.axes = state.views[0].axes; 267 | this.properties.border = state.views[0].border; 268 | this.properties.tricubicFilter = src.tricubicfilter; 269 | 270 | if (state.views[0].translate) this.translate = state.views[0].translate; 271 | //Initial rotation (Euler angles or quaternion accepted) 272 | if (state.views[0].rotate) { 273 | if (state.views[0].rotate.length == 3) { 274 | this.rotateZ(-state.views[0].rotate[2]); 275 | this.rotateY(-state.views[0].rotate[1]); 276 | this.rotateX(-state.views[0].rotate[0]); 277 | } else if (state.views[0].rotate[3] != 0) 278 | this.rotate = quat4.create(state.views[0].rotate); 279 | } 280 | } 281 | 282 | Volume.prototype.get = function(matrix) { 283 | var data = {}; 284 | if (matrix) { 285 | //Include the modelview matrix array 286 | data.modelview = this.webgl.modelView.toArray(); 287 | } else { 288 | //Translate + rotation quaternion 289 | data.translate = this.translate; 290 | data.rotate = [this.rotate[0], this.rotate[1], this.rotate[2], this.rotate[3]]; 291 | } 292 | data.properties = this.properties; 293 | return data; 294 | } 295 | 296 | var frames = 0; 297 | var testtime; 298 | 299 | Volume.prototype.draw = function(lowquality, testmode) { 300 | if (!this.properties || !this.webgl) return; //Getting called before vars defined, TODO:fix 301 | //this.time = new Date().getTime(); 302 | if (this.width == 0 || this.height == 0) { 303 | //Get size from window 304 | this.width = window.innerWidth; 305 | this.height = window.innerHeight; 306 | } 307 | 308 | if (this.width != this.canvas.width || this.height != this.canvas.height) { 309 | //Get size from element 310 | this.canvas.width = this.width; 311 | this.canvas.height = this.height; 312 | this.canvas.setAttribute("width", this.width); 313 | this.canvas.setAttribute("height", this.height); 314 | if (this.gl) { 315 | this.gl.viewportWidth = this.width; 316 | this.gl.viewportHeight = this.height; 317 | this.webgl.viewport = new Viewport(0, 0, this.width, this.height); 318 | } 319 | } 320 | //Reset to auto-size... 321 | //this.width = this.height = 0; 322 | //console.log(this.width + "," + this.height); 323 | 324 | this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); 325 | this.gl.viewport(this.webgl.viewport.x, this.webgl.viewport.y, this.webgl.viewport.width, this.webgl.viewport.height); 326 | 327 | //box/axes draw fully opaque behind volume 328 | if (this.properties.border) this.drawBox(1.0); 329 | if (this.properties.axes) this.drawAxis(1.0); 330 | 331 | //Volume render (skip while interacting if lowpower device flag is set) 332 | if (!(lowquality && !this.properties.interactive)) { 333 | //Setup volume camera 334 | this.webgl.modelView.push(); 335 | this.rayCamera(); 336 | 337 | this.webgl.use(this.program); 338 | //this.webgl.modelView.scale(this.scaling); //Apply scaling 339 | this.gl.disableVertexAttribArray(this.program.attributes["aVertexColour"]); 340 | 341 | this.gl.activeTexture(this.gl.TEXTURE0); 342 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.webgl.textures[0]); 343 | 344 | this.gl.activeTexture(this.gl.TEXTURE1); 345 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.webgl.gradientTexture); 346 | 347 | //Only render full quality when not interacting 348 | //this.gl.uniform1i(this.program.uniforms["uSamples"], this.samples); 349 | this.gl.uniform1i(this.program.uniforms["uSamples"], lowquality ? this.properties.samples * 0.5 : this.properties.samples); 350 | this.gl.uniform1i(this.program.uniforms["uVolume"], 0); 351 | this.gl.uniform1i(this.program.uniforms["uTransferFunction"], 1); 352 | this.gl.uniform1i(this.program.uniforms["uEnableColour"], this.properties.usecolourmap); 353 | this.gl.uniform1i(this.program.uniforms["uFilter"], lowquality ? false : this.properties.tricubicFilter); 354 | this.gl.uniform4fv(this.program.uniforms["uViewport"], new Float32Array([0, 0, this.gl.viewportWidth, this.gl.viewportHeight])); 355 | 356 | var bbmin = [this.properties.xmin, this.properties.ymin, this.properties.zmin]; 357 | var bbmax = [this.properties.xmax, this.properties.ymax, this.properties.zmax]; 358 | this.gl.uniform3fv(this.program.uniforms["uBBMin"], new Float32Array(bbmin)); 359 | this.gl.uniform3fv(this.program.uniforms["uBBMax"], new Float32Array(bbmax)); 360 | this.gl.uniform3fv(this.program.uniforms["uResolution"], new Float32Array(this.resolution)); 361 | 362 | this.gl.uniform1f(this.program.uniforms["uDensityFactor"], this.properties.density); 363 | // brightness and contrast 364 | this.gl.uniform1f(this.program.uniforms["uSaturation"], this.properties.saturation); 365 | this.gl.uniform1f(this.program.uniforms["uBrightness"], this.properties.brightness); 366 | this.gl.uniform1f(this.program.uniforms["uContrast"], this.properties.contrast); 367 | this.gl.uniform1f(this.program.uniforms["uPower"], this.properties.power); 368 | 369 | this.gl.uniform1f(this.program.uniforms["uIsoValue"], this.properties.isovalue); 370 | var colour = new Colour(this.properties.colour); 371 | colour.alpha = this.properties.isoalpha; 372 | this.gl.uniform4fv(this.program.uniforms["uIsoColour"], colour.rgbaGL()); 373 | this.gl.uniform1f(this.program.uniforms["uIsoSmooth"], this.properties.isosmooth); 374 | this.gl.uniform1i(this.program.uniforms["uIsoWalls"], this.properties.isowalls); 375 | 376 | //Data value range (default only for now) 377 | this.gl.uniform2fv(this.program.uniforms["uRange"], new Float32Array([0.0, 1.0])); 378 | //Density clip range 379 | this.gl.uniform2fv(this.program.uniforms["uDenMinMax"], new Float32Array([this.properties.minclip, this.properties.maxclip])); 380 | 381 | //Draw two triangles 382 | this.webgl.initDraw2d(); //This sends the matrices, uNMatrix may not be correct here though 383 | this.gl.uniformMatrix4fv(this.program.uniforms["uInvPMatrix"], false, this.invPMatrix); 384 | //this.gl.enableVertexAttribArray(this.program.attributes["aVertexPosition"]); 385 | //this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.webgl.vertexPositionBuffer); 386 | //this.gl.vertexAttribPointer(this.program.attributes["aVertexPosition"], this.webgl.vertexPositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 387 | //this.webgl.setMatrices(); 388 | 389 | this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, this.webgl.vertexPositionBuffer.numItems); 390 | 391 | this.webgl.modelView.pop(); 392 | } else { 393 | //Always draw axis even if turned off to show interaction 394 | if (!this.properties.axes) this.drawAxis(1.0); 395 | //Bounding box 396 | this.drawBox(1.0); 397 | } 398 | 399 | //this.timeAction("Render", this.time); 400 | 401 | //Draw box/axes again as overlay (near transparent) 402 | this.gl.clear(this.gl.DEPTH_BUFFER_BIT); 403 | if (this.properties.axes) this.drawAxis(0.2); 404 | if (this.properties.border) this.drawBox(0.2); 405 | 406 | //Running speed test? 407 | if (testmode) { 408 | frames++; 409 | $('status').innerHTML = "Speed test: frame " + frames; 410 | if (frames == 5) { 411 | var elapsed = new Date().getTime() - testtime; 412 | console.log("5 frames in " + (elapsed / 1000) + " seconds"); 413 | //Reduce quality for slower device 414 | if (elapsed > 1000) { 415 | this.properties.samples = Math.floor(this.properties.samples * 1000 / elapsed); 416 | if (this.properties.samples < 32) this.properties.samples = 32; 417 | $('status').innerHTML = "5 frames in " + (elapsed / 1000) + " seconds, Reduced quality to " + this.properties.samples; 418 | //Hide info window in 2 sec 419 | setTimeout(function() {info.hide()}, 2000); 420 | } else { 421 | info.hide(); 422 | } 423 | } else { 424 | this.draw(true, true); 425 | } 426 | } 427 | } 428 | 429 | Volume.prototype.camera = function() { 430 | //Apply translation to origin, any rotation and scaling 431 | this.webgl.modelView.identity() 432 | this.webgl.modelView.translate(this.translate) 433 | // Adjust centre of rotation, default is same as focal point so this does nothing... 434 | adjust = [-(this.focus[0] - this.centre[0]), -(this.focus[1] - this.centre[1]), -(this.focus[2] - this.centre[2])]; 435 | this.webgl.modelView.translate(adjust); 436 | 437 | // rotate model 438 | var rotmat = quat4.toMat4(this.rotate); 439 | this.webgl.modelView.mult(rotmat); 440 | //this.webgl.modelView.mult(this.rotate); 441 | 442 | // Adjust back for rotation centre 443 | adjust = [this.focus[0] - this.centre[0], this.focus[1] - this.centre[1], this.focus[2] - this.centre[2]]; 444 | this.webgl.modelView.translate(adjust); 445 | 446 | // Translate back by centre of model to align eye with model centre 447 | this.webgl.modelView.translate([-this.focus[0], -this.focus[1], -this.focus[2] * this.orientation]); 448 | 449 | //Perspective matrix 450 | this.webgl.setPerspective(this.fov, this.gl.viewportWidth / this.gl.viewportHeight, 0.1, 100.0); 451 | } 452 | 453 | Volume.prototype.rayCamera = function() { 454 | //Apply translation to origin, any rotation and scaling 455 | this.webgl.modelView.identity() 456 | this.webgl.modelView.translate(this.translate) 457 | 458 | // rotate model 459 | var rotmat = quat4.toMat4(this.rotate); 460 | this.webgl.modelView.mult(rotmat); 461 | 462 | //For a volume cube other than [0,0,0] - [1,1,1], need to translate/scale here... 463 | this.webgl.modelView.translate([-this.scaling[0]*0.5, -this.scaling[1]*0.5, -this.scaling[2]*0.5]); //Translate to origin 464 | //Inverse of scaling 465 | this.webgl.modelView.scale([this.iscale[0], this.iscale[1], this.iscale[2]]); 466 | 467 | //Perspective matrix 468 | this.webgl.setPerspective(this.fov, this.gl.viewportWidth / this.gl.viewportHeight, 0.1, 100.0); 469 | 470 | //Get inverted matrix for volume shader 471 | this.invPMatrix = mat4.create(this.webgl.perspective.matrix); 472 | mat4.inverse(this.invPMatrix); 473 | } 474 | 475 | Volume.prototype.drawAxis = function(alpha) { 476 | this.camera(); 477 | this.webgl.use(this.lineprogram); 478 | this.gl.uniform1f(this.lineprogram.uniforms["uAlpha"], alpha); 479 | this.gl.uniform4fv(this.lineprogram.uniforms["uColour"], new Float32Array([1.0, 1.0, 1.0, 0.0])); 480 | 481 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.linePositionBuffer); 482 | this.gl.enableVertexAttribArray(this.lineprogram.attributes["aVertexPosition"]); 483 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexPosition"], this.linePositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 484 | 485 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.lineColourBuffer); 486 | this.gl.enableVertexAttribArray(this.lineprogram.attributes["aVertexColour"]); 487 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexColour"], this.lineColourBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 488 | 489 | //Axis position, default centre, use slicer positions if available 490 | var pos = [0.5*this.scaling[0], 0.5*this.scaling[1], 0.5*this.scaling[2]]; 491 | if (this.slicer) { 492 | pos = [this.slicer.slices[0]*this.scaling[0], 493 | this.slicer.slices[1]*this.scaling[1], 494 | this.slicer.slices[2]*this.scaling[2]]; 495 | } 496 | this.webgl.modelView.translate(pos); 497 | this.webgl.setMatrices(); 498 | this.gl.drawArrays(this.gl.LINES, 0, this.linePositionBuffer.numItems); 499 | this.webgl.modelView.translate([-pos[0], -pos[1], -pos[2]]); 500 | } 501 | 502 | Volume.prototype.drawBox = function(alpha) { 503 | this.camera(); 504 | this.webgl.use(this.lineprogram); 505 | this.gl.uniform1f(this.lineprogram.uniforms["uAlpha"], alpha); 506 | this.gl.uniform4fv(this.lineprogram.uniforms["uColour"], this.borderColour.rgbaGL()); 507 | 508 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.boxPositionBuffer); 509 | this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.boxIndexBuffer); 510 | this.gl.enableVertexAttribArray(this.lineprogram.attributes["aVertexPosition"]); 511 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexPosition"], this.boxPositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 512 | this.gl.vertexAttribPointer(this.lineprogram.attributes["aVertexColour"], 4, this.gl.UNSIGNED_BYTE, true, 0, 0); 513 | 514 | this.webgl.modelView.scale(this.scaling); //Apply scaling 515 | this.webgl.setMatrices(); 516 | this.gl.drawElements(this.gl.LINES, this.boxIndexBuffer.numItems, this.gl.UNSIGNED_SHORT, 0); 517 | } 518 | 519 | Volume.prototype.timeAction = function(action, start) { 520 | if (!window.requestAnimationFrame) return; 521 | var timer = start || new Date().getTime(); 522 | function logTime() { 523 | var elapsed = new Date().getTime() - timer; 524 | if (elapsed < 50) 525 | window.requestAnimationFrame(logTime); //Not enough time, assume triggered too early, try again 526 | else { 527 | console.log(action + " took: " + (elapsed / 1000) + " seconds"); 528 | /*if (elapsed > 200 && this.quality > 32) { 529 | this.quality = Math.floor(this.quality * 0.5); 530 | OK.debug("Reducing quality to " + this.quality + " samples"); 531 | this.draw(); 532 | } else if (elapsed < 100 && this.quality < 512 && this.quality >= 128) { 533 | this.quality = this.quality * 2; 534 | OK.debug("Increasing quality to " + this.quality + " samples"); 535 | this.draw(); 536 | }*/ 537 | } 538 | } 539 | window.requestAnimationFrame(logTime); 540 | } 541 | 542 | Volume.prototype.rotateX = function(deg) { 543 | this.rotation(deg, [1,0,0]); 544 | } 545 | 546 | Volume.prototype.rotateY = function(deg) { 547 | this.rotation(deg, [0,1,0]); 548 | } 549 | 550 | Volume.prototype.rotateZ = function(deg) { 551 | this.rotation(deg, [0,0,1]); 552 | } 553 | 554 | Volume.prototype.rotation = function(deg, axis) { 555 | //Quaterion rotate 556 | var arad = deg * Math.PI / 180.0; 557 | var rotation = quat4.fromAngleAxis(arad, axis); 558 | rotation = quat4.normalize(rotation); 559 | this.rotate = quat4.multiply(rotation, this.rotate); 560 | } 561 | 562 | Volume.prototype.zoom = function(factor) { 563 | this.translate[2] += factor * this.modelsize; 564 | } 565 | 566 | Volume.prototype.zoomClip = function(factor) { 567 | //var clip = parseFloat($("nearclip").value) - factor; 568 | //$("nearclip").value = clip; 569 | this.draw(); 570 | //OK.debug(clip + " " + $("nearclip").value); 571 | } 572 | 573 | Volume.prototype.click = function(event, mouse) { 574 | this.rotating = false; 575 | this.draw(); 576 | return false; 577 | } 578 | 579 | Volume.prototype.move = function(event, mouse) { 580 | this.rotating = false; 581 | if (!mouse.isdown) return true; 582 | 583 | //Switch buttons for translate/rotate 584 | var button = mouse.button; 585 | 586 | switch (button) 587 | { 588 | case 0: 589 | this.rotateY(mouse.deltaX/5.0); 590 | this.rotateX(mouse.deltaY/5.0); 591 | this.rotating = true; 592 | break; 593 | case 1: 594 | this.rotateZ(Math.sqrt(mouse.deltaX*mouse.deltaX + mouse.deltaY*mouse.deltaY)/5.0); 595 | this.rotating = true; 596 | break; 597 | case 2: 598 | var adjust = this.modelsize / 1000; //1/1000th of size 599 | this.translate[0] += mouse.deltaX * adjust; 600 | this.translate[1] -= mouse.deltaY * adjust; 601 | break; 602 | } 603 | 604 | this.draw(true); 605 | return false; 606 | } 607 | 608 | Volume.prototype.wheel = function(event, mouse) { 609 | if (event.shiftKey) { 610 | var factor = event.spin * 0.01; 611 | this.zoomClip(factor); 612 | } else { 613 | var factor = event.spin * 0.05; 614 | this.zoom(factor); 615 | } 616 | this.delayedRender(250); //Delayed high quality render 617 | 618 | return false; //Prevent default 619 | } 620 | 621 | Volume.prototype.pinch = function(event, mouse) { 622 | 623 | var zoom = (event.distance * 0.0001); 624 | console.log(' --> ' + zoom); 625 | this.zoom(zoom); 626 | this.delayedRender(250); //Delayed high quality render 627 | } 628 | 629 | //Delayed high quality render 630 | Volume.prototype.delayedRender = function(time, skipImm) { 631 | if (!skipImm) this.draw(true); //Draw immediately in low quality 632 | //Set timer to draw the final render 633 | if (this.delaytimer) clearTimeout(this.delaytimer); 634 | var that = this; 635 | this.delaytimer = setTimeout(function() {that.draw();}, time); 636 | } 637 | 638 | Volume.prototype.applyBackground = function(bg) { 639 | if (!bg) return; 640 | this.background = new Colour(bg); 641 | var hsv = this.background.HSV(); 642 | this.borderColour = hsv.V > 50 ? new Colour(0xff444444) : new Colour(0xffbbbbbb); 643 | 644 | //document.body.style.background = bg; 645 | 646 | //Set canvas background 647 | if (this.properties.usecolourmap) 648 | this.canvas.style.backgroundColor = bg; 649 | else 650 | this.canvas.style.backgroundColor = "black"; 651 | 652 | 653 | } 654 | --------------------------------------------------------------------------------