├── .gitignore ├── LICENSE.md ├── README.md ├── colormaps ├── cool-warm-paraview.png ├── grayscale.png ├── matplotlib-plasma.png ├── matplotlib-virdis.png ├── rainbow.png ├── samsel-linear-green.png └── samsel-linear-ygb-1211g.png ├── embed_example.html ├── index.html └── js ├── gl-matrix-min.js ├── neuron.js ├── shader-srcs.js ├── swc.js ├── tiff.js ├── tiff.raw.js ├── tiff.raw.wasm └── webgl-util.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.raw 3 | *.swc 4 | 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Will Usher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL Neuron Viewer 2 | 3 | A neuron visualization system in WebGL, [try it out online!](https://www.willusher.io/webgl-neuron/) 4 | The volume data 5 | is rendered using my [WebGL volume raycaster](https://github.com/Twinklebear/webgl-volume-raycaster/). 6 | Neuron traces can be uploaded in the [SWC](http://research.mssm.edu/cnic/swc.html) file format. 7 | It can also load single-channel 8 or 16bit unsigned int TIFF images, if you want to try 8 | it on your own data. 9 | 10 | Uses [webgl-util](https://github.com/Twinklebear/webgl-util) for some WebGL 11 | utilities, [glMatrix](http://glmatrix.net/) for matrix/vector operations, 12 | and my fork of [tiff.js](https://github.com/Twinklebear/tiff.js) to load TIFF images. 13 | 14 | ## Images 15 | 16 | Displaying the [DIADEM NC Layer 1 Axons](http://diademchallenge.org/neocortical_layer_1_axons_readme.html) 17 | dataset and the provided reference traces, courtesy De Paola et al. 2006, 18 | from the [DIADEM Challenge](http://diademchallenge.org/). 19 | 20 | ![DIADEM NC Layer 1](https://i.imgur.com/9vVRCLE.png) 21 | 22 | The Marmoset neurons are the 16bit TIFF stack version of the dataset available 23 | on [OpenScivisDatasets](http://sci.utah.edu/~klacansky/cdn/open-scivis-datasets/marmoset_neurons/), 24 | courtesy of Fred Federer and Alessandra Angelucci. 25 | 26 | ![Marmoset](https://i.imgur.com/lwlbLCw.png) 27 | 28 | -------------------------------------------------------------------------------- /colormaps/cool-warm-paraview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/colormaps/cool-warm-paraview.png -------------------------------------------------------------------------------- /colormaps/grayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/colormaps/grayscale.png -------------------------------------------------------------------------------- /colormaps/matplotlib-plasma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/colormaps/matplotlib-plasma.png -------------------------------------------------------------------------------- /colormaps/matplotlib-virdis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/colormaps/matplotlib-virdis.png -------------------------------------------------------------------------------- /colormaps/rainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/colormaps/rainbow.png -------------------------------------------------------------------------------- /colormaps/samsel-linear-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/colormaps/samsel-linear-green.png -------------------------------------------------------------------------------- /colormaps/samsel-linear-ygb-1211g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/colormaps/samsel-linear-ygb-1211g.png -------------------------------------------------------------------------------- /embed_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | WebGL Neuron Viewer Embedding Example 12 | 13 | 14 |
15 |
16 |
17 |

WebGL Neuron Viewer Embedding Example

18 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | WebGL Neuron Viewer 12 | 13 | 14 |
15 |
16 |
17 |

WebGL Neuron Viewer

18 | 19 |
20 |
21 |
22 |

Volume:

23 |
24 |
25 | Colormap: 26 |
27 |
28 | 30 |
31 | 32 | 34 | 35 |
36 | 37 | 39 |
40 | 41 |
42 | 43 | 45 |
46 | 47 |
48 | Voxel Spacing 49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 |
61 | 62 |
63 |

Controls

64 |

Desktop: Left-click + drag to rotate, scroll to zoom, 65 | right-click + drag to pan. 66 |
67 | Touch: One finger drag to rotate, pinch to zoom, two finger drag to pan. 68 |

69 | 70 |

Description

71 |

72 | This is a WebGL viewer for Connectomics 73 | data, and can render both the microscopy acquired image volume in combination with a set 74 | of neuron traces. The DIADEM NC Layer 1 Axons and reference traces are the 75 | NC Layer 1 76 | Axons dataset, courtesy De Paola et al. 2006, included as part of the 77 | DIADEM Challenge. 78 | 79 | Get the code on GitHub! 80 |

81 | 82 |

83 | You can upload a TIFF stack or multi-page TIFF from your computer, or fetch a multi-page TIFF 84 | from a remote URL (e.g., Dropbox, Google Drive). When fetching a TIFF from a URL the remote 85 | site must support 86 | Cross-Origin Resource Sharing for this site to fetch the data. If fetching from a URL fails, 87 | you can download the file from the URL yourself and upload it. 88 | Supported TIFF Formats are single-channel 8 and 16bit unsigned int images. 89 | 16bit images may appear pixelated, as WebGL2 does not support filtering on these textures. 90 |

91 |
92 | 93 |
94 |
95 | 96 |
97 |
99 |
100 |
101 | 102 | 103 | 119 |
120 |

Traces

121 |
122 |
123 | 124 | 126 |
127 |
128 | 129 | 131 |
132 |
133 | 134 | 136 |
137 |
138 |
139 | 140 | 141 |
142 |
143 |
144 |
145 |
146 | Name 147 |
148 |
149 | # Soma 150 |
151 |
152 | # Branches 153 |
154 |
155 | # Points 156 |
157 |
158 |
159 |
160 |
161 |
162 | 164 |
165 |
166 |
167 |
168 |
169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /js/gl-matrix-min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview gl-matrix - High performance matrix and vector operations 3 | * @author Brandon Jones 4 | * @author Colin MacKenzie IV 5 | * @version 2.4.0 6 | */ 7 | 8 | /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. */ 27 | 28 | !function(t,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var r=n();for(var a in r)("object"==typeof exports?exports:t)[a]=r[a]}}("undefined"!=typeof self?self:this,function(){return function(t){function n(a){if(r[a])return r[a].exports;var e=r[a]={i:a,l:!1,exports:{}};return t[a].call(e.exports,e,e.exports,n),e.l=!0,e.exports}var r={};return n.m=t,n.c=r,n.d=function(t,r,a){n.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:a})},n.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(r,"a",r),r},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=4)}([function(t,n,r){"use strict";function a(t){n.ARRAY_TYPE=i=t}function e(t){return t*s}function u(t,n){return Math.abs(t-n)<=o*Math.max(1,Math.abs(t),Math.abs(n))}Object.defineProperty(n,"__esModule",{value:!0}),n.setMatrixArrayType=a,n.toRadian=e,n.equals=u;var o=n.EPSILON=1e-6,i=n.ARRAY_TYPE="undefined"!=typeof Float32Array?Float32Array:Array,s=(n.RANDOM=Math.random,Math.PI/180)},function(t,n,r){"use strict";function a(){var t=new g.ARRAY_TYPE(9);return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function e(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[4],t[4]=n[5],t[5]=n[6],t[6]=n[8],t[7]=n[9],t[8]=n[10],t}function u(t){var n=new g.ARRAY_TYPE(9);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n[6]=t[6],n[7]=t[7],n[8]=t[8],n}function o(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t}function i(t,n,r,a,e,u,o,i,s){var c=new g.ARRAY_TYPE(9);return c[0]=t,c[1]=n,c[2]=r,c[3]=a,c[4]=e,c[5]=u,c[6]=o,c[7]=i,c[8]=s,c}function s(t,n,r,a,e,u,o,i,s,c){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t[4]=u,t[5]=o,t[6]=i,t[7]=s,t[8]=c,t}function c(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function f(t,n){if(t===n){var r=n[1],a=n[2],e=n[5];t[1]=n[3],t[2]=n[6],t[3]=r,t[5]=n[7],t[6]=a,t[7]=e}else t[0]=n[0],t[1]=n[3],t[2]=n[6],t[3]=n[1],t[4]=n[4],t[5]=n[7],t[6]=n[2],t[7]=n[5],t[8]=n[8];return t}function M(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=f*o-i*c,h=-f*u+i*s,l=c*u-o*s,v=r*M+a*h+e*l;return v?(v=1/v,t[0]=M*v,t[1]=(-f*a+e*c)*v,t[2]=(i*a-e*o)*v,t[3]=h*v,t[4]=(f*r-e*s)*v,t[5]=(-i*r+e*u)*v,t[6]=l*v,t[7]=(-c*r+a*s)*v,t[8]=(o*r-a*u)*v,t):null}function h(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8];return t[0]=o*f-i*c,t[1]=e*c-a*f,t[2]=a*i-e*o,t[3]=i*s-u*f,t[4]=r*f-e*s,t[5]=e*u-r*i,t[6]=u*c-o*s,t[7]=a*s-r*c,t[8]=r*o-a*u,t}function l(t){var n=t[0],r=t[1],a=t[2],e=t[3],u=t[4],o=t[5],i=t[6],s=t[7],c=t[8];return n*(c*u-o*s)+r*(-c*e+o*i)+a*(s*e-u*i)}function v(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=r[0],l=r[1],v=r[2],d=r[3],b=r[4],m=r[5],p=r[6],P=r[7],E=r[8];return t[0]=h*a+l*o+v*c,t[1]=h*e+l*i+v*f,t[2]=h*u+l*s+v*M,t[3]=d*a+b*o+m*c,t[4]=d*e+b*i+m*f,t[5]=d*u+b*s+m*M,t[6]=p*a+P*o+E*c,t[7]=p*e+P*i+E*f,t[8]=p*u+P*s+E*M,t}function d(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=r[0],l=r[1];return t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=i,t[5]=s,t[6]=h*a+l*o+c,t[7]=h*e+l*i+f,t[8]=h*u+l*s+M,t}function b(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],s=n[5],c=n[6],f=n[7],M=n[8],h=Math.sin(r),l=Math.cos(r);return t[0]=l*a+h*o,t[1]=l*e+h*i,t[2]=l*u+h*s,t[3]=l*o-h*a,t[4]=l*i-h*e,t[5]=l*s-h*u,t[6]=c,t[7]=f,t[8]=M,t}function m(t,n,r){var a=r[0],e=r[1];return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=e*n[3],t[4]=e*n[4],t[5]=e*n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t}function p(t,n){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=n[0],t[7]=n[1],t[8]=1,t}function P(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=0,t[3]=-r,t[4]=a,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function E(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=0,t[4]=n[1],t[5]=0,t[6]=0,t[7]=0,t[8]=1,t}function O(t,n){return t[0]=n[0],t[1]=n[1],t[2]=0,t[3]=n[2],t[4]=n[3],t[5]=0,t[6]=n[4],t[7]=n[5],t[8]=1,t}function x(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,s=e+e,c=r*o,f=a*o,M=a*i,h=e*o,l=e*i,v=e*s,d=u*o,b=u*i,m=u*s;return t[0]=1-M-v,t[3]=f-m,t[6]=h+b,t[1]=f+m,t[4]=1-c-v,t[7]=l-d,t[2]=h-b,t[5]=l+d,t[8]=1-c-M,t}function A(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],s=n[6],c=n[7],f=n[8],M=n[9],h=n[10],l=n[11],v=n[12],d=n[13],b=n[14],m=n[15],p=r*i-a*o,P=r*s-e*o,E=r*c-u*o,O=a*s-e*i,x=a*c-u*i,A=e*c-u*s,q=f*d-M*v,y=f*b-h*v,w=f*m-l*v,R=M*b-h*d,L=M*m-l*d,S=h*m-l*b,_=p*S-P*L+E*R+O*w-x*y+A*q;return _?(_=1/_,t[0]=(i*S-s*L+c*R)*_,t[1]=(s*w-o*S-c*y)*_,t[2]=(o*L-i*w+c*q)*_,t[3]=(e*L-a*S-u*R)*_,t[4]=(r*S-e*w+u*y)*_,t[5]=(a*w-r*L-u*q)*_,t[6]=(d*A-b*x+m*O)*_,t[7]=(b*E-v*A-m*P)*_,t[8]=(v*x-d*E+m*p)*_,t):null}function q(t,n,r){return t[0]=2/n,t[1]=0,t[2]=0,t[3]=0,t[4]=-2/r,t[5]=0,t[6]=-1,t[7]=1,t[8]=1,t}function y(t){return"mat3("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+")"}function w(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2))}function R(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t[4]=n[4]+r[4],t[5]=n[5]+r[5],t[6]=n[6]+r[6],t[7]=n[7]+r[7],t[8]=n[8]+r[8],t}function L(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t[4]=n[4]-r[4],t[5]=n[5]-r[5],t[6]=n[6]-r[6],t[7]=n[7]-r[7],t[8]=n[8]-r[8],t}function S(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t[4]=n[4]*r,t[5]=n[5]*r,t[6]=n[6]*r,t[7]=n[7]*r,t[8]=n[8]*r,t}function _(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t[4]=n[4]+r[4]*a,t[5]=n[5]+r[5]*a,t[6]=n[6]+r[6]*a,t[7]=n[7]+r[7]*a,t[8]=n[8]+r[8]*a,t}function I(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]&&t[6]===n[6]&&t[7]===n[7]&&t[8]===n[8]}function N(t,n){var r=t[0],a=t[1],e=t[2],u=t[3],o=t[4],i=t[5],s=t[6],c=t[7],f=t[8],M=n[0],h=n[1],l=n[2],v=n[3],d=n[4],b=n[5],m=n[6],p=n[7],P=n[8];return Math.abs(r-M)<=g.EPSILON*Math.max(1,Math.abs(r),Math.abs(M))&&Math.abs(a-h)<=g.EPSILON*Math.max(1,Math.abs(a),Math.abs(h))&&Math.abs(e-l)<=g.EPSILON*Math.max(1,Math.abs(e),Math.abs(l))&&Math.abs(u-v)<=g.EPSILON*Math.max(1,Math.abs(u),Math.abs(v))&&Math.abs(o-d)<=g.EPSILON*Math.max(1,Math.abs(o),Math.abs(d))&&Math.abs(i-b)<=g.EPSILON*Math.max(1,Math.abs(i),Math.abs(b))&&Math.abs(s-m)<=g.EPSILON*Math.max(1,Math.abs(s),Math.abs(m))&&Math.abs(c-p)<=g.EPSILON*Math.max(1,Math.abs(c),Math.abs(p))&&Math.abs(f-P)<=g.EPSILON*Math.max(1,Math.abs(f),Math.abs(P))}Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=a,n.fromMat4=e,n.clone=u,n.copy=o,n.fromValues=i,n.set=s,n.identity=c,n.transpose=f,n.invert=M,n.adjoint=h,n.determinant=l,n.multiply=v,n.translate=d,n.rotate=b,n.scale=m,n.fromTranslation=p,n.fromRotation=P,n.fromScaling=E,n.fromMat2d=O,n.fromQuat=x,n.normalFromMat4=A,n.projection=q,n.str=y,n.frob=w,n.add=R,n.subtract=L,n.multiplyScalar=S,n.multiplyScalarAndAdd=_,n.exactEquals=I,n.equals=N;var Y=r(0),g=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(Y);n.mul=v,n.sub=L},function(t,n,r){"use strict";function a(){var t=new Z.ARRAY_TYPE(3);return t[0]=0,t[1]=0,t[2]=0,t}function e(t){var n=new Z.ARRAY_TYPE(3);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n}function u(t){var n=t[0],r=t[1],a=t[2];return Math.sqrt(n*n+r*r+a*a)}function o(t,n,r){var a=new Z.ARRAY_TYPE(3);return a[0]=t,a[1]=n,a[2]=r,a}function i(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t}function s(t,n,r,a){return t[0]=n,t[1]=r,t[2]=a,t}function c(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t}function f(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t}function M(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t[2]=n[2]*r[2],t}function h(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t[2]=n[2]/r[2],t}function l(t,n){return t[0]=Math.ceil(n[0]),t[1]=Math.ceil(n[1]),t[2]=Math.ceil(n[2]),t}function v(t,n){return t[0]=Math.floor(n[0]),t[1]=Math.floor(n[1]),t[2]=Math.floor(n[2]),t}function d(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t[2]=Math.min(n[2],r[2]),t}function b(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t[2]=Math.max(n[2],r[2]),t}function m(t,n){return t[0]=Math.round(n[0]),t[1]=Math.round(n[1]),t[2]=Math.round(n[2]),t}function p(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t}function P(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t}function E(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return Math.sqrt(r*r+a*a+e*e)}function O(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return r*r+a*a+e*e}function x(t){var n=t[0],r=t[1],a=t[2];return n*n+r*r+a*a}function A(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t}function q(t,n){return t[0]=1/n[0],t[1]=1/n[1],t[2]=1/n[2],t}function y(t,n){var r=n[0],a=n[1],e=n[2],u=r*r+a*a+e*e;return u>0&&(u=1/Math.sqrt(u),t[0]=n[0]*u,t[1]=n[1]*u,t[2]=n[2]*u),t}function w(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function R(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],s=r[2];return t[0]=e*s-u*i,t[1]=u*o-a*s,t[2]=a*i-e*o,t}function L(t,n,r,a){var e=n[0],u=n[1],o=n[2];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t[2]=o+a*(r[2]-o),t}function S(t,n,r,a,e,u){var o=u*u,i=o*(2*u-3)+1,s=o*(u-2)+u,c=o*(u-1),f=o*(3-2*u);return t[0]=n[0]*i+r[0]*s+a[0]*c+e[0]*f,t[1]=n[1]*i+r[1]*s+a[1]*c+e[1]*f,t[2]=n[2]*i+r[2]*s+a[2]*c+e[2]*f,t}function _(t,n,r,a,e,u){var o=1-u,i=o*o,s=u*u,c=i*o,f=3*u*i,M=3*s*o,h=s*u;return t[0]=n[0]*c+r[0]*f+a[0]*M+e[0]*h,t[1]=n[1]*c+r[1]*f+a[1]*M+e[1]*h,t[2]=n[2]*c+r[2]*f+a[2]*M+e[2]*h,t}function I(t,n){n=n||1;var r=2*Z.RANDOM()*Math.PI,a=2*Z.RANDOM()-1,e=Math.sqrt(1-a*a)*n;return t[0]=Math.cos(r)*e,t[1]=Math.sin(r)*e,t[2]=a*n,t}function N(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[3]*a+r[7]*e+r[11]*u+r[15];return o=o||1,t[0]=(r[0]*a+r[4]*e+r[8]*u+r[12])/o,t[1]=(r[1]*a+r[5]*e+r[9]*u+r[13])/o,t[2]=(r[2]*a+r[6]*e+r[10]*u+r[14])/o,t}function Y(t,n,r){var a=n[0],e=n[1],u=n[2];return t[0]=a*r[0]+e*r[3]+u*r[6],t[1]=a*r[1]+e*r[4]+u*r[7],t[2]=a*r[2]+e*r[5]+u*r[8],t}function g(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],s=r[2],c=r[3],f=c*a+i*u-s*e,M=c*e+s*a-o*u,h=c*u+o*e-i*a,l=-o*a-i*e-s*u;return t[0]=f*c+l*-o+M*-s-h*-i,t[1]=M*c+l*-i+h*-o-f*-s,t[2]=h*c+l*-s+f*-i-M*-o,t}function T(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[0],u[1]=e[1]*Math.cos(a)-e[2]*Math.sin(a),u[2]=e[1]*Math.sin(a)+e[2]*Math.cos(a),t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t}function j(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[2]*Math.sin(a)+e[0]*Math.cos(a),u[1]=e[1],u[2]=e[2]*Math.cos(a)-e[0]*Math.sin(a),t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t}function D(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[0]*Math.cos(a)-e[1]*Math.sin(a),u[1]=e[0]*Math.sin(a)+e[1]*Math.cos(a),u[2]=e[2],t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t}function V(t,n){var r=o(t[0],t[1],t[2]),a=o(n[0],n[1],n[2]);y(r,r),y(a,a);var e=w(r,a);return e>1?0:e<-1?Math.PI:Math.acos(e)}function z(t){return"vec3("+t[0]+", "+t[1]+", "+t[2]+")"}function F(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]}function Q(t,n){var r=t[0],a=t[1],e=t[2],u=n[0],o=n[1],i=n[2];return Math.abs(r-u)<=Z.EPSILON*Math.max(1,Math.abs(r),Math.abs(u))&&Math.abs(a-o)<=Z.EPSILON*Math.max(1,Math.abs(a),Math.abs(o))&&Math.abs(e-i)<=Z.EPSILON*Math.max(1,Math.abs(e),Math.abs(i))}Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.len=n.sqrDist=n.dist=n.div=n.mul=n.sub=void 0,n.create=a,n.clone=e,n.length=u,n.fromValues=o,n.copy=i,n.set=s,n.add=c,n.subtract=f,n.multiply=M,n.divide=h,n.ceil=l,n.floor=v,n.min=d,n.max=b,n.round=m,n.scale=p,n.scaleAndAdd=P,n.distance=E,n.squaredDistance=O,n.squaredLength=x,n.negate=A,n.inverse=q,n.normalize=y,n.dot=w,n.cross=R,n.lerp=L,n.hermite=S,n.bezier=_,n.random=I,n.transformMat4=N,n.transformMat3=Y,n.transformQuat=g,n.rotateX=T,n.rotateY=j,n.rotateZ=D,n.angle=V,n.str=z,n.exactEquals=F,n.equals=Q;var X=r(0),Z=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(X);n.sub=f,n.mul=M,n.div=h,n.dist=E,n.sqrDist=O,n.len=u,n.sqrLen=x,n.forEach=function(){var t=a();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=3),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i0&&(o=1/Math.sqrt(o),t[0]=r*o,t[1]=a*o,t[2]=e*o,t[3]=u*o),t}function w(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]+t[3]*n[3]}function R(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t[2]=o+a*(r[2]-o),t[3]=i+a*(r[3]-i),t}function L(t,n){return n=n||1,t[0]=T.RANDOM(),t[1]=T.RANDOM(),t[2]=T.RANDOM(),t[3]=T.RANDOM(),y(t,t),m(t,t,n),t}function S(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3];return t[0]=r[0]*a+r[4]*e+r[8]*u+r[12]*o,t[1]=r[1]*a+r[5]*e+r[9]*u+r[13]*o,t[2]=r[2]*a+r[6]*e+r[10]*u+r[14]*o,t[3]=r[3]*a+r[7]*e+r[11]*u+r[15]*o,t}function _(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],s=r[2],c=r[3],f=c*a+i*u-s*e,M=c*e+s*a-o*u,h=c*u+o*e-i*a,l=-o*a-i*e-s*u;return t[0]=f*c+l*-o+M*-s-h*-i,t[1]=M*c+l*-i+h*-o-f*-s,t[2]=h*c+l*-s+f*-i-M*-o,t[3]=n[3],t}function I(t){return"vec4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"}function N(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]}function Y(t,n){var r=t[0],a=t[1],e=t[2],u=t[3],o=n[0],i=n[1],s=n[2],c=n[3];return Math.abs(r-o)<=T.EPSILON*Math.max(1,Math.abs(r),Math.abs(o))&&Math.abs(a-i)<=T.EPSILON*Math.max(1,Math.abs(a),Math.abs(i))&&Math.abs(e-s)<=T.EPSILON*Math.max(1,Math.abs(e),Math.abs(s))&&Math.abs(u-c)<=T.EPSILON*Math.max(1,Math.abs(u),Math.abs(c))}Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.len=n.sqrDist=n.dist=n.div=n.mul=n.sub=void 0,n.create=a,n.clone=e,n.fromValues=u,n.copy=o,n.set=i,n.add=s,n.subtract=c,n.multiply=f,n.divide=M,n.ceil=h,n.floor=l,n.min=v,n.max=d,n.round=b,n.scale=m,n.scaleAndAdd=p,n.distance=P,n.squaredDistance=E,n.length=O,n.squaredLength=x,n.negate=A,n.inverse=q,n.normalize=y,n.dot=w,n.lerp=R,n.random=L,n.transformMat4=S,n.transformQuat=_,n.str=I,n.exactEquals=N,n.equals=Y;var g=r(0),T=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(g);n.sub=c,n.mul=f,n.div=M,n.dist=P,n.sqrDist=E,n.len=O,n.sqrLen=x,n.forEach=function(){var t=a();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=4),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i0?(a=2*Math.sqrt(r+1),t[3]=.25*a,t[0]=(n[6]-n[9])/a,t[1]=(n[8]-n[2])/a,t[2]=(n[1]-n[4])/a):n[0]>n[5]&n[0]>n[10]?(a=2*Math.sqrt(1+n[0]-n[5]-n[10]),t[3]=(n[6]-n[9])/a,t[0]=.25*a,t[1]=(n[1]+n[4])/a,t[2]=(n[8]+n[2])/a):n[5]>n[10]?(a=2*Math.sqrt(1+n[5]-n[0]-n[10]),t[3]=(n[8]-n[2])/a,t[0]=(n[1]+n[4])/a,t[1]=.25*a,t[2]=(n[6]+n[9])/a):(a=2*Math.sqrt(1+n[10]-n[0]-n[5]),t[3]=(n[1]-n[4])/a,t[0]=(n[8]+n[2])/a,t[1]=(n[6]+n[9])/a,t[2]=.25*a),t}function _(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3],s=e+e,c=u+u,f=o+o,M=e*s,h=e*c,l=e*f,v=u*c,d=u*f,b=o*f,m=i*s,p=i*c,P=i*f,E=a[0],O=a[1],x=a[2];return t[0]=(1-(v+b))*E,t[1]=(h+P)*E,t[2]=(l-p)*E,t[3]=0,t[4]=(h-P)*O,t[5]=(1-(M+b))*O,t[6]=(d+m)*O,t[7]=0,t[8]=(l+p)*x,t[9]=(d-m)*x,t[10]=(1-(M+v))*x,t[11]=0,t[12]=r[0],t[13]=r[1],t[14]=r[2],t[15]=1,t}function I(t,n,r,a,e){var u=n[0],o=n[1],i=n[2],s=n[3],c=u+u,f=o+o,M=i+i,h=u*c,l=u*f,v=u*M,d=o*f,b=o*M,m=i*M,p=s*c,P=s*f,E=s*M,O=a[0],x=a[1],A=a[2],q=e[0],y=e[1],w=e[2];return t[0]=(1-(d+m))*O,t[1]=(l+E)*O,t[2]=(v-P)*O,t[3]=0,t[4]=(l-E)*x,t[5]=(1-(h+m))*x,t[6]=(b+p)*x,t[7]=0,t[8]=(v+P)*A,t[9]=(b-p)*A,t[10]=(1-(h+d))*A,t[11]=0,t[12]=r[0]+q-(t[0]*q+t[4]*y+t[8]*w),t[13]=r[1]+y-(t[1]*q+t[5]*y+t[9]*w),t[14]=r[2]+w-(t[2]*q+t[6]*y+t[10]*w),t[15]=1,t}function N(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,s=e+e,c=r*o,f=a*o,M=a*i,h=e*o,l=e*i,v=e*s,d=u*o,b=u*i,m=u*s;return t[0]=1-M-v,t[1]=f+m,t[2]=h-b,t[3]=0,t[4]=f-m,t[5]=1-c-v,t[6]=l+d,t[7]=0,t[8]=h+b,t[9]=l-d,t[10]=1-c-M,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t}function Y(t,n,r,a,e,u,o){var i=1/(r-n),s=1/(e-a),c=1/(u-o);return t[0]=2*u*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=2*u*s,t[6]=0,t[7]=0,t[8]=(r+n)*i,t[9]=(e+a)*s,t[10]=(o+u)*c,t[11]=-1,t[12]=0,t[13]=0,t[14]=o*u*2*c,t[15]=0,t}function g(t,n,r,a,e){var u=1/Math.tan(n/2),o=1/(a-e);return t[0]=u/r,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=u,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=(e+a)*o,t[11]=-1,t[12]=0,t[13]=0,t[14]=2*e*a*o,t[15]=0,t}function T(t,n,r,a){var e=Math.tan(n.upDegrees*Math.PI/180),u=Math.tan(n.downDegrees*Math.PI/180),o=Math.tan(n.leftDegrees*Math.PI/180),i=Math.tan(n.rightDegrees*Math.PI/180),s=2/(o+i),c=2/(e+u);return t[0]=s,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=c,t[6]=0,t[7]=0,t[8]=-(o-i)*s*.5,t[9]=(e-u)*c*.5,t[10]=a/(r-a),t[11]=-1,t[12]=0,t[13]=0,t[14]=a*r/(r-a),t[15]=0,t}function j(t,n,r,a,e,u,o){var i=1/(n-r),s=1/(a-e),c=1/(u-o);return t[0]=-2*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=-2*s,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=2*c,t[11]=0,t[12]=(n+r)*i,t[13]=(e+a)*s,t[14]=(o+u)*c,t[15]=1,t}function D(t,n,r,a){var e=void 0,u=void 0,o=void 0,i=void 0,s=void 0,c=void 0,f=void 0,M=void 0,h=void 0,l=void 0,v=n[0],d=n[1],b=n[2],m=a[0],p=a[1],P=a[2],E=r[0],O=r[1],x=r[2];return Math.abs(v-E)0&&(l=1/Math.sqrt(l),f*=l,M*=l,h*=l);var v=s*h-c*M,d=c*f-i*h,b=i*M-s*f;return t[0]=v,t[1]=d,t[2]=b,t[3]=0,t[4]=M*b-h*d,t[5]=h*v-f*b,t[6]=f*d-M*v,t[7]=0,t[8]=f,t[9]=M,t[10]=h,t[11]=0,t[12]=e,t[13]=u,t[14]=o,t[15]=1,t}function z(t){return"mat4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+", "+t[9]+", "+t[10]+", "+t[11]+", "+t[12]+", "+t[13]+", "+t[14]+", "+t[15]+")"}function F(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2)+Math.pow(t[9],2)+Math.pow(t[10],2)+Math.pow(t[11],2)+Math.pow(t[12],2)+Math.pow(t[13],2)+Math.pow(t[14],2)+Math.pow(t[15],2))}function Q(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t[4]=n[4]+r[4],t[5]=n[5]+r[5],t[6]=n[6]+r[6],t[7]=n[7]+r[7],t[8]=n[8]+r[8],t[9]=n[9]+r[9],t[10]=n[10]+r[10],t[11]=n[11]+r[11],t[12]=n[12]+r[12],t[13]=n[13]+r[13],t[14]=n[14]+r[14],t[15]=n[15]+r[15],t}function X(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t[4]=n[4]-r[4],t[5]=n[5]-r[5],t[6]=n[6]-r[6],t[7]=n[7]-r[7],t[8]=n[8]-r[8],t[9]=n[9]-r[9],t[10]=n[10]-r[10],t[11]=n[11]-r[11],t[12]=n[12]-r[12],t[13]=n[13]-r[13],t[14]=n[14]-r[14],t[15]=n[15]-r[15],t}function Z(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t[4]=n[4]*r,t[5]=n[5]*r,t[6]=n[6]*r,t[7]=n[7]*r,t[8]=n[8]*r,t[9]=n[9]*r,t[10]=n[10]*r,t[11]=n[11]*r,t[12]=n[12]*r,t[13]=n[13]*r,t[14]=n[14]*r,t[15]=n[15]*r,t}function k(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t[4]=n[4]+r[4]*a,t[5]=n[5]+r[5]*a,t[6]=n[6]+r[6]*a,t[7]=n[7]+r[7]*a,t[8]=n[8]+r[8]*a,t[9]=n[9]+r[9]*a,t[10]=n[10]+r[10]*a,t[11]=n[11]+r[11]*a,t[12]=n[12]+r[12]*a,t[13]=n[13]+r[13]*a,t[14]=n[14]+r[14]*a,t[15]=n[15]+r[15]*a,t}function U(t,n){return t[0]===n[0]&&t[1]===n[1]&&t[2]===n[2]&&t[3]===n[3]&&t[4]===n[4]&&t[5]===n[5]&&t[6]===n[6]&&t[7]===n[7]&&t[8]===n[8]&&t[9]===n[9]&&t[10]===n[10]&&t[11]===n[11]&&t[12]===n[12]&&t[13]===n[13]&&t[14]===n[14]&&t[15]===n[15]}function W(t,n){var r=t[0],a=t[1],e=t[2],u=t[3],o=t[4],i=t[5],s=t[6],c=t[7],f=t[8],M=t[9],h=t[10],l=t[11],v=t[12],d=t[13],b=t[14],m=t[15],p=n[0],P=n[1],E=n[2],O=n[3],x=n[4],A=n[5],q=n[6],y=n[7],w=n[8],R=n[9],L=n[10],S=n[11],_=n[12],I=n[13],N=n[14],Y=n[15];return Math.abs(r-p)<=C.EPSILON*Math.max(1,Math.abs(r),Math.abs(p))&&Math.abs(a-P)<=C.EPSILON*Math.max(1,Math.abs(a),Math.abs(P))&&Math.abs(e-E)<=C.EPSILON*Math.max(1,Math.abs(e),Math.abs(E))&&Math.abs(u-O)<=C.EPSILON*Math.max(1,Math.abs(u),Math.abs(O))&&Math.abs(o-x)<=C.EPSILON*Math.max(1,Math.abs(o),Math.abs(x))&&Math.abs(i-A)<=C.EPSILON*Math.max(1,Math.abs(i),Math.abs(A))&&Math.abs(s-q)<=C.EPSILON*Math.max(1,Math.abs(s),Math.abs(q))&&Math.abs(c-y)<=C.EPSILON*Math.max(1,Math.abs(c),Math.abs(y))&&Math.abs(f-w)<=C.EPSILON*Math.max(1,Math.abs(f),Math.abs(w))&&Math.abs(M-R)<=C.EPSILON*Math.max(1,Math.abs(M),Math.abs(R))&&Math.abs(h-L)<=C.EPSILON*Math.max(1,Math.abs(h),Math.abs(L))&&Math.abs(l-S)<=C.EPSILON*Math.max(1,Math.abs(l),Math.abs(S))&&Math.abs(v-_)<=C.EPSILON*Math.max(1,Math.abs(v),Math.abs(_))&&Math.abs(d-I)<=C.EPSILON*Math.max(1,Math.abs(d),Math.abs(I))&&Math.abs(b-N)<=C.EPSILON*Math.max(1,Math.abs(b),Math.abs(N))&&Math.abs(m-Y)<=C.EPSILON*Math.max(1,Math.abs(m),Math.abs(Y))}Object.defineProperty(n,"__esModule",{value:!0}),n.sub=n.mul=void 0,n.create=a,n.clone=e,n.copy=u,n.fromValues=o,n.set=i,n.identity=s,n.transpose=c,n.invert=f,n.adjoint=M,n.determinant=h,n.multiply=l,n.translate=v,n.scale=d,n.rotate=b,n.rotateX=m,n.rotateY=p,n.rotateZ=P,n.fromTranslation=E,n.fromScaling=O,n.fromRotation=x,n.fromXRotation=A,n.fromYRotation=q,n.fromZRotation=y,n.fromRotationTranslation=w,n.getTranslation=R,n.getScaling=L,n.getRotation=S,n.fromRotationTranslationScale=_,n.fromRotationTranslationScaleOrigin=I,n.fromQuat=N,n.frustum=Y,n.perspective=g,n.perspectiveFromFieldOfView=T,n.ortho=j,n.lookAt=D,n.targetTo=V,n.str=z,n.frob=F,n.add=Q,n.subtract=X,n.multiplyScalar=Z,n.multiplyScalarAndAdd=k,n.exactEquals=U,n.equals=W;var B=r(0),C=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(B);n.mul=l,n.sub=X},function(t,n,r){"use strict";function a(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}function e(){var t=new E.ARRAY_TYPE(4);return t[0]=0,t[1]=0,t[2]=0,t[3]=1,t}function u(t){return t[0]=0,t[1]=0,t[2]=0,t[3]=1,t}function o(t,n,r){r*=.5;var a=Math.sin(r);return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=Math.cos(r),t}function i(t,n){var r=2*Math.acos(n[3]),a=Math.sin(r/2);return 0!=a?(t[0]=n[0]/a,t[1]=n[1]/a,t[2]=n[2]/a):(t[0]=1,t[1]=0,t[2]=0),r}function s(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],s=r[1],c=r[2],f=r[3];return t[0]=a*f+o*i+e*c-u*s,t[1]=e*f+o*s+u*i-a*c,t[2]=u*f+o*c+a*s-e*i,t[3]=o*f-a*i-e*s-u*c,t}function c(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s+o*i,t[1]=e*s+u*i,t[2]=u*s-e*i,t[3]=o*s-a*i,t}function f(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s-u*i,t[1]=e*s+o*i,t[2]=u*s+a*i,t[3]=o*s-e*i,t}function M(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),s=Math.cos(r);return t[0]=a*s+e*i,t[1]=e*s-a*i,t[2]=u*s+o*i,t[3]=o*s-u*i,t}function h(t,n){var r=n[0],a=n[1],e=n[2];return t[0]=r,t[1]=a,t[2]=e,t[3]=Math.sqrt(Math.abs(1-r*r-a*a-e*e)),t}function l(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3],s=r[0],c=r[1],f=r[2],M=r[3],h=void 0,l=void 0,v=void 0,d=void 0,b=void 0;return l=e*s+u*c+o*f+i*M,l<0&&(l=-l,s=-s,c=-c,f=-f,M=-M),1-l>1e-6?(h=Math.acos(l),v=Math.sin(h),d=Math.sin((1-a)*h)/v,b=Math.sin(a*h)/v):(d=1-a,b=a),t[0]=d*e+b*s,t[1]=d*u+b*c,t[2]=d*o+b*f,t[3]=d*i+b*M,t}function v(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*r+a*a+e*e+u*u,i=o?1/o:0;return t[0]=-r*i,t[1]=-a*i,t[2]=-e*i,t[3]=u*i,t}function d(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t[3]=n[3],t}function b(t,n){var r=n[0]+n[4]+n[8],a=void 0;if(r>0)a=Math.sqrt(r+1),t[3]=.5*a,a=.5/a,t[0]=(n[5]-n[7])*a,t[1]=(n[6]-n[2])*a,t[2]=(n[1]-n[3])*a;else{var e=0;n[4]>n[0]&&(e=1),n[8]>n[3*e+e]&&(e=2);var u=(e+1)%3,o=(e+2)%3;a=Math.sqrt(n[3*e+e]-n[3*u+u]-n[3*o+o]+1),t[e]=.5*a,a=.5/a,t[3]=(n[3*u+o]-n[3*o+u])*a,t[u]=(n[3*u+e]+n[3*e+u])*a,t[o]=(n[3*o+e]+n[3*e+o])*a}return t}function m(t,n,r,a){var e=.5*Math.PI/180;n*=e,r*=e,a*=e;var u=Math.sin(n),o=Math.cos(n),i=Math.sin(r),s=Math.cos(r),c=Math.sin(a),f=Math.cos(a);return t[0]=u*s*f-o*i*c,t[1]=o*i*f+u*s*c,t[2]=o*s*c-u*i*f,t[3]=o*s*f+u*i*c,t}function p(t){return"quat("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"}Object.defineProperty(n,"__esModule",{value:!0}),n.setAxes=n.sqlerp=n.rotationTo=n.equals=n.exactEquals=n.normalize=n.sqrLen=n.squaredLength=n.len=n.length=n.lerp=n.dot=n.scale=n.mul=n.add=n.set=n.copy=n.fromValues=n.clone=void 0,n.create=e,n.identity=u,n.setAxisAngle=o,n.getAxisAngle=i,n.multiply=s,n.rotateX=c,n.rotateY=f,n.rotateZ=M,n.calculateW=h,n.slerp=l,n.invert=v,n.conjugate=d,n.fromMat3=b,n.fromEuler=m,n.str=p;var P=r(0),E=a(P),O=r(1),x=a(O),A=r(2),q=a(A),y=r(3),w=a(y),R=(n.clone=w.clone,n.fromValues=w.fromValues,n.copy=w.copy,n.set=w.set,n.add=w.add,n.mul=s,n.scale=w.scale,n.dot=w.dot,n.lerp=w.lerp,n.length=w.length),L=(n.len=R,n.squaredLength=w.squaredLength),S=(n.sqrLen=L,n.normalize=w.normalize);n.exactEquals=w.exactEquals,n.equals=w.equals,n.rotationTo=function(){var t=q.create(),n=q.fromValues(1,0,0),r=q.fromValues(0,1,0);return function(a,e,u){var i=q.dot(e,u);return i<-.999999?(q.cross(t,n,e),q.len(t)<1e-6&&q.cross(t,r,e),q.normalize(t,t),o(a,t,Math.PI),a):i>.999999?(a[0]=0,a[1]=0,a[2]=0,a[3]=1,a):(q.cross(t,e,u),a[0]=t[0],a[1]=t[1],a[2]=t[2],a[3]=1+i,S(a,a))}}(),n.sqlerp=function(){var t=e(),n=e();return function(r,a,e,u,o,i){return l(t,a,o,i),l(n,e,u,i),l(r,t,n,2*i*(1-i)),r}}(),n.setAxes=function(){var t=x.create();return function(n,r,a,e){return t[0]=a[0],t[3]=a[1],t[6]=a[2],t[1]=e[0],t[4]=e[1],t[7]=e[2],t[2]=-r[0],t[5]=-r[1],t[8]=-r[2],S(n,b(n,t))}}()},function(t,n,r){"use strict";function a(){var t=new V.ARRAY_TYPE(2);return t[0]=0,t[1]=0,t}function e(t){var n=new V.ARRAY_TYPE(2);return n[0]=t[0],n[1]=t[1],n}function u(t,n){var r=new V.ARRAY_TYPE(2);return r[0]=t,r[1]=n,r}function o(t,n){return t[0]=n[0],t[1]=n[1],t}function i(t,n,r){return t[0]=n,t[1]=r,t}function s(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t}function c(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t}function f(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t}function M(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t}function h(t,n){return t[0]=Math.ceil(n[0]),t[1]=Math.ceil(n[1]),t}function l(t,n){return t[0]=Math.floor(n[0]),t[1]=Math.floor(n[1]),t}function v(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t}function d(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t}function b(t,n){return t[0]=Math.round(n[0]),t[1]=Math.round(n[1]),t}function m(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t}function p(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t}function P(t,n){var r=n[0]-t[0],a=n[1]-t[1];return Math.sqrt(r*r+a*a)}function E(t,n){var r=n[0]-t[0],a=n[1]-t[1];return r*r+a*a}function O(t){var n=t[0],r=t[1];return Math.sqrt(n*n+r*r)}function x(t){var n=t[0],r=t[1];return n*n+r*r}function A(t,n){return t[0]=-n[0],t[1]=-n[1],t}function q(t,n){return t[0]=1/n[0],t[1]=1/n[1],t}function y(t,n){var r=n[0],a=n[1],e=r*r+a*a;return e>0&&(e=1/Math.sqrt(e),t[0]=n[0]*e,t[1]=n[1]*e),t}function w(t,n){return t[0]*n[0]+t[1]*n[1]}function R(t,n,r){var a=n[0]*r[1]-n[1]*r[0];return t[0]=t[1]=0,t[2]=a,t}function L(t,n,r,a){var e=n[0],u=n[1];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t}function S(t,n){n=n||1;var r=2*V.RANDOM()*Math.PI;return t[0]=Math.cos(r)*n,t[1]=Math.sin(r)*n,t}function _(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e,t[1]=r[1]*a+r[3]*e,t}function I(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e+r[4],t[1]=r[1]*a+r[3]*e+r[5],t}function N(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[3]*e+r[6],t[1]=r[1]*a+r[4]*e+r[7],t}function Y(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[4]*e+r[12],t[1]=r[1]*a+r[5]*e+r[13],t}function g(t){return"vec2("+t[0]+", "+t[1]+")"}function T(t,n){return t[0]===n[0]&&t[1]===n[1]}function j(t,n){var r=t[0],a=t[1],e=n[0],u=n[1];return Math.abs(r-e)<=V.EPSILON*Math.max(1,Math.abs(r),Math.abs(e))&&Math.abs(a-u)<=V.EPSILON*Math.max(1,Math.abs(a),Math.abs(u))}Object.defineProperty(n,"__esModule",{value:!0}),n.forEach=n.sqrLen=n.sqrDist=n.dist=n.div=n.mul=n.sub=n.len=void 0,n.create=a,n.clone=e,n.fromValues=u,n.copy=o,n.set=i,n.add=s,n.subtract=c,n.multiply=f,n.divide=M,n.ceil=h,n.floor=l,n.min=v,n.max=d,n.round=b,n.scale=m,n.scaleAndAdd=p,n.distance=P,n.squaredDistance=E,n.length=O,n.squaredLength=x,n.negate=A,n.inverse=q,n.normalize=y,n.dot=w,n.cross=R,n.lerp=L,n.random=S,n.transformMat2=_,n.transformMat2d=I,n.transformMat3=N,n.transformMat4=Y,n.str=g,n.exactEquals=T,n.equals=j;var D=r(0),V=function(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n.default=t,n}(D);n.len=O,n.sub=c,n.mul=f,n.div=M,n.dist=P,n.sqrDist=E,n.sqrLen=x,n.forEach=function(){var t=a();return function(n,r,a,e,u,o){var i=void 0,s=void 0;for(r||(r=2),a||(a=0),s=e?Math.min(e*r+a,n.length):n.length,i=a;i 0) { 235 | swcShader.use(gl); 236 | gl.uniform1i(swcShader.uniforms["volume"], 0); 237 | gl.uniform1i(swcShader.uniforms["ivolume"], 5); 238 | gl.uniform2fv(swcShader.uniforms["value_range"], volValueRange); 239 | gl.uniform1i(swcShader.uniforms["volume_is_int"], volumeIsInt); 240 | gl.uniform1i(swcShader.uniforms["highlight_errors"], highlightErrors.checked); 241 | 242 | gl.uniform3iv(swcShader.uniforms["volume_dims"], volDims); 243 | gl.uniform3fv(swcShader.uniforms["volume_scale"], volScale); 244 | gl.uniformMatrix4fv(swcShader.uniforms["proj_view"], false, projView); 245 | 246 | for (var i = 0; i < neurons.length; ++i) { 247 | var swc = neurons[i]; 248 | if (!swc.visible.checked) { 249 | continue; 250 | } 251 | 252 | // Upload new SWC files 253 | if (swc.vao == null) { 254 | swc.vao = gl.createVertexArray(); 255 | gl.bindVertexArray(swc.vao); 256 | 257 | swc.vbo = gl.createBuffer(); 258 | gl.bindBuffer(gl.ARRAY_BUFFER, swc.vbo); 259 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(swc.points), gl.STATIC_DRAW); 260 | gl.enableVertexAttribArray(0); 261 | gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); 262 | 263 | swc.ebo = gl.createBuffer(); 264 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, swc.ebo); 265 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(swc.indices), gl.STATIC_DRAW); 266 | } 267 | 268 | var color = hexToRGBf(swc.color.value); 269 | gl.uniform3fv(swcShader.uniforms["swc_color"], color); 270 | 271 | // Draw the SWC file 272 | gl.bindVertexArray(swc.vao); 273 | for (var j = 0; j < swc.branches.length; ++j) { 274 | var b = swc.branches[j]; 275 | gl.drawElements(gl.LINE_STRIP, b["count"], gl.UNSIGNED_SHORT, 2 * b["start"]); 276 | } 277 | } 278 | } 279 | 280 | if (num_diff_segment_vertices > 0) { 281 | swcShader.use(gl); 282 | gl.uniform3iv(swcShader.uniforms["volume_dims"], volDims); 283 | gl.uniform3fv(swcShader.uniforms["volume_scale"], volScale); 284 | gl.uniformMatrix4fv(swcShader.uniforms["proj_view"], false, projView); 285 | gl.uniform3fv(swcShader.uniforms["swc_color"], [1.0, 0.0, 0.0]); 286 | 287 | gl.bindVertexArray(diff_segments_vao); 288 | gl.drawArrays(gl.LINES, 0, num_diff_segment_vertices); 289 | } 290 | 291 | gl.bindFramebuffer(gl.FRAMEBUFFER, colorFbo); 292 | if (volumeLoaded && showVolume.checked) { 293 | gl.activeTexture(gl.TEXTURE4); 294 | gl.bindTexture(gl.TEXTURE_2D, renderTargets[1]); 295 | shader.use(gl); 296 | gl.uniform2fv(shader.uniforms["value_range"], volValueRange); 297 | gl.uniform3iv(shader.uniforms["volume_dims"], volDims); 298 | gl.uniform3fv(shader.uniforms["volume_scale"], volScale); 299 | gl.uniformMatrix4fv(shader.uniforms["proj_view"], false, projView); 300 | gl.uniformMatrix4fv(shader.uniforms["inv_proj"], false, invProj); 301 | gl.uniform1i(shader.uniforms["volume_is_int"], volumeIsInt); 302 | 303 | var invView = mat4.invert(mat4.create(), camera.camera); 304 | gl.uniformMatrix4fv(shader.uniforms["inv_view"], false, invView); 305 | gl.uniform3fv(shader.uniforms["eye_pos"], eye); 306 | gl.uniform1i(shader.uniforms["highlight_trace"], highlightTrace.checked); 307 | gl.uniform1f(shader.uniforms["threshold"], volumeThreshold.value); 308 | gl.uniform1f(shader.uniforms["saturation_threshold"], saturationThreshold.value); 309 | 310 | gl.bindVertexArray(volumeVao); 311 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, cubeStrip.length / 3); 312 | } 313 | 314 | // Seems like we can't blit the framebuffer b/c the default draw fbo might be 315 | // using multiple samples? 316 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 317 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 318 | gl.disable(gl.BLEND); 319 | gl.disable(gl.CULL_FACE); 320 | blitImageShader.use(gl); 321 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 322 | gl.enable(gl.CULL_FACE); 323 | gl.enable(gl.BLEND); 324 | 325 | // Wait for rendering to actually finish 326 | gl.finish(); 327 | var endTime = performance.now(); 328 | var renderTime = endTime - startTime; 329 | var targetSamplingRate = renderTime / targetFrameTime; 330 | 331 | // If we're dropping frames, decrease the sampling rate, or if we're 332 | // rendering faster try increasing it to provide better quality 333 | if (!newVolumeUpload) { 334 | // Chrome doesn't actually wait for gl.finish to return 335 | if (targetSamplingRate > 0.8) { 336 | samplingRate = 0.9 * samplingRate + 0.1 * targetSamplingRate; 337 | shader.use(gl); 338 | gl.uniform1f(shader.uniforms["dt_scale"], samplingRate); 339 | } 340 | } 341 | newVolumeUpload = false; 342 | startTime = endTime; 343 | } 344 | 345 | var selectColormap = function() { 346 | var selection = document.getElementById("colormapList").value; 347 | var colormapImage = new Image(); 348 | colormapImage.onload = function() { 349 | gl.activeTexture(gl.TEXTURE1); 350 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 180, 1, 351 | gl.RGBA, gl.UNSIGNED_BYTE, colormapImage); 352 | }; 353 | colormapImage.src = colormaps[selection]; 354 | } 355 | 356 | window.onload = function() { 357 | fillcolormapSelector(); 358 | 359 | highlightTrace = document.getElementById("highlightTrace"); 360 | highlightErrors = document.getElementById("highlightErrors"); 361 | highlightErrors.checked = false; 362 | 363 | showVolume = document.getElementById("showVolume"); 364 | showVolume.checked = true; 365 | 366 | volumeThreshold = document.getElementById("threshold"); 367 | volumeThreshold.value = 0.1; 368 | 369 | saturationThreshold = document.getElementById("saturationThreshold"); 370 | saturationThreshold.value = 1; 371 | 372 | loadingProgressText = document.getElementById("loadingText"); 373 | loadingProgressBar = document.getElementById("loadingProgressBar"); 374 | 375 | canvas = document.getElementById("glcanvas"); 376 | 377 | // For some random JS/HTML reason it won't find the function if it's not set here 378 | document.getElementById("fetchTIFFButton").onclick = fetchTIFF; 379 | document.getElementById("uploadSWC").onchange = uploadSWC; 380 | document.getElementById("shareURLButton").onclick = buildShareURL; 381 | 382 | voxelSpacingInputs = [ 383 | document.getElementById("voxelSpacingX"), 384 | document.getElementById("voxelSpacingY"), 385 | document.getElementById("voxelSpacingZ") 386 | ]; 387 | 388 | if (window.location.hash) { 389 | var regexResolution = /(\d+)x(\d+)/; 390 | var regexVoxelSpacing = /(\d+\.?\d?)x(\d+\.?\d?)x(\d+\.?\d?)/; 391 | var urlParams = window.location.hash.substr(1).split("&"); 392 | for (var i = 0; i < urlParams.length; ++i) { 393 | var str = decodeURI(urlParams[i]); 394 | console.log(str); 395 | // URL load param 396 | if (str.startsWith("url=")) { 397 | volumeURL = str.substr(4); 398 | continue; 399 | } 400 | // Volume threshold 401 | if (str.startsWith("thresh=")) { 402 | volumeThreshold.value = clamp(parseFloat(str.substr(7)), 0, 1); 403 | continue; 404 | } 405 | // Saturation threshold 406 | if (str.startsWith("sat=")) { 407 | saturationThreshold.value = clamp(parseFloat(str.substr(4)), 0, 1); 408 | continue; 409 | } 410 | // Voxel Spacing 411 | if (str.startsWith("vox=")) { 412 | var m = str.substr(4).match(regexVoxelSpacing); 413 | voxelSpacingFromURL = true; 414 | voxelSpacingInputs[0].value = Math.max(parseFloat(m[1]), 1); 415 | voxelSpacingInputs[1].value = Math.max(parseFloat(m[2]), 1); 416 | voxelSpacingInputs[2].value = Math.max(parseFloat(m[3]), 1); 417 | continue; 418 | } 419 | // Colormap 420 | if (str.startsWith("cmap=")) { 421 | var cmap = parseInt(str.substr(5)); 422 | var selector = document.getElementById("colormapList"); 423 | selector.value = "Grayscale"; 424 | if (cmap == 2) { 425 | selector.value = "Cool Warm"; 426 | } else if (cmap == 3) { 427 | selector.value = "Matplotlib Plasma"; 428 | } else if (cmap == 4) { 429 | selector.value = "Matplotlib Virdis"; 430 | } else if (cmap == 5) { 431 | selector.value = "Rainbow"; 432 | } else if (cmap == 6) { 433 | selector.value = "Samsel Linear Green"; 434 | } else if (cmap == 7) { 435 | selector.value = "Samsel Linear YGB 1211G"; 436 | } 437 | continue; 438 | } 439 | // When embedding as an iframe, go hide the UI text and leave just the controls 440 | if (str == "embed") { 441 | document.getElementById("viewerTitle").setAttribute("style", "display:none"); 442 | document.getElementById("shareURLUI").setAttribute("style", "display:none"); 443 | document.getElementById("uiText").setAttribute("style", "display:none"); 444 | document.getElementById("loadDiademReference").setAttribute("style", "display:none"); 445 | } 446 | if (str == "embedMinimal") { 447 | document.getElementById("viewerTitle").setAttribute("style", "display:none"); 448 | document.getElementById("shareURLUI").setAttribute("style", "display:none"); 449 | document.getElementById("uiText").setAttribute("style", "display:none"); 450 | document.getElementById("loadDiademReference").setAttribute("style", "display:none"); 451 | document.getElementById("hideEmbedMinimal").setAttribute("style", "display:none"); 452 | } 453 | // Canvas dimensions 454 | var m = str.match(regexResolution); 455 | if (m) { 456 | WIDTH = parseInt(m[1]); 457 | HEIGHT = parseInt(m[2]); 458 | canvas.width = WIDTH; 459 | canvas.height = HEIGHT; 460 | canvas.className = ""; 461 | } 462 | } 463 | } 464 | 465 | gl = canvas.getContext("webgl2"); 466 | if (!gl) { 467 | alert("Unable to initialize WebGL2. Your browser may not support it"); 468 | return; 469 | } 470 | WIDTH = canvas.getAttribute("width"); 471 | HEIGHT = canvas.getAttribute("height"); 472 | 473 | proj = mat4.perspective(mat4.create(), 60 * Math.PI / 180.0, 474 | WIDTH / HEIGHT, 0.01, 100); 475 | invProj = mat4.invert(mat4.create(), proj); 476 | 477 | camera = new ArcballCamera(defaultEye, center, up, 1, [WIDTH, HEIGHT]); 478 | projView = mat4.create(); 479 | 480 | // Register mouse and touch listeners 481 | var controller = new Controller(); 482 | controller.mousemove = function(prev, cur, evt) { 483 | if (evt.buttons == 1) { 484 | camera.rotate(prev, cur); 485 | } else if (evt.buttons == 2) { 486 | camera.pan([cur[0] - prev[0], prev[1] - cur[1]]); 487 | } 488 | }; 489 | controller.wheel = function(amt) { camera.zoom(amt); }; 490 | controller.pinch = controller.wheel; 491 | controller.twoFingerDrag = function(drag) { camera.pan(drag); }; 492 | 493 | controller.registerForCanvas(canvas); 494 | 495 | // Setup VAO and VBO to render the cube to run the raymarching shader 496 | volumeVao = gl.createVertexArray(); 497 | gl.bindVertexArray(volumeVao); 498 | 499 | var vbo = gl.createBuffer(); 500 | gl.bindBuffer(gl.ARRAY_BUFFER, vbo); 501 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cubeStrip), gl.STATIC_DRAW); 502 | 503 | gl.enableVertexAttribArray(0); 504 | gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); 505 | 506 | blitImageShader = new Shader(gl, quadVertShader, quadFragShader); 507 | blitImageShader.use(gl); 508 | gl.uniform1i(blitImageShader.uniforms["colors"], 3); 509 | 510 | swcShader = new Shader(gl, swcVertShader, swcFragShader); 511 | 512 | shader = new Shader(gl, vertShader, fragShader); 513 | shader.use(gl); 514 | 515 | gl.uniform1i(shader.uniforms["volume"], 0); 516 | gl.uniform1i(shader.uniforms["ivolume"], 5); 517 | gl.uniform1i(shader.uniforms["colormap"], 1); 518 | gl.uniform1i(shader.uniforms["depth"], 4); 519 | gl.uniform1f(shader.uniforms["dt_scale"], 1.0); 520 | gl.uniform2iv(shader.uniforms["canvas_dims"], [WIDTH, HEIGHT]); 521 | 522 | // Setup required OpenGL state for drawing the back faces and 523 | // composting with the background color 524 | gl.enable(gl.CULL_FACE); 525 | gl.cullFace(gl.FRONT); 526 | gl.enable(gl.BLEND); 527 | gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 528 | 529 | gl.clearColor(0.01, 0.01, 0.01, 1.0); 530 | gl.clearDepth(1.0); 531 | 532 | // Setup the render targets for the splat rendering pass 533 | renderTargets = [gl.createTexture(), gl.createTexture()] 534 | gl.bindTexture(gl.TEXTURE_2D, renderTargets[0]); 535 | gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, WIDTH, HEIGHT); 536 | 537 | gl.bindTexture(gl.TEXTURE_2D, renderTargets[1]); 538 | gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT32F, WIDTH, HEIGHT); 539 | 540 | for (var i = 0; i < 2; ++i) { 541 | gl.bindTexture(gl.TEXTURE_2D, renderTargets[i]); 542 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 543 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 544 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 545 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 546 | } 547 | 548 | gl.activeTexture(gl.TEXTURE3); 549 | gl.bindTexture(gl.TEXTURE_2D, renderTargets[0]); 550 | gl.activeTexture(gl.TEXTURE4); 551 | gl.bindTexture(gl.TEXTURE_2D, renderTargets[1]); 552 | 553 | depthColorFbo = gl.createFramebuffer(); 554 | gl.bindFramebuffer(gl.FRAMEBUFFER, depthColorFbo); 555 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 556 | gl.TEXTURE_2D, renderTargets[0], 0); 557 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, 558 | gl.TEXTURE_2D, renderTargets[1], 0); 559 | gl.drawBuffers([gl.COLOR_ATTACHMENT0]); 560 | 561 | colorFbo = gl.createFramebuffer(); 562 | gl.bindFramebuffer(gl.FRAMEBUFFER, colorFbo); 563 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 564 | gl.TEXTURE_2D, renderTargets[0], 0); 565 | gl.drawBuffers([gl.COLOR_ATTACHMENT0]); 566 | 567 | // Load the default colormap and upload it, after which we 568 | // load the default volume. 569 | var colormapImage = new Image(); 570 | colormapImage.onload = function() { 571 | var colormap = gl.createTexture(); 572 | gl.activeTexture(gl.TEXTURE1); 573 | gl.bindTexture(gl.TEXTURE_2D, colormap); 574 | gl.texStorage2D(gl.TEXTURE_2D, 1, gl.SRGB8_ALPHA8 , 180, 1); 575 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 576 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE); 577 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 578 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 180, 1, 579 | gl.RGBA, gl.UNSIGNED_BYTE, colormapImage); 580 | 581 | 582 | if (volumeURL) { 583 | fetchTIFFURL(volumeURL); 584 | document.getElementById("tiffUploadBox").style = "display:block"; 585 | } else { 586 | loadRAWVolume(volumes["DIADEM NC Layer 1 Axons"]); 587 | } 588 | setInterval(renderLoop, targetFrameTime); 589 | }; 590 | colormapImage.src = colormaps[document.getElementById("colormapList").value]; 591 | } 592 | 593 | var fillcolormapSelector = function() { 594 | var selector = document.getElementById("colormapList"); 595 | for (p in colormaps) { 596 | var opt = document.createElement("option"); 597 | opt.value = p; 598 | opt.innerHTML = p; 599 | selector.appendChild(opt); 600 | } 601 | } 602 | 603 | var TIFFGLFormat = function(sampleFormat, bytesPerSample) { 604 | // For neuron data I doubt they'll have negative vals, so just treat "int" as uint 605 | if (sampleFormat === TiffSampleFormat.UINT 606 | || sampleFormat == TiffSampleFormat.UNSPECIFIED 607 | || sampleFormat == TiffSampleFormat.INT) 608 | { 609 | if (bytesPerSample == 1) { 610 | return gl.R8; 611 | } else if (bytesPerSample == 2) { 612 | return gl.R16UI 613 | } 614 | } 615 | alert("Unsupported TIFF Format, only 8 & 16 bit uint are supported"); 616 | } 617 | 618 | var makeTIFFGLVolume = function(tiff) { 619 | var imgFormat = TIFFGetField(tiff, TiffTag.SAMPLEFORMAT); 620 | var bytesPerSample = TIFFGetField(tiff, TiffTag.BITSPERSAMPLE) / 8; 621 | var width = TIFFGetField(tiff, TiffTag.IMAGEWIDTH); 622 | var height = TIFFGetField(tiff, TiffTag.IMAGELENGTH); 623 | 624 | if (!voxelSpacingFromURL) { 625 | for (var i = 0; i < 3; ++i) { 626 | voxelSpacingInputs[i].value = 1; 627 | } 628 | } 629 | 630 | var description = TIFFGetStringField(tiff, TiffTag.IMAGEDESCRIPTION); 631 | if (!voxelSpacingFromURL && description) { 632 | var findSpacing = /spacing=(\d+)/; 633 | var m = description.match(findSpacing); 634 | if (m) { 635 | voxelSpacingInputs[2].value = parseFloat(m[1]); 636 | } 637 | } 638 | 639 | var glFormat = TIFFGLFormat(imgFormat, bytesPerSample); 640 | if (volumeTexture) { 641 | gl.deleteTexture(volumeTexture); 642 | } 643 | if (glFormat == gl.R8) { 644 | gl.activeTexture(gl.TEXTURE0); 645 | } else { 646 | gl.activeTexture(gl.TEXTURE5); 647 | } 648 | volumeTexture = gl.createTexture(); 649 | gl.bindTexture(gl.TEXTURE_3D, volumeTexture); 650 | gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); 651 | gl.texStorage3D(gl.TEXTURE_3D, 1, glFormat, volDims[0], volDims[1], volDims[2]); 652 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE); 653 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 654 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 655 | 656 | if (glFormat == gl.R8) { 657 | volumeIsInt = 0; 658 | volValueRange[0] = 0; 659 | volValueRange[1] = 1; 660 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 661 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 662 | } else { 663 | volumeIsInt = 1; 664 | // R16 is not normalized/texture filterable so we need to normalize it 665 | volValueRange[0] = Infinity; 666 | volValueRange[1] = -Infinity; 667 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 668 | gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 669 | } 670 | } 671 | 672 | var loadTIFFSlice = function(tiff, z_index, slice_scratch) { 673 | var bps = TIFFGetField(tiff, TiffTag.BITSPERSAMPLE); 674 | 675 | // We only support single channel images 676 | if (TIFFGetField(tiff, TiffTag.SAMPLESPERPIXEL) != 1) { 677 | alert("Only single channel images are supported"); 678 | return; 679 | } 680 | 681 | var imgFormat = TIFFGetField(tiff, TiffTag.SAMPLEFORMAT); 682 | 683 | var width = TIFFGetField(tiff, TiffTag.IMAGEWIDTH); 684 | var height = TIFFGetField(tiff, TiffTag.IMAGELENGTH); 685 | 686 | var numStrips = TIFFNumberOfStrips(tiff); 687 | var rowsPerStrip = TIFFGetField(tiff, TiffTag.ROWSPERSTRIP); 688 | 689 | var bytesPerSample = TIFFGetField(tiff, TiffTag.BITSPERSAMPLE) / 8; 690 | 691 | var sbuf = TIFFMalloc(TIFFStripSize(tiff)); 692 | for (var s = 0; s < numStrips; ++s) { 693 | var read = TIFFReadEncodedStrip(tiff, s, sbuf, -1); 694 | if (read == -1) { 695 | alert("Error reading encoded strip from TIFF file " + file); 696 | } 697 | // Just make a view into the heap, not a copy 698 | var stripData = new Uint8Array(Module.HEAPU8.buffer, sbuf, read); 699 | slice_scratch.set(stripData, s * rowsPerStrip * width * bytesPerSample); 700 | } 701 | TIFFFree(sbuf); 702 | 703 | // Flip the image in Y, since TIFF y axis is downwards 704 | for (var y = 0; y < height / 2; ++y) { 705 | for (var x = 0; x < width; ++x) { 706 | for (var b = 0; b < bytesPerSample; ++b) { 707 | var tmp = slice_scratch[(y * width + x) * bytesPerSample]; 708 | slice_scratch[(y * width + x) * bytesPerSample] = 709 | slice_scratch[((height - y - 1) * width + x) * bytesPerSample]; 710 | slice_scratch[((height - y - 1) * width + x) * bytesPerSample] = tmp; 711 | } 712 | } 713 | } 714 | 715 | var glFormat = TIFFGLFormat(imgFormat, bytesPerSample); 716 | if (glFormat == gl.R8) { 717 | gl.activeTexture(gl.TEXTURE0); 718 | gl.bindTexture(gl.TEXTURE_3D, volumeTexture); 719 | gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, z_index, 720 | width, height, 1, gl.RED, gl.UNSIGNED_BYTE, slice_scratch); 721 | } else { 722 | gl.activeTexture(gl.TEXTURE5); 723 | gl.bindTexture(gl.TEXTURE_3D, volumeTexture); 724 | var u16arr = new Uint16Array(slice_scratch.buffer); 725 | for (var j = 0; j < u16arr.length; ++j) { 726 | volValueRange[0] = Math.min(volValueRange[0], u16arr[j]); 727 | volValueRange[1] = Math.max(volValueRange[1], u16arr[j]); 728 | } 729 | gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, z_index, 730 | width, height, 1, gl.RED_INTEGER, gl.UNSIGNED_SHORT, u16arr); 731 | } 732 | } 733 | 734 | var loadMultipageTiff = function(tiff, numDirectories) { 735 | TIFFSetDirectory(tiff, 0); 736 | var width = TIFFGetField(tiff, TiffTag.IMAGEWIDTH); 737 | var height = TIFFGetField(tiff, TiffTag.IMAGELENGTH); 738 | var bytesPerSample = TIFFGetField(tiff, TiffTag.BITSPERSAMPLE) / 8; 739 | var slice_scratch = new Uint8Array(width * height * bytesPerSample); 740 | for (var i = 0; i < numDirectories; ++i) { 741 | loadTIFFSlice(tiff, i, slice_scratch); 742 | TIFFReadDirectory(tiff); 743 | 744 | var percent = i / numDirectories * 100; 745 | loadingProgressBar.setAttribute("style", "width: " + percent.toFixed(2) + "%"); 746 | } 747 | 748 | volumeLoaded = true; 749 | newVolumeUpload = true; 750 | loadingProgressText.innerHTML = "Loaded Volume"; 751 | loadingProgressBar.setAttribute("style", "width: 101%"); 752 | } 753 | 754 | var uploadTIFF = function(files) { 755 | var showURL = document.getElementById("shareURL").setAttribute("style", "display:none"); 756 | 757 | var numLoaded = 0; 758 | volumeLoaded = false; 759 | 760 | loadingProgressText.innerHTML = "Loading Volume"; 761 | loadingProgressBar.setAttribute("style", "width: 0%"); 762 | 763 | var slice_scratch = null; 764 | var start = performance.now(); 765 | 766 | var loadFile = function(i) { 767 | var file = files[i]; 768 | var reader = new FileReader(); 769 | reader.onerror = function() { 770 | alert("Error reading TIFF file " + file.name); 771 | }; 772 | reader.onprogress = function(evt) { 773 | var percent = numLoaded / files.length * 100; 774 | loadingProgressBar.setAttribute("style", "width: " + percent.toFixed(2) + "%"); 775 | }; 776 | reader.onload = function(evt) { 777 | var buf = reader.result; 778 | if (buf) { 779 | var fname = "temp" + i + ".tiff"; 780 | FS.createDataFile("/", fname, new Uint8Array(reader.result), true, false); 781 | var tiff = TIFFOpen(fname, "r"); 782 | 783 | if (!slice_scratch) { 784 | var width = TIFFGetField(tiff, TiffTag.IMAGEWIDTH); 785 | var height = TIFFGetField(tiff, TiffTag.IMAGELENGTH); 786 | var bytesPerSample = TIFFGetField(tiff, TiffTag.BITSPERSAMPLE) / 8; 787 | slice_scratch = new Uint8Array(width * height * bytesPerSample); 788 | } 789 | 790 | loadTIFFSlice(tiff, i, slice_scratch); 791 | 792 | TIFFClose(tiff); 793 | FS.unlink("/" + fname); 794 | 795 | numLoaded += 1; 796 | if (numLoaded == files.length) { 797 | var end = performance.now(); 798 | volumeLoaded = true; 799 | newVolumeUpload = true; 800 | loadingProgressText.innerHTML = "Loaded Volume"; 801 | loadingProgressBar.setAttribute("style", "width: 101%"); 802 | console.log(`Loading TIFF took ${end - start}ms`); 803 | } else { 804 | var percent = numLoaded / files.length * 100; 805 | loadingProgressBar.setAttribute("style", "width: " + percent.toFixed(2) + "%"); 806 | // We serialize the loading somewhat to not overload the browser when 807 | // trying to upload a lot of files 808 | loadFile(i + 1); 809 | } 810 | } else { 811 | alert("Unable to load file " + file.name); 812 | } 813 | }; 814 | reader.readAsArrayBuffer(file); 815 | }; 816 | 817 | // First we need to load the first file to get the width, height and color format info 818 | var reader = new FileReader(); 819 | reader.onerror = function() { 820 | alert("Error reading TIFF file " + files[0].name); 821 | }; 822 | reader.onload = function(evt) { 823 | var buf = reader.result; 824 | if (buf) { 825 | FS.createDataFile("/", "temp_test.tiff", new Uint8Array(reader.result), true, false); 826 | var tiff = TIFFOpen("temp_test.tiff", "r"); 827 | 828 | var numDirectories = 0; 829 | if (!TIFFLastDirectory(tiff)) { 830 | do { 831 | ++numDirectories; 832 | } while (TIFFReadDirectory(tiff)); 833 | TIFFSetDirectory(tiff, 0); 834 | } 835 | 836 | // We only support single channel images 837 | if (TIFFGetField(tiff, TiffTag.SAMPLESPERPIXEL) != 1) { 838 | alert("Only single channel images are supported"); 839 | return; 840 | } 841 | 842 | var width = TIFFGetField(tiff, TiffTag.IMAGEWIDTH); 843 | var height = TIFFGetField(tiff, TiffTag.IMAGELENGTH); 844 | volDims = [width, height, 0]; 845 | if (numDirectories != 0) { 846 | volDims[2] = numDirectories; 847 | 848 | document.getElementById("volumeName").innerHTML = 849 | "Volume: Multi-page '" + files[0].name + "', " + numDirectories + " pages"; 850 | } else { 851 | volDims[2] = files.length; 852 | document.getElementById("volumeName").innerHTML = 853 | "Volume: Stack '" + files[0].name + "', " + files.length + " slices"; 854 | } 855 | 856 | makeTIFFGLVolume(tiff); 857 | 858 | if (numDirectories == 0) { 859 | TIFFClose(tiff); 860 | FS.unlink("/temp_test.tiff"); 861 | loadFile(0); 862 | } else { 863 | loadMultipageTiff(tiff, numDirectories); 864 | TIFFClose(tiff); 865 | FS.unlink("/temp_test.tiff"); 866 | } 867 | } else { 868 | alert("Unable to load file " + file.name); 869 | } 870 | }; 871 | reader.readAsArrayBuffer(files[0]); 872 | } 873 | 874 | var fetchTIFF = function() { 875 | var showURL = document.getElementById("shareURL").setAttribute("style", "display:none"); 876 | var url = document.getElementById("fetchTIFF").value; 877 | voxelSpacingFromURL = false; 878 | fetchTIFFURL(url); 879 | } 880 | 881 | var fetchTIFFURL = function(url) { 882 | volumeURL = url; 883 | volumeLoaded = false; 884 | 885 | // Users will paste the shared URL from Dropbox or Google Drive 886 | // so we need to change it to the direct URL to fetch from 887 | var dropboxRegex = /.*dropbox.com\/s\/([^?]+)/ 888 | var googleDriveRegex = /.*drive.google.com.*id=([^&]+)/ 889 | var m = url.match(dropboxRegex); 890 | if (m) { 891 | url = "https://www.dl.dropboxusercontent.com/s/" + m[1] + "?dl=1"; 892 | } else { 893 | m = url.match(googleDriveRegex); 894 | if (m) { 895 | var GOOGLE_DRIVE_API_KEY = ""; 896 | url = "https://www.googleapis.com/drive/v3/files/" + m[1] + 897 | "?alt=media&key=" + GOOGLE_DRIVE_API_KEY; 898 | } else { 899 | alert("Unsupported/handled URL: " + url); 900 | return; 901 | } 902 | } 903 | 904 | var req = new XMLHttpRequest(); 905 | 906 | loadingProgressText.innerHTML = "Loading Volume"; 907 | loadingProgressBar.setAttribute("style", "width: 0%"); 908 | 909 | req.open("GET", url, true); 910 | req.responseType = "arraybuffer"; 911 | req.onerror = function(evt) { 912 | loadingProgressText.innerHTML = "Error Loading Volume: Does your resource support CORS?"; 913 | loadingProgressBar.setAttribute("style", "width: 0%"); 914 | alert("Failed to load volume at " + url + ". Does the resource support CORS?"); 915 | }; 916 | req.onload = function(evt) { 917 | loadingProgressText.innerHTML = "Fetched Volume"; 918 | loadingProgressBar.setAttribute("style", "width: 50%"); 919 | var dataBuffer = req.response; 920 | if (req.status == 200 && dataBuffer) { 921 | FS.createDataFile("/", "remote_fetch.tiff", new Uint8Array(dataBuffer), true, false); 922 | var tiff = TIFFOpen("remote_fetch.tiff", "r"); 923 | 924 | var numDirectories = 0; 925 | if (!TIFFLastDirectory(tiff)) { 926 | do { 927 | ++numDirectories; 928 | } while (TIFFReadDirectory(tiff)); 929 | TIFFSetDirectory(tiff, 0); 930 | } 931 | 932 | // We only support single channel images 933 | if (TIFFGetField(tiff, TiffTag.SAMPLESPERPIXEL) != 1) { 934 | alert("Only single channel images are supported"); 935 | } else if (numDirectories == 0) { 936 | alert("Only multi-page TIFFs are supported via remote fetch"); 937 | } else { 938 | var width = TIFFGetField(tiff, TiffTag.IMAGEWIDTH); 939 | var height = TIFFGetField(tiff, TiffTag.IMAGELENGTH); 940 | volDims = [width, height, numDirectories]; 941 | document.getElementById("volumeName").innerHTML = 942 | "Volume: Multi-page '" + volumeURL + "', " + numDirectories + " pages"; 943 | 944 | makeTIFFGLVolume(tiff); 945 | 946 | loadMultipageTiff(tiff, numDirectories); 947 | } 948 | TIFFClose(tiff); 949 | FS.unlink("/remote_fetch.tiff"); 950 | } else { 951 | alert("Unable to load TIFF from remote URL"); 952 | } 953 | }; 954 | req.send(); 955 | } 956 | 957 | // Load up the SWC files the user gave us 958 | var uploadSWC = function() { 959 | var files = document.getElementById("uploadSWC").files; 960 | var swcList = document.getElementById("swcList"); 961 | // Javascript is a mess... 962 | var loadFile = function(i) { 963 | var file = files[i]; 964 | var reader = new FileReader(); 965 | reader.onerror = function() { 966 | alert("Error reading file " + file.name); 967 | }; 968 | reader.onload = function(evt) { 969 | var text = reader.result; 970 | if (text) { 971 | var swc = new SWCTree(text, file.name); 972 | addSWCFile(swc); 973 | } else { 974 | alert("Unable to load file " + file.name); 975 | } 976 | }; 977 | reader.readAsText(file); 978 | }; 979 | 980 | for (var i = 0; i < files.length; ++i) { 981 | loadFile(i); 982 | } 983 | } 984 | 985 | var loadReference = function() { 986 | var referenceTraces = [ 987 | "NC_01.swc", 988 | "NC_02.swc", 989 | "NC_03.swc", 990 | "NC_04.swc", 991 | "NC_05.swc", 992 | "NC_06.swc", 993 | "NC_07.swc", 994 | "NC_08.swc", 995 | "NC_09.swc", 996 | "NC_10.swc", 997 | "NC_11.swc", 998 | "NC_12.swc", 999 | "NC_13.swc", 1000 | "NC_14.swc", 1001 | "NC_15.swc", 1002 | "NC_16.swc", 1003 | "NC_17.swc", 1004 | "NC_18.swc", 1005 | "NC_19.swc", 1006 | "NC_20.swc", 1007 | "NC_21.swc", 1008 | "NC_22.swc", 1009 | "NC_23.swc", 1010 | "NC_24.swc", 1011 | "NC_25.swc", 1012 | "NC_26.swc", 1013 | "NC_27.swc", 1014 | "NC_28.swc", 1015 | "NC_29.swc", 1016 | "NC_30.swc", 1017 | "NC_31.swc", 1018 | "NC_32.swc", 1019 | "NC_33.swc", 1020 | "NC_34.swc" 1021 | ]; 1022 | var swcFileRegex = /(\w+).swc.*/; 1023 | 1024 | // Javascript is a mess... 1025 | var launcReq = function(i) { 1026 | var file = referenceTraces[i]; 1027 | var url = "https://cdn.willusher.io/webgl-neuron-data/" + file; 1028 | var req = new XMLHttpRequest(); 1029 | 1030 | req.open("GET", url, true); 1031 | req.responseType = "text"; 1032 | req.onerror = function(evt) { 1033 | alert("Failed to load reference trace from " + url); 1034 | }; 1035 | req.onload = function(evt) { 1036 | var text = req.response; 1037 | if (text) { 1038 | var m = req.responseURL.match(swcFileRegex); 1039 | var swc = new SWCTree(text, m[1]); 1040 | addSWCFile(swc); 1041 | } else { 1042 | alert("Unable to load reference trace from " + url); 1043 | } 1044 | }; 1045 | req.send(); 1046 | }; 1047 | 1048 | for (var i = 0; i < referenceTraces.length; ++i) { 1049 | launcReq(i); 1050 | } 1051 | } 1052 | 1053 | var colorBrewerColors = [ 1054 | "#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", 1055 | "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a" 1056 | ]; 1057 | /* 1058 | var colorBrewerColors = [ 1059 | "#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", 1060 | "#ffd92f", "#e5c494", "#b3b3b3" 1061 | ]; 1062 | */ 1063 | var nextSWCColor = 0; 1064 | 1065 | var swcSelectionChanged = function() { 1066 | // Find the two which are selected 1067 | var a = -1; 1068 | var b = -1; 1069 | var warned = false; 1070 | for (var i = 0; i < neurons.length; ++i) { 1071 | if (neurons[i].selected.checked) { 1072 | if (a == -1) { 1073 | a = i; 1074 | } else if (b == -1) { 1075 | b = i; 1076 | } else { 1077 | neurons[i].selected.checked = false; 1078 | if (!warned) { 1079 | warned = true; 1080 | alert("Only two neurons can be compared, automatically deselecting others"); 1081 | } 1082 | } 1083 | } 1084 | } 1085 | 1086 | if (a != -1 && b != -1) { 1087 | var diff_segments = computeTreeDifferences(neurons[a], neurons[b]); 1088 | num_diff_segment_vertices = diff_segments.length / 3; 1089 | 1090 | if (!diff_segments_vao) { 1091 | diff_segments_vao = gl.createVertexArray(); 1092 | } 1093 | gl.bindVertexArray(diff_segments_vao); 1094 | 1095 | if (!diff_segments_vbo) { 1096 | diff_segments_vbo = gl.createBuffer(); 1097 | } 1098 | gl.bindBuffer(gl.ARRAY_BUFFER, diff_segments_vbo); 1099 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(diff_segments), gl.STATIC_DRAW); 1100 | gl.enableVertexAttribArray(0); 1101 | gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); 1102 | } else { 1103 | num_diff_segment_vertices = 0; 1104 | } 1105 | } 1106 | 1107 | var addSWCFile = function(swc) { 1108 | var swcHTMLContent = ` 1109 |
1110 |
1111 |
1112 | ${swc.name} 1113 |
1114 |
1115 | ${swc.numSoma} 1116 |
1117 |
1118 | ${swc.branches.length} 1119 |
1120 |
1121 | ${swc.points.length / 3} 1122 |
1123 |
1124 |
1125 |
1126 | 1127 | 1129 |
1130 |
1131 | 1132 | 1134 |
1135 |
1136 | 1137 | 1138 |
1139 |
1140 |
1141 |
1142 | ` 1143 | swcList.insertAdjacentHTML("beforeend", swcHTMLContent); 1144 | 1145 | swc.visible = document.getElementById("traceVisible" + neurons.length); 1146 | swc.visible.checked = true; 1147 | swc.selected = document.getElementById("traceSelected" + neurons.length); 1148 | swc.selected.checked = false; 1149 | swc.selected.addEventListener("change", swcSelectionChanged); 1150 | swc.color = document.getElementById("swcColor" + neurons.length); 1151 | swc.color.value = colorBrewerColors[nextSWCColor]; 1152 | nextSWCColor = (nextSWCColor + 1) % colorBrewerColors.length; 1153 | 1154 | neurons.push(swc); 1155 | } 1156 | 1157 | -------------------------------------------------------------------------------- /js/shader-srcs.js: -------------------------------------------------------------------------------- 1 | var vertShader = 2 | `#version 300 es 3 | #line 4 4 | layout(location=0) in vec3 pos; 5 | uniform mat4 proj_view; 6 | uniform vec3 eye_pos; 7 | uniform vec3 volume_scale; 8 | 9 | out vec3 vray_dir; 10 | flat out vec3 transformed_eye; 11 | 12 | void main(void) { 13 | // TODO: For non-uniform size volumes we need to transform them differently as well 14 | // to center them properly 15 | vec3 volume_translation = vec3(0.5) - volume_scale * 0.5; 16 | gl_Position = proj_view * vec4(pos * volume_scale + volume_translation, 1); 17 | transformed_eye = (eye_pos - volume_translation) / volume_scale; 18 | vray_dir = pos - transformed_eye; 19 | }`; 20 | 21 | var fragShader = 22 | `#version 300 es 23 | #line 24 24 | precision highp int; 25 | precision highp float; 26 | uniform highp sampler3D volume; 27 | uniform highp usampler3D ivolume; 28 | uniform highp sampler2D colormap; 29 | uniform highp sampler2D depth; 30 | uniform vec2 value_range; 31 | uniform ivec3 volume_dims; 32 | uniform vec3 eye_pos; 33 | uniform vec3 volume_scale; 34 | uniform float dt_scale; 35 | uniform mat4 inv_proj; 36 | uniform mat4 inv_view; 37 | uniform int highlight_trace; 38 | uniform float threshold; 39 | uniform float saturation_threshold; 40 | uniform int volume_is_int; 41 | uniform ivec2 canvas_dims; 42 | 43 | in vec3 vray_dir; 44 | flat in vec3 transformed_eye; 45 | out vec4 color; 46 | 47 | vec2 intersect_box(vec3 orig, vec3 dir) { 48 | const vec3 box_min = vec3(0); 49 | const vec3 box_max = vec3(1); 50 | vec3 inv_dir = 1.0 / dir; 51 | vec3 tmin_tmp = (box_min - orig) * inv_dir; 52 | vec3 tmax_tmp = (box_max - orig) * inv_dir; 53 | vec3 tmin = min(tmin_tmp, tmax_tmp); 54 | vec3 tmax = max(tmin_tmp, tmax_tmp); 55 | float t0 = max(tmin.x, max(tmin.y, tmin.z)); 56 | float t1 = min(tmax.x, min(tmax.y, tmax.z)); 57 | return vec2(t0, t1); 58 | } 59 | 60 | // Pseudo-random number gen from 61 | // http://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/ 62 | // with some tweaks for the range of values 63 | float wang_hash(int seed) { 64 | seed = (seed ^ 61) ^ (seed >> 16); 65 | seed *= 9; 66 | seed = seed ^ (seed >> 4); 67 | seed *= 0x27d4eb2d; 68 | seed = seed ^ (seed >> 15); 69 | return float(seed % 2147483647) / float(2147483647); 70 | } 71 | 72 | // Linearize the depth value passed in 73 | float linearize(float d) { 74 | float near = 0.0; 75 | float far = 1.0; 76 | return (2.f * d - near - far) / (far - near); 77 | } 78 | 79 | // Reconstruct the view-space position 80 | vec4 compute_view_pos(float z) { 81 | // TODO: We don't really care about the full view position here 82 | vec4 pos = vec4(gl_FragCoord.xy / vec2(canvas_dims) * 2.f - 1.f, z, 1.f); 83 | pos = inv_proj * pos; 84 | return pos / pos.w; 85 | } 86 | 87 | void main(void) { 88 | vec3 ray_dir = normalize(vray_dir); 89 | vec2 t_hit = intersect_box(transformed_eye, ray_dir); 90 | if (t_hit.x > t_hit.y) { 91 | discard; 92 | } 93 | 94 | t_hit.x = max(t_hit.x, 0.0); 95 | 96 | float z = linearize(texelFetch(depth, ivec2(gl_FragCoord), 0).x); 97 | if (z < 1.0) { 98 | vec3 volume_translation = vec3(0.5) - volume_scale * 0.5; 99 | vec3 geom_pos = (inv_view * compute_view_pos(z)).xyz; 100 | geom_pos = (geom_pos - volume_translation) / volume_scale; 101 | t_hit.y = min(length(geom_pos - transformed_eye), t_hit.y); 102 | 103 | // Highlighting the trace just skips properly compositing it in the volume 104 | // to always show it on top 105 | if (highlight_trace != 0) { 106 | color = vec4(0); 107 | return; 108 | } 109 | } 110 | 111 | vec3 dt_vec = 1.0 / (vec3(volume_dims) * abs(ray_dir)); 112 | float dt = dt_scale * min(dt_vec.x, min(dt_vec.y, dt_vec.z)); 113 | float offset = wang_hash(int(gl_FragCoord.x + float(canvas_dims.x) * gl_FragCoord.y)); 114 | vec3 p = transformed_eye + (t_hit.x + offset * dt) * ray_dir; 115 | for (float t = t_hit.x; t < t_hit.y; t += dt) { 116 | float val = 0.0; 117 | if (volume_is_int == 0) { 118 | val = texture(volume, p).r; 119 | } else { 120 | val = float(texture(ivolume, p).r); 121 | } 122 | val = (val - value_range.x) / (value_range.y - value_range.x); 123 | 124 | if (val >= saturation_threshold) { 125 | val = 1.0; 126 | } 127 | 128 | if (val >= threshold) { 129 | val = clamp((val - threshold) / (1.0 - threshold), 0.0, 1.0); 130 | vec4 val_color = vec4(texture(colormap, vec2(val, 0.5)).rgb, val); 131 | // Opacity correction 132 | val_color.a = 1.0 - pow(1.0 - val_color.a, dt_scale); 133 | color.rgb += (1.0 - color.a) * val_color.a * val_color.rgb; 134 | color.a += (1.0 - color.a) * val_color.a; 135 | if (color.a >= 0.95) { 136 | break; 137 | } 138 | } 139 | p += ray_dir * dt; 140 | } 141 | }`; 142 | 143 | var swcVertShader = 144 | `#version 300 es 145 | #line 146 146 | layout(location=0) in vec3 pos; 147 | 148 | uniform mat4 proj_view; 149 | uniform vec3 volume_scale; 150 | uniform ivec3 volume_dims; 151 | 152 | out vec3 vol_pos; 153 | 154 | void main(void) { 155 | vec3 volume_translation = vec3(0.5) - volume_scale * 0.5; 156 | vol_pos = (pos / vec3(volume_dims)); 157 | gl_Position = proj_view * vec4(vol_pos * volume_scale + volume_translation, 1.0); 158 | }`; 159 | 160 | var swcFragShader = 161 | `#version 300 es 162 | #line 163 163 | precision highp float; 164 | 165 | uniform highp sampler3D volume; 166 | uniform highp usampler3D ivolume; 167 | uniform vec2 value_range; 168 | uniform int volume_is_int; 169 | uniform int highlight_errors; 170 | 171 | uniform vec3 swc_color; 172 | 173 | in vec3 vol_pos; 174 | out vec4 color; 175 | 176 | void main(void) { 177 | float val = 0.0; 178 | if (highlight_errors != 0) { 179 | if (volume_is_int == 0) { 180 | val = texture(volume, vol_pos).r; 181 | } else { 182 | val = float(texture(ivolume, vol_pos).r); 183 | } 184 | val = 1.f - (val - value_range.x) / (value_range.y - value_range.x); 185 | const float error_threshold = 0.2f; 186 | val = clamp(val - (1.f - error_threshold), 0.f, error_threshold) / error_threshold; 187 | } 188 | color = vec4(mix(swc_color, vec3(1, 0, 0), val), 1); 189 | }`; 190 | 191 | var quadVertShader = 192 | `#version 300 es 193 | #line 194 194 | const vec4 pos[4] = vec4[4]( 195 | vec4(-1, 1, 0.5, 1), 196 | vec4(-1, -1, 0.5, 1), 197 | vec4(1, 1, 0.5, 1), 198 | vec4(1, -1, 0.5, 1) 199 | ); 200 | void main(void){ 201 | gl_Position = pos[gl_VertexID]; 202 | }`; 203 | 204 | var quadFragShader = 205 | `#version 300 es 206 | #line 207 207 | precision highp int; 208 | precision highp float; 209 | 210 | uniform sampler2D colors; 211 | out vec4 color; 212 | 213 | float linear_to_srgb(float x) { 214 | if (x <= 0.0031308f) { 215 | return 12.92f * x; 216 | } 217 | return 1.055f * pow(x, 1.f / 2.4f) - 0.055f; 218 | } 219 | 220 | void main(void){ 221 | ivec2 uv = ivec2(gl_FragCoord.xy); 222 | color = texelFetch(colors, uv, 0); 223 | color.x = linear_to_srgb(color.x); 224 | color.y = linear_to_srgb(color.y); 225 | color.z = linear_to_srgb(color.z); 226 | }`; 227 | 228 | -------------------------------------------------------------------------------- /js/swc.js: -------------------------------------------------------------------------------- 1 | // Load the SWC Tree structure from the swc file passed 2 | var SWCTree = function(swcFile, name) { 3 | this.name = name; 4 | this.branches = []; 5 | this.indices = [] 6 | this.points = [] 7 | this.numSoma = 0; 8 | 9 | this.vao = null; 10 | this.vbo = null; 11 | this.ebo = null; 12 | 13 | var branch = null; 14 | var lines = swcFile.split("\n"); 15 | for (var i = 0; i < lines.length; ++i) { 16 | if (lines[i][0] == "#") { 17 | continue; 18 | } 19 | if (lines[i].length == 0 && branch != null) { 20 | branch["count"] = this.indices.length - branch["start"]; 21 | this.branches.push(branch); 22 | branch = null; 23 | continue; 24 | } 25 | var vals = lines[i].trim().split(" "); 26 | var id = parseInt(vals[0]); 27 | var x = parseFloat(vals[2]); 28 | var y = parseFloat(vals[3]); 29 | var z = parseFloat(vals[4]); 30 | var parentID = parseInt(vals[6]); 31 | if (id == 1) { 32 | branch = { "start": 0 }; 33 | this.indices.push(0); 34 | this.numSoma = 1; 35 | } else if (parentID != id - 1 || parentID == -1) { 36 | branch["count"] = this.indices.length - branch["start"]; 37 | this.branches.push(branch); 38 | 39 | branch = { "start": this.indices.length }; 40 | // IDs in the file start at 1 41 | if (parentID != -1) { 42 | this.indices.push(parentID - 1); 43 | } else { 44 | this.numSoma += 1; 45 | } 46 | this.indices.push(this.points.length / 3); 47 | } else { 48 | this.indices.push(this.points.length / 3); 49 | } 50 | this.points.push(x); 51 | this.points.push(y); 52 | this.points.push(z); 53 | } 54 | if (branch) { 55 | branch["count"] = this.indices.length - branch["start"]; 56 | this.branches.push(branch); 57 | } 58 | } 59 | 60 | var distance = function(a, b) { 61 | return Math.sqrt(Math.pow(a[0] - b[0], 2.0) + Math.pow(a[1] - b[1], 2.0) + Math.pow(a[2] - b[2], 2.0)); 62 | } 63 | 64 | // Compute the difference lines between the two trees to draw as line segments 65 | // to visualize the difference between the two trees 66 | var computeTreeDifferences = function(a, b) { 67 | var segments = []; 68 | 69 | // TODO: Is it worth building a k-d tree to accelerate the queries? Most of the SWCs 70 | // we're comparing should be pretty small 71 | for (var i = 0; i < a.points.length / 3; ++i) { 72 | var pa = [a.points[i * 3], a.points[i * 3 + 1], a.points[i * 3 + 2]]; 73 | 74 | var dist = Number.POSITIVE_INFINITY; 75 | var nearest_pt = -1; 76 | for (var j = 0; j < b.points.length / 3; ++j) { 77 | var pb = [b.points[j * 3], b.points[j * 3 + 1], b.points[j * 3 + 2]]; 78 | var d = distance(pa, pb); 79 | if (d < dist) { 80 | dist = d; 81 | nearest_pt = j; 82 | } 83 | } 84 | var pb = [b.points[nearest_pt * 3], b.points[nearest_pt * 3 + 1], b.points[nearest_pt * 3 + 2]]; 85 | segments = segments.concat(pa).concat(pb); 86 | } 87 | 88 | for (var i = 0; i < b.points.length / 3; ++i) { 89 | var pb = [b.points[i * 3], b.points[i * 3 + 1], b.points[i * 3 + 2]]; 90 | 91 | var dist = Number.POSITIVE_INFINITY; 92 | var nearest_pt = -1; 93 | for (var j = 0; j < a.points.length / 3; ++j) { 94 | var pa = [a.points[j * 3], a.points[j * 3 + 1], a.points[j * 3 + 2]]; 95 | var d = distance(pa, pb); 96 | if (d < dist) { 97 | dist = d; 98 | nearest_pt = j; 99 | } 100 | } 101 | var pa = [a.points[nearest_pt * 3], a.points[nearest_pt * 3 + 1], a.points[nearest_pt * 3 + 2]]; 102 | segments = segments.concat(pa).concat(pb); 103 | } 104 | return segments; 105 | } 106 | 107 | -------------------------------------------------------------------------------- /js/tiff.js: -------------------------------------------------------------------------------- 1 | var TiffTag = { 2 | SUBFILETYPE: 254, 3 | OSUBFILETYPE: 255, 4 | IMAGEWIDTH: 256, 5 | IMAGELENGTH: 257, 6 | BITSPERSAMPLE: 258, 7 | COMPRESSION: 259, 8 | PHOTOMETRIC: 262, 9 | THRESHHOLDING: 263, 10 | CELLWIDTH: 264, 11 | CELLLENGTH: 265, 12 | FILLORDER: 266, 13 | DOCUMENTNAME: 269, 14 | IMAGEDESCRIPTION: 270, 15 | MAKE: 271, 16 | MODEL: 272, 17 | STRIPOFFSETS: 273, 18 | ORIENTATION: 274, 19 | SAMPLESPERPIXEL: 277, 20 | ROWSPERSTRIP: 278, 21 | STRIPBYTECOUNTS: 279, 22 | MINSAMPLEVALUE: 280, 23 | MAXSAMPLEVALUE: 281, 24 | XRESOLUTION: 282, 25 | YRESOLUTION: 283, 26 | PLANARCONFIG: 284, 27 | PAGENAME: 285, 28 | XPOSITION: 286, 29 | YPOSITION: 287, 30 | FREEOFFSETS: 288, 31 | FREEBYTECOUNTS: 289, 32 | GRAYRESPONSEUNIT: 290, 33 | GRAYRESPONSECURVE: 291, 34 | RESOLUTIONUNIT: 296, 35 | PAGENUMBER: 297, 36 | COLORRESPONSEUNIT: 300, 37 | TRANSFERFUNCTION: 301, 38 | SOFTWARE: 305, 39 | DATETIME: 306, 40 | ARTIST: 315, 41 | HOSTCOMPUTER: 316, 42 | PREDICTOR: 317, 43 | WHITEPOINT: 318, 44 | PRIMARYCHROMATICITIES: 319, 45 | COLORMAP: 320, 46 | HALFTONEHINTS: 321, 47 | TILEWIDTH: 322, 48 | TILELENGTH: 323, 49 | TILEOFFSETS: 324, 50 | TILEBYTECOUNTS: 325, 51 | BADFAXLINES: 326, 52 | CLEANFAXDATA: 327, 53 | CONSECUTIVEBADFAXLINES: 328, 54 | SUBIFD: 330, 55 | INKSET: 332, 56 | INKNAMES: 333, 57 | NUMBEROFINKS: 334, 58 | DOTRANGE: 336, 59 | TARGETPRINTER: 337, 60 | EXTRASAMPLES: 338, 61 | SAMPLEFORMAT: 339, 62 | SMINSAMPLEVALUE: 340, 63 | SMAXSAMPLEVALUE: 341, 64 | CLIPPATH: 343, 65 | XCLIPPATHUNITS: 344, 66 | YCLIPPATHUNITS: 345, 67 | INDEXED: 346, 68 | JPEGTABLES: 347, 69 | OPIPROXY: 351, 70 | GLOBALPARAMETERSIFD: 400, 71 | PROFILETYPE: 401, 72 | FAXPROFILE: 402, 73 | CODINGMETHODS: 403, 74 | VERSIONYEAR: 404, 75 | MODENUMBER: 405, 76 | DECODE: 433, 77 | IMAGEBASECOLOR: 434, 78 | JPEGPROC: 512, 79 | JPEGIFOFFSET: 513, 80 | JPEGIFBYTECOUNT: 514, 81 | JPEGRESTARTINTERVAL: 515, 82 | JPEGLOSSLESSPREDICTORS: 517, 83 | JPEGPOINTTRANSFORM: 518, 84 | JPEGQTABLES: 519, 85 | JPEGDCTABLES: 520, 86 | JPEGACTABLES: 521, 87 | YCBCRCOEFFICIENTS: 529, 88 | YCBCRSUBSAMPLING: 530, 89 | YCBCRPOSITIONING: 531, 90 | REFERENCEBLACKWHITE: 532, 91 | STRIPROWCOUNTS: 559, 92 | XMLPACKET: 700, 93 | OPIIMAGEID: 32781, 94 | REFPTS: 32953, 95 | REGIONTACKPOINT: 32954, 96 | REGIONWARPCORNERS: 32955, 97 | REGIONAFFINE: 32956, 98 | MATTEING: 32995, 99 | DATATYPE: 32996, 100 | IMAGEDEPTH: 32997, 101 | TILEDEPTH: 32998, 102 | PIXAR_IMAGEFULLWIDTH: 33300, 103 | PIXAR_IMAGEFULLLENGTH: 33301, 104 | PIXAR_TEXTUREFORMAT: 33302, 105 | PIXAR_WRAPMODES: 33303, 106 | PIXAR_FOVCOT: 33304, 107 | PIXAR_MATRIX_WORLDTOSCREEN: 33305, 108 | PIXAR_MATRIX_WORLDTOCAMERA: 33306, 109 | WRITERSERIALNUMBER: 33405, 110 | CFAREPEATPATTERNDIM: 33421, 111 | CFAPATTERN: 33422, 112 | COPYRIGHT: 33432, 113 | RICHTIFFIPTC: 33723, 114 | FRAMECOUNT: 34232, 115 | PHOTOSHOP: 34377, 116 | EXIFIFD: 34665, 117 | ICCPROFILE: 34675, 118 | IMAGELAYER: 34732, 119 | JBIGOPTIONS: 34750, 120 | GPSIFD: 34853, 121 | FAXRECVPARAMS: 34908, 122 | FAXSUBADDRESS: 34909, 123 | FAXRECVTIME: 34910, 124 | FAXDCS: 34911, 125 | STONITS: 37439, 126 | FEDEX_EDR: 34929, 127 | INTEROPERABILITYIFD: 40965, 128 | LERC_PARAMETERS: 50674, 129 | DNGVERSION: 50706, 130 | DNGBACKWARDVERSION: 50707, 131 | UNIQUECAMERAMODEL: 50708, 132 | LOCALIZEDCAMERAMODEL: 50709, 133 | CFAPLANECOLOR: 50710, 134 | CFALAYOUT: 50711, 135 | LINEARIZATIONTABLE: 50712, 136 | BLACKLEVELREPEATDIM: 50713, 137 | BLACKLEVEL: 50714, 138 | BLACKLEVELDELTAH: 50715, 139 | BLACKLEVELDELTAV: 50716, 140 | WHITELEVEL: 50717, 141 | DEFAULTSCALE: 50718, 142 | DEFAULTCROPORIGIN: 50719, 143 | DEFAULTCROPSIZE: 50720, 144 | ANALOGBALANCE: 50727, 145 | ASSHOTNEUTRAL: 50728, 146 | ASSHOTWHITEXY: 50729, 147 | BASELINEEXPOSURE: 50730, 148 | BASELINENOISE: 50731, 149 | BASELINESHARPNESS: 50732, 150 | BAYERGREENSPLIT: 50733, 151 | LINEARRESPONSELIMIT: 50734, 152 | CAMERASERIALNUMBER: 50735, 153 | LENSINFO: 50736, 154 | CHROMABLURRADIUS: 50737, 155 | ANTIALIASSTRENGTH: 50738, 156 | SHADOWSCALE: 50739, 157 | DNGPRIVATEDATA: 50740, 158 | MAKERNOTESAFETY: 50741, 159 | BESTQUALITYSCALE: 50780, 160 | RAWDATAUNIQUEID: 50781, 161 | ORIGINALRAWFILENAME: 50827, 162 | ORIGINALRAWFILEDATA: 50828, 163 | ACTIVEAREA: 50829, 164 | MASKEDAREAS: 50830, 165 | ASSHOTICCPROFILE: 50831, 166 | ASSHOTPREPROFILEMATRIX: 50832, 167 | CURRENTICCPROFILE: 50833, 168 | CURRENTPREPROFILEMATRIX: 50834, 169 | DCSHUESHIFTVALUES: 65535, 170 | FAXMODE: 65536, 171 | JPEGQUALITY: 65537, 172 | JPEGCOLORMODE: 65538, 173 | JPEGTABLESMODE: 65539, 174 | FAXFILLFUNC: 65540, 175 | PIXARLOGDATAFMT: 65549, 176 | DCSIMAGERTYPE: 65550, 177 | DCSINTERPMODE: 65551, 178 | DCSBALANCEARRAY: 65552, 179 | DCSCORRECTMATRIX: 65553, 180 | DCSGAMMA: 65554, 181 | DCSTOESHOULDERPTS: 65555, 182 | DCSCALIBRATIONFD: 65556, 183 | ZIPQUALITY: 65557, 184 | PIXARLOGQUALITY: 65558, 185 | DCSCLIPRECTANGLE: 65559, 186 | SGILOGDATAFMT: 65560, 187 | SGILOGENCODE: 65561, 188 | LZMAPRESET: 65562, 189 | PERSAMPLE: 65563, 190 | ZSTD_LEVEL: 65564, 191 | LERC_VERSION: 65565, 192 | LERC_ADD_COMPRESSION: 65566, 193 | LERC_MAXZERROR: 65567, 194 | WEBP_LEVEL: 65568, 195 | WEBP_LOSSLESS: 65569 196 | }; 197 | 198 | var TiffSampleFormat = { 199 | UNSPECIFIED: 0, 200 | UINT: 1, 201 | INT: 2, 202 | IEEEFP: 3, 203 | VOID: 4, 204 | COMPLEXINT: 5, 205 | COMPLEXIEEEFP: 6 206 | }; 207 | 208 | var TIFFOpen = Module.cwrap("TIFFOpen", "number", ["string", "string"]); 209 | 210 | var TIFFClose = Module.cwrap("TIFFClose", "number", ["number"]); 211 | 212 | var TIFFNumberOfStrips = Module.cwrap("TIFFNumberOfStrips", "number", ["number"]); 213 | 214 | var TIFFStripSize = Module.cwrap("TIFFStripSize", "number", ["number"]); 215 | 216 | var TIFFReadEncodedStrip = Module.cwrap("TIFFReadEncodedStrip", "number", 217 | ["number", "number", "number", "number"]); 218 | 219 | var TIFFMalloc = Module.cwrap("_TIFFmalloc", "number", ["number"]); 220 | 221 | var TIFFFree = Module.cwrap("_TIFFfree", "number", ["number"]); 222 | 223 | var TIFFGetField = Module.cwrap("GetField", "number", ["number", "number"]); 224 | 225 | var TIFFLastDirectory = Module.cwrap("LastDirectory", "number", ["number"]); 226 | 227 | var TIFFReadDirectory = Module.cwrap("ReadDirectory", "number", ["number"]); 228 | 229 | var TIFFSetDirectory = Module.cwrap("SetDirectory", "number", ["number", "number"]); 230 | 231 | var TIFFGetStringField = Module.cwrap("GetStringField", "string", ["number", "number"]); 232 | 233 | -------------------------------------------------------------------------------- /js/tiff.raw.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgl-neuron/3ac30115aac995e38cd76b560fee4d010dd3380b/js/tiff.raw.wasm -------------------------------------------------------------------------------- /js/webgl-util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Compute the view frustum in world space from the provided 3 | // column major projection * view matrix 4 | var Frustum = function(projView) { 5 | var rows = [vec4.create(), vec4.create(), vec4.create(), vec4.create()]; 6 | for (var i = 0; i < rows.length; ++i) { 7 | rows[i] = vec4.set(rows[i], projView[i], projView[4 + i], 8 | projView[8 + i], projView[12 + i]); 9 | } 10 | 11 | this.planes = [ 12 | // -x plane 13 | vec4.add(vec4.create(), rows[3], rows[0]), 14 | // +x plane 15 | vec4.sub(vec4.create(), rows[3], rows[0]), 16 | // -y plane 17 | vec4.add(vec4.create(), rows[3], rows[1]), 18 | // +y plane 19 | vec4.sub(vec4.create(), rows[3], rows[1]), 20 | // -z plane 21 | vec4.add(vec4.create(), rows[3], rows[2]), 22 | // +z plane 23 | vec4.sub(vec4.create(), rows[3], rows[2]) 24 | ]; 25 | 26 | // Normalize the planes 27 | for (var i = 0; i < this.planes.length; ++i) { 28 | var s = 1.0 / Math.sqrt(this.planes[i][0] * this.planes[i][0] + 29 | this.planes[i][1] * this.planes[i][1] + this.planes[i][2] * this.planes[i][2]); 30 | this.planes[i][0] *= s; 31 | this.planes[i][1] *= s; 32 | this.planes[i][2] *= s; 33 | this.planes[i][3] *= s; 34 | } 35 | 36 | // Compute the frustum points as well 37 | var invProjView = mat4.invert(mat4.create(), projView); 38 | this.points = [ 39 | // x_l, y_l, z_l 40 | vec4.set(vec4.create(), -1, -1, -1, 1), 41 | // x_h, y_l, z_l 42 | vec4.set(vec4.create(), 1, -1, -1, 1), 43 | // x_l, y_h, z_l 44 | vec4.set(vec4.create(), -1, 1, -1, 1), 45 | // x_h, y_h, z_l 46 | vec4.set(vec4.create(), 1, 1, -1, 1), 47 | // x_l, y_l, z_h 48 | vec4.set(vec4.create(), -1, -1, 1, 1), 49 | // x_h, y_l, z_h 50 | vec4.set(vec4.create(), 1, -1, 1, 1), 51 | // x_l, y_h, z_h 52 | vec4.set(vec4.create(), -1, 1, 1, 1), 53 | // x_h, y_h, z_h 54 | vec4.set(vec4.create(), 1, 1, 1, 1) 55 | ]; 56 | for (var i = 0; i < 8; ++i) { 57 | this.points[i] = vec4.transformMat4(this.points[i], this.points[i], invProjView); 58 | this.points[i][0] /= this.points[i][3]; 59 | this.points[i][1] /= this.points[i][3]; 60 | this.points[i][2] /= this.points[i][3]; 61 | this.points[i][3] = 1.0; 62 | } 63 | } 64 | 65 | // Check if the box is contained in the Frustum 66 | // The box should be [x_lower, y_lower, z_lower, x_upper, y_upper, z_upper] 67 | // This is done using Inigo Quilez's approach to help with large 68 | // bounds: https://www.iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm 69 | Frustum.prototype.containsBox = function(box) { 70 | // Test the box against each plane 71 | var vec = vec4.create(); 72 | var out = 0; 73 | for (var i = 0; i < this.planes.length; ++i) { 74 | out = 0; 75 | // x_l, y_l, z_l 76 | vec4.set(vec, box[0], box[1], box[2], 1.0); 77 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 78 | // x_h, y_l, z_l 79 | vec4.set(vec, box[3], box[1], box[2], 1.0); 80 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 81 | // x_l, y_h, z_l 82 | vec4.set(vec, box[0], box[4], box[2], 1.0); 83 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 84 | // x_h, y_h, z_l 85 | vec4.set(vec, box[3], box[4], box[2], 1.0); 86 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 87 | // x_l, y_l, z_h 88 | vec4.set(vec, box[0], box[1], box[5], 1.0); 89 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 90 | // x_h, y_l, z_h 91 | vec4.set(vec, box[3], box[1], box[5], 1.0); 92 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 93 | // x_l, y_h, z_h 94 | vec4.set(vec, box[0], box[4], box[5], 1.0); 95 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 96 | // x_h, y_h, z_h 97 | vec4.set(vec, box[3], box[4], box[5], 1.0); 98 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 99 | 100 | if (out == 8) { 101 | return false; 102 | } 103 | } 104 | 105 | // Test the frustum against the box 106 | out = 0; 107 | for (var i = 0; i < 8; ++i) { 108 | out += this.points[i][0] > box[3] ? 1 : 0; 109 | } 110 | if (out == 8) { 111 | return false; 112 | } 113 | 114 | out = 0; 115 | for (var i = 0; i < 8; ++i) { 116 | out += this.points[i][0] < box[0] ? 1 : 0; 117 | } 118 | if (out == 8) { 119 | return false; 120 | } 121 | 122 | out = 0; 123 | for (var i = 0; i < 8; ++i) { 124 | out += this.points[i][1] > box[4] ? 1 : 0; 125 | } 126 | if (out == 8) { 127 | return false; 128 | } 129 | 130 | out = 0; 131 | for (var i = 0; i < 8; ++i) { 132 | out += this.points[i][1] < box[1] ? 1 : 0; 133 | } 134 | if (out == 8) { 135 | return false; 136 | } 137 | 138 | out = 0; 139 | for (var i = 0; i < 8; ++i) { 140 | out += this.points[i][2] > box[5] ? 1 : 0; 141 | } 142 | if (out == 8) { 143 | return false; 144 | } 145 | 146 | out = 0; 147 | for (var i = 0; i < 8; ++i) { 148 | out += this.points[i][2] < box[2] ? 1 : 0; 149 | } 150 | if (out == 8) { 151 | return false; 152 | } 153 | return true; 154 | } 155 | 156 | 157 | var Shader = function(gl, vertexSrc, fragmentSrc) { 158 | var self = this; 159 | this.program = compileShader(gl, vertexSrc, fragmentSrc); 160 | 161 | var regexUniform = /uniform[^;]+[ ](\w+);/g 162 | var matchUniformName = /uniform[^;]+[ ](\w+);/ 163 | 164 | this.uniforms = {}; 165 | 166 | var vertexUnifs = vertexSrc.match(regexUniform); 167 | var fragUnifs = fragmentSrc.match(regexUniform); 168 | 169 | if (vertexUnifs) { 170 | vertexUnifs.forEach(function(unif) { 171 | var m = unif.match(matchUniformName); 172 | self.uniforms[m[1]] = -1; 173 | }); 174 | } 175 | if (fragUnifs) { 176 | fragUnifs.forEach(function(unif) { 177 | var m = unif.match(matchUniformName); 178 | self.uniforms[m[1]] = -1; 179 | }); 180 | } 181 | 182 | for (var unif in this.uniforms) { 183 | this.uniforms[unif] = gl.getUniformLocation(this.program, unif); 184 | } 185 | } 186 | 187 | Shader.prototype.use = function(gl) { 188 | gl.useProgram(this.program); 189 | } 190 | 191 | // Compile and link the shaders vert and frag. vert and frag should contain 192 | // the shader source code for the vertex and fragment shaders respectively 193 | // Returns the compiled and linked program, or null if compilation or linking failed 194 | var compileShader = function(gl, vert, frag){ 195 | var vs = gl.createShader(gl.VERTEX_SHADER); 196 | gl.shaderSource(vs, vert); 197 | gl.compileShader(vs); 198 | if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)){ 199 | alert("Vertex shader failed to compile, see console for log"); 200 | console.log(gl.getShaderInfoLog(vs)); 201 | return null; 202 | } 203 | 204 | var fs = gl.createShader(gl.FRAGMENT_SHADER); 205 | gl.shaderSource(fs, frag); 206 | gl.compileShader(fs); 207 | if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)){ 208 | alert("Fragment shader failed to compile, see console for log"); 209 | console.log(gl.getShaderInfoLog(fs)); 210 | return null; 211 | } 212 | 213 | var program = gl.createProgram(); 214 | gl.attachShader(program, vs); 215 | gl.attachShader(program, fs); 216 | gl.linkProgram(program); 217 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)){ 218 | alert("Shader failed to link, see console for log"); 219 | console.log(gl.getProgramInfoLog(program)); 220 | return null; 221 | } 222 | return program; 223 | } 224 | 225 | var getGLExtension = function(ext) { 226 | if (!gl.getExtension(ext)) { 227 | alert("Missing " + ext + " WebGL extension"); 228 | return false; 229 | } 230 | return true; 231 | } 232 | 233 | /* The arcball camera will be placed at the position 'eye', rotating 234 | * around the point 'center', with the up vector 'up'. 'screenDims' 235 | * should be the dimensions of the canvas or region taking mouse input 236 | * so the mouse positions can be normalized into [-1, 1] from the pixel 237 | * coordinates. 238 | */ 239 | var ArcballCamera = function(eye, center, up, zoomSpeed, screenDims) { 240 | var veye = vec3.set(vec3.create(), eye[0], eye[1], eye[2]); 241 | var vcenter = vec3.set(vec3.create(), center[0], center[1], center[2]); 242 | var vup = vec3.set(vec3.create(), up[0], up[1], up[2]); 243 | vec3.normalize(vup, vup); 244 | 245 | var zAxis = vec3.sub(vec3.create(), vcenter, veye); 246 | var viewDist = vec3.len(zAxis); 247 | vec3.normalize(zAxis, zAxis); 248 | 249 | var xAxis = vec3.cross(vec3.create(), zAxis, vup); 250 | vec3.normalize(xAxis, xAxis); 251 | 252 | var yAxis = vec3.cross(vec3.create(), xAxis, zAxis); 253 | vec3.normalize(yAxis, yAxis); 254 | 255 | vec3.cross(xAxis, zAxis, yAxis); 256 | vec3.normalize(xAxis, xAxis); 257 | 258 | this.zoomSpeed = zoomSpeed; 259 | this.invScreen = [1.0 / screenDims[0], 1.0 / screenDims[1]]; 260 | 261 | this.centerTranslation = mat4.fromTranslation(mat4.create(), center); 262 | mat4.invert(this.centerTranslation, this.centerTranslation); 263 | 264 | var vt = vec3.set(vec3.create(), 0, 0, -1.0 * viewDist); 265 | this.translation = mat4.fromTranslation(mat4.create(), vt); 266 | 267 | var rotMat = mat3.fromValues(xAxis[0], xAxis[1], xAxis[2], 268 | yAxis[0], yAxis[1], yAxis[2], 269 | -zAxis[0], -zAxis[1], -zAxis[2]); 270 | mat3.transpose(rotMat, rotMat); 271 | this.rotation = quat.fromMat3(quat.create(), rotMat); 272 | quat.normalize(this.rotation, this.rotation); 273 | 274 | this.camera = mat4.create(); 275 | this.invCamera = mat4.create(); 276 | this.updateCameraMatrix(); 277 | } 278 | 279 | ArcballCamera.prototype.rotate = function(prevMouse, curMouse) { 280 | var mPrev = vec2.set(vec2.create(), 281 | clamp(prevMouse[0] * 2.0 * this.invScreen[0] - 1.0, -1.0, 1.0), 282 | clamp(1.0 - prevMouse[1] * 2.0 * this.invScreen[1], -1.0, 1.0)); 283 | 284 | var mCur = vec2.set(vec2.create(), 285 | clamp(curMouse[0] * 2.0 * this.invScreen[0] - 1.0, -1.0, 1.0), 286 | clamp(1.0 - curMouse[1] * 2.0 * this.invScreen[1], -1.0, 1.0)); 287 | 288 | var mPrevBall = screenToArcball(mPrev); 289 | var mCurBall = screenToArcball(mCur); 290 | // rotation = curBall * prevBall * rotation 291 | this.rotation = quat.mul(this.rotation, mPrevBall, this.rotation); 292 | this.rotation = quat.mul(this.rotation, mCurBall, this.rotation); 293 | 294 | this.updateCameraMatrix(); 295 | } 296 | 297 | ArcballCamera.prototype.zoom = function(amount) { 298 | var vt = vec3.set(vec3.create(), 0.0, 0.0, amount * this.invScreen[1] * this.zoomSpeed); 299 | var t = mat4.fromTranslation(mat4.create(), vt); 300 | this.translation = mat4.mul(this.translation, t, this.translation); 301 | if (this.translation[14] >= -0.2) { 302 | this.translation[14] = -0.2; 303 | } 304 | this.updateCameraMatrix(); 305 | } 306 | 307 | ArcballCamera.prototype.pan = function(mouseDelta) { 308 | var delta = vec4.set(vec4.create(), mouseDelta[0] * this.invScreen[0] * Math.abs(this.translation[14]), 309 | mouseDelta[1] * this.invScreen[1] * Math.abs(this.translation[14]), 0, 0); 310 | var worldDelta = vec4.transformMat4(vec4.create(), delta, this.invCamera); 311 | var translation = mat4.fromTranslation(mat4.create(), worldDelta); 312 | this.centerTranslation = mat4.mul(this.centerTranslation, translation, this.centerTranslation); 313 | this.updateCameraMatrix(); 314 | } 315 | 316 | ArcballCamera.prototype.updateCameraMatrix = function() { 317 | // camera = translation * rotation * centerTranslation 318 | var rotMat = mat4.fromQuat(mat4.create(), this.rotation); 319 | this.camera = mat4.mul(this.camera, rotMat, this.centerTranslation); 320 | this.camera = mat4.mul(this.camera, this.translation, this.camera); 321 | this.invCamera = mat4.invert(this.invCamera, this.camera); 322 | } 323 | 324 | ArcballCamera.prototype.eyePos = function() { 325 | return [camera.invCamera[12], camera.invCamera[13], camera.invCamera[14]]; 326 | } 327 | 328 | ArcballCamera.prototype.eyeDir = function() { 329 | var dir = vec4.set(vec4.create(), 0.0, 0.0, -1.0, 0.0); 330 | dir = vec4.transformMat4(dir, dir, this.invCamera); 331 | dir = vec4.normalize(dir, dir); 332 | return [dir[0], dir[1], dir[2]]; 333 | } 334 | 335 | ArcballCamera.prototype.upDir = function() { 336 | var dir = vec4.set(vec4.create(), 0.0, 1.0, 0.0, 0.0); 337 | dir = vec4.transformMat4(dir, dir, this.invCamera); 338 | dir = vec4.normalize(dir, dir); 339 | return [dir[0], dir[1], dir[2]]; 340 | } 341 | 342 | var screenToArcball = function(p) { 343 | var dist = vec2.dot(p, p); 344 | if (dist <= 1.0) { 345 | return quat.set(quat.create(), p[0], p[1], Math.sqrt(1.0 - dist), 0); 346 | } else { 347 | var unitP = vec2.normalize(vec2.create(), p); 348 | // cgmath is w, x, y, z 349 | // glmatrix is x, y, z, w 350 | return quat.set(quat.create(), unitP[0], unitP[1], 0, 0); 351 | } 352 | } 353 | var clamp = function(a, min, max) { 354 | return a < min ? min : a > max ? max : a; 355 | } 356 | 357 | var pointDist = function(a, b) { 358 | var v = [b[0] - a[0], b[1] - a[1]]; 359 | return Math.sqrt(Math.pow(v[0], 2.0) + Math.pow(v[1], 2.0)); 360 | } 361 | 362 | var Buffer = function(capacity, dtype) { 363 | this.len = 0; 364 | this.capacity = capacity; 365 | if (dtype == "uint8") { 366 | this.buffer = new Uint8Array(capacity); 367 | } else if (dtype == "int8") { 368 | this.buffer = new Int8Array(capacity); 369 | } else if (dtype == "uint16") { 370 | this.buffer = new Uint16Array(capacity); 371 | } else if (dtype == "int16") { 372 | this.buffer = new Int16Array(capacity); 373 | } else if (dtype == "uint32") { 374 | this.buffer = new Uint32Array(capacity); 375 | } else if (dtype == "int32") { 376 | this.buffer = new Int32Array(capacity); 377 | } else if (dtype == "float32") { 378 | this.buffer = new Float32Array(capacity); 379 | } else if (dtype == "float64") { 380 | this.buffer = new Float64Array(capacity); 381 | } else { 382 | console.log("ERROR: unsupported type " + dtype); 383 | } 384 | } 385 | 386 | Buffer.prototype.append = function(buf) { 387 | if (this.len + buf.length >= this.capacity) { 388 | var newCap = Math.floor(this.capacity * 1.5); 389 | var tmp = new (this.buffer.constructor)(newCap); 390 | tmp.set(this.buffer); 391 | 392 | this.capacity = newCap; 393 | this.buffer = tmp; 394 | } 395 | this.buffer.set(buf, this.len); 396 | this.len += buf.length; 397 | } 398 | 399 | Buffer.prototype.clear = function() { 400 | this.len = 0; 401 | } 402 | 403 | Buffer.prototype.stride = function() { 404 | return this.buffer.BYTES_PER_ELEMENT; 405 | } 406 | 407 | Buffer.prototype.view = function(offset, length) { 408 | return new (this.buffer.constructor)(this.buffer.buffer, offset, length); 409 | } 410 | 411 | // Various utilities that don't really fit anywhere else 412 | 413 | // Parse the hex string to RGB values in [0, 255] 414 | var hexToRGB = function(hex) { 415 | var val = parseInt(hex.substr(1), 16); 416 | var r = (val >> 16) & 255; 417 | var g = (val >> 8) & 255; 418 | var b = val & 255; 419 | return [r, g, b]; 420 | } 421 | 422 | // Parse the hex string to RGB values in [0, 1] 423 | var hexToRGBf = function(hex) { 424 | var c = hexToRGB(hex); 425 | return [c[0] / 255.0, c[1] / 255.0, c[2] / 255.0]; 426 | } 427 | 428 | /* The controller can register callbacks for various events on a canvas: 429 | * 430 | * mousemove: function(prevMouse, curMouse, evt) 431 | * receives both regular mouse events, and single-finger drags (sent as a left-click), 432 | * 433 | * press: function(curMouse, evt) 434 | * receives mouse click and touch start events 435 | * 436 | * wheel: function(amount) 437 | * mouse wheel scrolling 438 | * 439 | * pinch: function(amount) 440 | * two finger pinch, receives the distance change between the fingers 441 | * 442 | * twoFingerDrag: function(dragVector) 443 | * two finger drag, receives the drag movement amount 444 | */ 445 | var Controller = function() { 446 | this.mousemove = null; 447 | this.press = null; 448 | this.wheel = null; 449 | this.twoFingerDrag = null; 450 | this.pinch = null; 451 | } 452 | 453 | Controller.prototype.registerForCanvas = function(canvas) { 454 | var prevMouse = null; 455 | var mouseState = [false, false]; 456 | var self = this; 457 | canvas.addEventListener("mousemove", function(evt) { 458 | evt.preventDefault(); 459 | var rect = canvas.getBoundingClientRect(); 460 | var curMouse = [evt.clientX - rect.left, evt.clientY - rect.top]; 461 | if (!prevMouse) { 462 | prevMouse = [evt.clientX - rect.left, evt.clientY - rect.top]; 463 | } else if (self.mousemove) { 464 | self.mousemove(prevMouse, curMouse, evt); 465 | } 466 | prevMouse = curMouse; 467 | }); 468 | 469 | canvas.addEventListener("mousedown", function(evt) { 470 | evt.preventDefault(); 471 | var rect = canvas.getBoundingClientRect(); 472 | var curMouse = [evt.clientX - rect.left, evt.clientY - rect.top]; 473 | if (self.press) { 474 | self.press(curMouse, evt); 475 | } 476 | }); 477 | 478 | canvas.addEventListener("wheel", function(evt) { 479 | evt.preventDefault(); 480 | if (self.wheel) { 481 | self.wheel(-evt.deltaY); 482 | } 483 | }); 484 | 485 | canvas.oncontextmenu = function (evt) { 486 | evt.preventDefault(); 487 | }; 488 | 489 | var touches = {}; 490 | canvas.addEventListener("touchstart", function(evt) { 491 | var rect = canvas.getBoundingClientRect(); 492 | evt.preventDefault(); 493 | for (var i = 0; i < evt.changedTouches.length; ++i) { 494 | var t = evt.changedTouches[i]; 495 | touches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top]; 496 | if (evt.changedTouches.length == 1 && self.press) { 497 | self.press(touches[t.identifier], evt); 498 | } 499 | } 500 | }); 501 | 502 | canvas.addEventListener("touchmove", function(evt) { 503 | evt.preventDefault(); 504 | var rect = canvas.getBoundingClientRect(); 505 | var numTouches = Object.keys(touches).length; 506 | // Single finger to rotate the camera 507 | if (numTouches == 1) { 508 | if (self.mousemove) { 509 | var t = evt.changedTouches[0]; 510 | var prevTouch = touches[t.identifier]; 511 | var curTouch = [t.clientX - rect.left, t.clientY - rect.top]; 512 | evt.buttons = 1; 513 | self.mousemove(prevTouch, curTouch, evt); 514 | } 515 | } else { 516 | var curTouches = {}; 517 | for (var i = 0; i < evt.changedTouches.length; ++i) { 518 | var t = evt.changedTouches[i]; 519 | curTouches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top]; 520 | } 521 | 522 | // If some touches didn't change make sure we have them in 523 | // our curTouches list to compute the pinch distance 524 | // Also get the old touch points to compute the distance here 525 | var oldTouches = []; 526 | for (t in touches) { 527 | if (!(t in curTouches)) { 528 | curTouches[t] = touches[t]; 529 | } 530 | oldTouches.push(touches[t]); 531 | } 532 | 533 | var newTouches = []; 534 | for (t in curTouches) { 535 | newTouches.push(curTouches[t]); 536 | } 537 | 538 | // Determine if the user is pinching or panning 539 | var motionVectors = [ 540 | vec2.set(vec2.create(), newTouches[0][0] - oldTouches[0][0], 541 | newTouches[0][1] - oldTouches[0][1]), 542 | vec2.set(vec2.create(), newTouches[1][0] - oldTouches[1][0], 543 | newTouches[1][1] - oldTouches[1][1]) 544 | ]; 545 | var motionDirs = [vec2.create(), vec2.create()]; 546 | vec2.normalize(motionDirs[0], motionVectors[0]); 547 | vec2.normalize(motionDirs[1], motionVectors[1]); 548 | 549 | var pinchAxis = vec2.set(vec2.create(), oldTouches[1][0] - oldTouches[0][0], 550 | oldTouches[1][1] - oldTouches[0][1]); 551 | vec2.normalize(pinchAxis, pinchAxis); 552 | 553 | var panAxis = vec2.lerp(vec2.create(), motionVectors[0], motionVectors[1], 0.5); 554 | vec2.normalize(panAxis, panAxis); 555 | 556 | var pinchMotion = [ 557 | vec2.dot(pinchAxis, motionDirs[0]), 558 | vec2.dot(pinchAxis, motionDirs[1]) 559 | ]; 560 | var panMotion = [ 561 | vec2.dot(panAxis, motionDirs[0]), 562 | vec2.dot(panAxis, motionDirs[1]) 563 | ]; 564 | 565 | // If we're primarily moving along the pinching axis and in the opposite direction with 566 | // the fingers, then the user is zooming. 567 | // Otherwise, if the fingers are moving along the same direction they're panning 568 | if (self.pinch && Math.abs(pinchMotion[0]) > 0.5 && Math.abs(pinchMotion[1]) > 0.5 569 | && Math.sign(pinchMotion[0]) != Math.sign(pinchMotion[1])) 570 | { 571 | // Pinch distance change for zooming 572 | var oldDist = pointDist(oldTouches[0], oldTouches[1]); 573 | var newDist = pointDist(newTouches[0], newTouches[1]); 574 | self.pinch(newDist - oldDist); 575 | } else if (self.twoFingerDrag && Math.abs(panMotion[0]) > 0.5 && Math.abs(panMotion[1]) > 0.5 576 | && Math.sign(panMotion[0]) == Math.sign(panMotion[1])) 577 | { 578 | // Pan by the average motion of the two fingers 579 | var panAmount = vec2.lerp(vec2.create(), motionVectors[0], motionVectors[1], 0.5); 580 | panAmount[1] = -panAmount[1]; 581 | self.twoFingerDrag(panAmount); 582 | } 583 | } 584 | 585 | // Update the existing list of touches with the current positions 586 | for (var i = 0; i < evt.changedTouches.length; ++i) { 587 | var t = evt.changedTouches[i]; 588 | touches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top]; 589 | } 590 | }); 591 | 592 | var touchEnd = function(evt) { 593 | evt.preventDefault(); 594 | for (var i = 0; i < evt.changedTouches.length; ++i) { 595 | var t = evt.changedTouches[i]; 596 | delete touches[t.identifier]; 597 | } 598 | } 599 | canvas.addEventListener("touchcancel", touchEnd); 600 | canvas.addEventListener("touchend", touchEnd); 601 | } 602 | 603 | --------------------------------------------------------------------------------