diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..b512c09d476623ff4bf8d0d63c29b784925dbdf8
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c3c07d114df83ae2c2dc663da720cc5451914f31..0ee38faacdd75f02e4d4c8567b72065c7d9ef719 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,10 +15,10 @@ jobs:
 
     steps:
     - uses: actions/checkout@v2
-    - name: Use Node.js 14.x for lint
+    - name: Use Node.js 16.x for lint
       uses: actions/setup-node@v1
       with:
-        node-version: '14.x'
+        node-version: '16.x'
     - run: npm i
     - run: npm run lint
 
@@ -26,38 +26,37 @@ jobs:
     if: always()
     runs-on: ubuntu-latest
 
-    strategy:
-      matrix:
-        node-version: [12.x, 14.x, 16.x]
-
     env:
       NODE_ENV: test
       
     steps:
     - uses: actions/checkout@v2
-    - name: Use Node.js ${{ matrix.node-version }}
+    - name: Use Node.js 16.x
       uses: actions/setup-node@v1
       with:
-        node-version: ${{ matrix.node-version }}
+        node-version: 16.x
     - run: npm i
-    - run: npm run test-ci
+    - run: |
+        if [[ "$GITHUB_REF" = *hotfix* ]] || [[ "$GITHUB_REF" = refs/heads/staging ]]
+        then
+          export SIIBRA_API_ENDPOINTS=https://siibra-api-rc.apps.hbp.eu/v2_0,https://siibra-api-rc.apps.jsc.hbp.eu/v2_0
+          node src/environments/parseEnv.js ./environment.ts
+        fi
+        npm run test-ci
 
   backend:
     if: always()
     runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        node-version: [12.x, 14.x, 16.x]
 
     env:
       NODE_ENV: test
 
     steps:
     - uses: actions/checkout@v2
-    - name: Use Node.js ${{ matrix.node-version }}
+    - name: Use Node.js 16.x
       uses: actions/setup-node@v1
       with:
-        node-version: ${{ matrix.node-version }}
+        node-version: 16.x
     - run: |
         cd deploy
         npm i
diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml
index 47734fc27ba6540ee111646142553af53a365798..057a93d7864f37820b14b5cf8f6ce141a6a08f85 100644
--- a/.github/workflows/docker_img.yml
+++ b/.github/workflows/docker_img.yml
@@ -18,7 +18,7 @@ jobs:
       PRODUCTION: 'true'
       DOCKER_REGISTRY: 'docker-registry.ebrains.eu/siibra/'
 
-      SIIBRA_API_STABLE: 'https://siibra-api-stable.apps.hbp.eu/v2_0'
+      SIIBRA_API_STABLE: 'https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0'
       SIIBRA_API_RC: 'https://siibra-api-rc.apps.hbp.eu/v2_0'
       SIIBRA_API_LATEST: 'https://siibra-api-latest.apps-dev.hbp.eu/v2_0'
 
@@ -37,31 +37,24 @@ jobs:
         if [[ "$GITHUB_REF" == 'refs/heads/master' ]]
         then
           echo "Either master, using prod env..."
-          echo "BS_REST_URL=${{ env.SIIBRA_API_STABLE }}" >> $GITHUB_ENV
+          echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_STABLE }}" >> $GITHUB_ENV
         elif [[ "$GITHUB_REF" == 'refs/heads/staging' ]]
         then
           echo "Either staging, using staging env..."
-          echo "BS_REST_URL=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV
+          echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV
         else
           if [[ "$GITHUB_REF" == *hotfix* ]]
           then
             echo "Hotfix branch, using prod env..."
-            echo "BS_REST_URL=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV
+            echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV
           else
             echo "Using dev env..."
-            echo "BS_REST_URL=${{ env.SIIBRA_API_LATEST }}" >> $GITHUB_ENV
+            echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_LATEST }}" >> $GITHUB_ENV
           fi
         fi
 
     - name: 'Set version variable & expmt feature flag'
       run: |
-        GIT_HASH=$(git rev-parse --short HEAD)
-        echo "Setting GIT_HASH: $GIT_HASH"
-        echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV
-
-        VERSION=$(jq -r '.version' package.json)
-        echo "Setting VERSION: $VERSION"
-        echo "VERSION=$VERSION" >> $GITHUB_ENV
         if [[ "$GITHUB_REF" == 'refs/heads/master' ]] || [[ "$GITHUB_REF" == 'refs/heads/staging' ]]
         then
           echo "prod/staging build, do not enable experimental features"
@@ -74,11 +67,9 @@ jobs:
         DOCKER_BUILT_TAG=${{ env.DOCKER_REGISTRY }}siibra-explorer:$BRANCH_NAME
         echo "Building $DOCKER_BUILT_TAG"
         docker build \
-          --build-arg GIT_HASH=$GIT_HASH \
-          --build-arg VERSION=$VERSION \
           --build-arg MATOMO_URL=$MATOMO_URL \
           --build-arg MATOMO_ID=$MATOMO_ID \
-          --build-arg BS_REST_URL=$BS_REST_URL \
+          --build-arg SIIBRA_API_ENDPOINTS=$SIIBRA_API_ENDPOINTS \
           --build-arg EXPERIMENTAL_FEATURE_FLAG=$EXPERIMENTAL_FEATURE_FLAG \
           -t $DOCKER_BUILT_TAG \
           .
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index 7263cbb65b77246cb748b3c7f1358d119578aea4..f63b14cd95658b20dc7795ad8804b6e322020b50 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -11,3 +11,4 @@
 </style>
 <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
 <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" defer></script>
+<link rel="stylesheet" href="icons/iav-icons.css">
diff --git a/.storybook/preview.js b/.storybook/preview.js
index 957e7cbef107fa2c3191e57245241fb901983f17..52750e8cde9405a8fc3282ed73730bc13101605d 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -4,6 +4,14 @@ setCompodocJson(docJson);
 
 import 'src/theme.scss'
 
+/**
+ * load custom icons
+ */
+import '!!file-loader?context=src/res&name=icons/iav-icons.css!src/res/icons/iav-icons.css'
+import '!!file-loader?context=src/res&name=icons/iav-icons.ttf!src/res/icons/iav-icons.ttf'
+import '!!file-loader?context=src/res&name=icons/iav-icons.woff!src/res/icons/iav-icons.woff'
+import '!!file-loader?context=src/res&name=icons/iav-icons.svg!src/res/icons/iav-icons.svg'
+
 export const parameters = {
   actions: { argTypesRegex: "^on[A-Z].*" },
   controls: {
diff --git a/Dockerfile b/Dockerfile
index 17e735232f80890da6b5b2b65db1b33c152c9765..8425647048ad716c0b86edb66ad609b36aa01a84 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,8 +3,8 @@ FROM node:14 as builder
 ARG BACKEND_URL
 ENV BACKEND_URL=${BACKEND_URL}
 
-ARG BS_REST_URL
-ENV BS_REST_URL=${BS_REST_URL:-https://siibra-api-stable.apps.hbp.eu/v1_0}
+ARG SIIBRA_API_ENDPOINTS
+ENV SIIBRA_API_ENDPOINTS=${SIIBRA_API_ENDPOINTS:-https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0}
 
 ARG STRICT_LOCAL
 ENV STRICT_LOCAL=${STRICT_LOCAL:-false}
@@ -21,11 +21,9 @@ ENV MATOMO_ID=${MATOMO_ID}
 ARG EXPERIMENTAL_FEATURE_FLAG
 ENV EXPERIMENTAL_FEATURE_FLAG=${EXPERIMENTAL_FEATURE_FLAG:-false}
 
-ARG GIT_HASH
-ENV GIT_HASH=${GIT_HASH:-unknownhash}
+ARG ENABLE_LEAP_MOTION
+ENV ENABLE_LEAP_MOTION=${ENABLE_LEAP_MOTION:-false}
 
-ARG VERSION
-ENV VERSION=${VERSION:-unknownversion}
 
 COPY . /iv
 WORKDIR /iv
@@ -39,6 +37,7 @@ RUN rm -rf ./node_modules
 
 RUN npm i
 RUN npm run build
+RUN node third_party/matomo/processMatomo.js
 RUN npm run build-storybook
 
 # gzipping container
diff --git a/angular.json b/angular.json
index 9797b5413cb83b570a3695fe1a1bdca44b6a2de7..48fe4b228e4f2fca1ed066681e73248be78ab271 100644
--- a/angular.json
+++ b/angular.json
@@ -80,6 +80,10 @@
               "input": "export-nehuba/dist/min/chunk_worker.bundle.js",
               "inject": false,
               "bundleName": "chunk_worker.bundle"
+            },{
+              "inject": false,
+              "input": "third_party/leap-0.6.4.js",
+              "bundleName": "leap-0.6.4"
             }]
           },
           "configurations": {
diff --git a/build_env.md b/build_env.md
index 1c29ea98614c3a40dbe91d7c620f9e6a5356b4e8..81833e992e2dd7b5e484e0593cee4409e128c603 100644
--- a/build_env.md
+++ b/build_env.md
@@ -4,12 +4,13 @@ As interactive atlas viewer uses [webpack define plugin](https://webpack.js.org/
 
 | name | description | default | example |
 | --- | --- | --- | --- |
-| `VERSION` | printed in console on viewer startup | `GIT_HASH` \|\| unspecificed hash | v2.2.2 |
 | `PRODUCTION` | if the build is for production, toggles optimisations such as minification | `undefined` | true |
 | `BACKEND_URL` | backend that the viewer calls to fetch available template spaces, parcellations, plugins, datasets | `null` | https://interactive-viewer.apps.hbp.eu/ |
-| `BS_REST_URL` | [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | https://siibra-api-stable.apps.hbp.eu/v1_0 |
+| ~~`BS_REST_URL`~~ _deprecated. use `SIIBRA_API_ENDPOINTS` instead_ | [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | `https://siibra-api-stable.apps.hbp.eu/v1_0` |
+| `SIIBRA_API_ENDPOINTS` | Comma separated endpoints of [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | `https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0` |
 | `MATOMO_URL` | base url for matomo analytics | `null` | https://example.com/matomo/ |
 | `MATOMO_ID` | application id for matomo analytics | `null` | 6 |
 | `STRICT_LOCAL` | hides **Explore** and **Download** buttons. Useful for offline demo's | `false` | `true` |
 | `KIOSK_MODE` | after 5 minutes of inactivity, shows overlay inviting users to interact | `false` | `true` |
-| `BUILD_TEXT` | overlay text at bottom right of the viewer. set to `''` to hide. | |
+| `EXPERIMENTAL_FEATURE_FLAG` | enabling experimental features | `false` | `true` |
+| `ENABLE_LEAP_MOTION` | enable leap motion controller | `false` | `true` |
diff --git a/common/constants.js b/common/constants.js
index cf56106555a3b26559711671e22fcc0e06494e86..c1fe2db5b6981123c9811587a0523a859b95c234 100644
--- a/common/constants.js
+++ b/common/constants.js
@@ -108,6 +108,7 @@ If you do not accept the Terms & Conditions you are not permitted to access or u
     CANNOT_DECIPHER_HEMISPHERE: 'Cannot decipher region hemisphere.',
     DOES_NOT_SUPPORT_MULTI_REGION_SELECTION: `Please only select a single region.`,
     MULTI_REGION_SELECTION: `Multi region selection`,
+    DESCRIPTION: 'Description',
     REGIONAL_FEATURES: 'Regional features',
     CONNECTIVITY: 'Connectivity',
     NO_ADDIONTAL_INFO_AVAIL: `Currently, no additional information is linked to this region.`,
@@ -135,7 +136,11 @@ If you do not accept the Terms & Conditions you are not permitted to access or u
     LOADING_ANNOTATION_MSG: `Loading annotations... Please wait...`,
 
     ATLAS_SELECTOR_LABEL_SPACES: `Spaces`,
-    ATLAS_SELECTOR_LABEL_PARC_MAPS: `Parcellation maps`
+    ATLAS_SELECTOR_LABEL_PARC_MAPS: `Parcellation maps`,
+
+    TOGGLE_LAYER_VISILITY: 'Toggle layer visility',
+    ORIENT_TO_LAYER: 'Orient to layer native orientation',
+    CONFIGURE_LAYER: 'Configure layer'
   }
 
   exports.QUICKTOUR_DESC ={
diff --git a/common/helpOnePager.md b/common/helpOnePager.md
index a8551e0b2b22258b633459a5854774ac301f8ae0..4fb8f16ec04794c71a642a1d92526a6a2e53800c 100644
--- a/common/helpOnePager.md
+++ b/common/helpOnePager.md
@@ -10,6 +10,7 @@
 | Next slice | `<ctrl>` + `[mousewheel]` | - |
 | Next 10 slice | `<ctrl>` + `<shift>` + `[mousewheel]` | - |
 | Toggle delineation | `[q]` | - |
+| Toggle cross hair | `[a]` | - |
 
 ---
 
diff --git a/common/util.js b/common/util.js
index 198b3f0b6a0de651198849214120e918799e8af9..44f458cafc4884e64f1a225c6d11a3425fbbda68 100644
--- a/common/util.js
+++ b/common/util.js
@@ -239,5 +239,78 @@
   exports.isVec4 = isVec4
   exports.isMat4 = isMat4
 
+  const cipher = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-'
+  const negString = '~'
+
+  const encodeInt = (number) => {
+    if (number % 1 !== 0) { throw new Error('cannot encodeInt on a float. Ensure float flag is set') }
+    if (isNaN(Number(number)) || number === null || number === Number.POSITIVE_INFINITY) { throw new Error('The input is not valid') }
+  
+    let residual
+    let result = ''
+  
+    if (number < 0) {
+      result += negString
+      residual = Math.floor(number * -1)
+    } else {
+      residual = Math.floor(number)
+    }
+  
+    /* eslint-disable-next-line no-constant-condition */
+    while (true) {
+      result = cipher.charAt(residual % 64) + result
+      residual = Math.floor(residual / 64)
+  
+      if (residual === 0) {
+        break
+      }
+    }
+    return result
+  }
+  const decodeToInt = (encodedString) => {
+    let _encodedString
+    let negFlag = false
+    if (encodedString.slice(-1) === negString) {
+      negFlag = true
+      _encodedString = encodedString.slice(0, -1)
+    } else {
+      _encodedString = encodedString
+    }
+    return (negFlag ? -1 : 1) * [..._encodedString].reduce((acc, curr) => {
+      const index = cipher.indexOf(curr)
+      if (index < 0) { throw new Error(`Poisoned b64 encoding ${encodedString}`) }
+      return acc * 64 + index
+    }, 0)
+  }
+
+  exports.sxplrNumB64Enc = {
+    separator: ".",
+    cipher,
+    encodeNumber: (number, opts = { float: false }) => {
+      const { float } = opts
+      if (!float) {
+        return encodeInt(number)
+      } else {
+        const floatArray = new Float32Array(1)
+        floatArray[0] = number
+        const intArray = new Uint32Array(floatArray.buffer)
+        const castedInt = intArray[0]
+        return encodeInt(castedInt)
+      }
+    },
+    decodeToNumber: (encodedString, opts = { float: false }) => {
+      const { float } = opts
+      if (!float) {
+        return decodeToInt(encodedString)
+      } else {
+        const _int = decodeToInt(encodedString)
+        const intArray = new Uint32Array(1)
+        intArray[0] = _int
+        const castedFloat = new Float32Array(intArray.buffer)
+        return castedFloat[0]
+      }
+    }
+  }
+
 
 })(typeof exports === 'undefined' ? module.exports : exports)
diff --git a/deploy/app.js b/deploy/app.js
index 7b48f151dcd9e9e9fc8d9595b37eab27d00e02b4..f4b33a1c39d3904ace5651ca22ed2a909e8d269b 100644
--- a/deploy/app.js
+++ b/deploy/app.js
@@ -7,7 +7,7 @@ const crypto = require('crypto')
 const cookieParser = require('cookie-parser')
 const bkwdMdl = require('./bkwdCompat')()
 
-const LOCAL_CDN_FLAG = !!process.env.PRECOMPUTED_SERVER
+const LOCAL_CDN_FLAG = !!process.env.LOCAL_CDN
 
 if (process.env.NODE_ENV !== 'production') {
   app.use(require('cors')())
@@ -126,7 +126,8 @@ if (LOCAL_CDN_FLAG) {
   const LOCAL_CDN = process.env.LOCAL_CDN
   const CDN_ARRAY = [
     'https://stackpath.bootstrapcdn.com',
-    'https://use.fontawesome.com'
+    'https://use.fontawesome.com',
+    'https://unpkg.com'
   ]
 
   let indexFile
@@ -214,8 +215,7 @@ app.get('/ready', async (req, res) => {
  * only use compression for production
  * this allows locally built aot to be served without errors
  */
-const { compressionMiddleware, setAlwaysOff } = require('nomiseco')
-if (LOCAL_CDN_FLAG) setAlwaysOff(true)
+const { compressionMiddleware } = require('nomiseco')
 
 app.use(compressionMiddleware, express.static(PUBLIC_PATH))
 
diff --git a/deploy/bkwdCompat/urlState.js b/deploy/bkwdCompat/urlState.js
index 66f92c3dc4f6b375fcfe0c70f16a685b43c30e0d..52b245bcc54d7b6c1bc2a479a354651e215143e7 100644
--- a/deploy/bkwdCompat/urlState.js
+++ b/deploy/bkwdCompat/urlState.js
@@ -1,6 +1,12 @@
 // this module is suppose to rewrite state stored in query param
 // and convert it to path based url
-const separator = '.'
+const { sxplrNumB64Enc } = require("../../common/util")
+
+const {
+  encodeNumber,
+  separator
+} = sxplrNumB64Enc
+
 const waxolmObj = {
   aId: 'minds/core/parcellationatlas/v1.0.0/522b368e-49a3-49fa-88d3-0870a307974a',
   id: 'minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8',
@@ -114,8 +120,12 @@ const WARNING_STRINGS = {
   REGION_SELECT_ERROR: 'Region selected cannot be processed properly.',
   TEMPLATE_ERROR: 'Template not found.',
 }
-
+const pliPreviewUrl = `/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:a1655b99-82f1-420f-a3c2-fe80fd4c8588/p:juelich:iav:atlas:v1.0.0:4/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIy..1qI1a.D31U~.i-Os~..HRE/f:siibra:features:voi:19c437087299dd62e7c507200f69aea6`
 module.exports = (query, _warningCb) => {
+
+  const HOST_PATHNAME = process.env.HOST_PATHNAME || ''
+  let redirectUrl = `${HOST_PATHNAME}/#`
+
   const {
     standaloneVolumes,
     niftiLayers, // deprecating - check if anyone calls this url
@@ -127,7 +137,7 @@ module.exports = (query, _warningCb) => {
     regionsSelected, // deprecating - check if any one calls this url
     cRegionsSelected,
 
-    navigation, // deprecating - check if any one calls this endpoint
+    navigation,
     cNavigation,
   } = query || {}
 
@@ -142,7 +152,6 @@ module.exports = (query, _warningCb) => {
     if (Object.values(WARNING_STRINGS).includes(arg)) _warningCb(arg)
   }
 
-  if (navigation) console.warn(`navigation has been deprecated`)
   if (regionsSelected) console.warn(`regionSelected has been deprecated`)
   if (niftiLayers) console.warn(`nifitlayers has been deprecated`)
 
@@ -156,6 +165,30 @@ module.exports = (query, _warningCb) => {
 
   // common search param & path
   let nav, dsp, r
+  if (navigation) {
+    try {
+
+      const [
+        _o, _po, _pz, _p, _z
+      ] = navigation.split("__")
+      const o = _o.split("_").map(v => Number(v))
+      const po = _po.split("_").map(v => Number(v))
+      const pz = Number(_pz)
+      const p = _p.split("_").map(v => Number(v))
+      const z = Number(_z)
+      const v = [
+        o.map(n => encodeNumber(n, {float: true})).join(separator),
+        po.map(n => encodeNumber(n, {float: true})).join(separator),
+        encodeNumber(Math.floor(pz)),
+        Array.from(p).map(v => Math.floor(v)).map(n => encodeNumber(n)).join(separator),
+        encodeNumber(Math.floor(z)),
+      ].join(`${separator}${separator}`)
+
+      nav = `/@:${encodeURI(v)}`
+    } catch (e) {
+      console.warn(`Parsing navigation param error`, e)
+    }
+  }
   if (cNavigation) nav = `/@:${encodeURI(cNavigation)}`
   if (previewingDatasetFiles) {
     try {
@@ -163,7 +196,17 @@ module.exports = (query, _warningCb) => {
       if (Array.isArray(parsedDsp)) {
         if (parsedDsp.length === 1) {
           const { datasetId, filename } = parsedDsp[0]
-          dsp = `/dsp:${encodeId(datasetId)}::${encodeURI(filename)}`
+          if (datasetId === "minds/core/dataset/v1.0.0/b08a7dbc-7c75-4ce7-905b-690b2b1e8957") {
+            /**
+             * if preview pli link, return hardcoded link
+             */
+            return `${HOST_PATHNAME}/#${pliPreviewUrl}`
+          } else {
+            /**
+             * TODO deprecate dsp
+             */
+            dsp = `/dsp:${encodeId(datasetId)}::${encodeURI(filename)}`
+          }
         } else {
           searchParam.set(`previewingDatasetFiles`, previewingDatasetFiles)
         }
@@ -206,8 +249,6 @@ module.exports = (query, _warningCb) => {
       // ignore region selected and move on
     }
   }
-  const HOST_PATHNAME = process.env.HOST_PATHNAME || ''
-  let redirectUrl = `${HOST_PATHNAME}/#`
   if (standaloneVolumes) {
     searchParam.set('standaloneVolumes', standaloneVolumes)
     if (nav) redirectUrl += nav
diff --git a/deploy/csp/index.js b/deploy/csp/index.js
index fcfa227d1687f5b52246f50634effd0ff6d0bbd8..9cc365a20323aad04d461fc9143c81f56cc91db4 100644
--- a/deploy/csp/index.js
+++ b/deploy/csp/index.js
@@ -54,7 +54,9 @@ const connectSrc = [
   'object.cscs.ch',
 
   // required for dataset previews
-  'hbp-kg-dataset-previewer.apps.hbp.eu/v2/',
+
+  // spatial transform
+  "hbp-spatial-backend.apps.hbp.eu",
 
   // injected by env var
   ...CSP_CONNECT_SRC
@@ -102,14 +104,13 @@ module.exports = {
         ],
         imgSrc: [
           "'self'",
-          "hbp-kg-dataset-previewer.apps.hbp.eu/v2/"
         ],
         scriptSrc:[
           "'self'",
           ...userScriptSrc,
           'unpkg.com/kg-dataset-previewer@1.2.0/', // preview component
-          'cdnjs.cloudflare.com/ajax/libs/d3/', // required for preview component
-          'cdnjs.cloudflare.com/ajax/libs/mathjax/', // math jax
+          'https://unpkg.com/d3@6.2.0/', // required for preview component
+          'https://unpkg.com/mathjax@3.1.2/', // math jax
           'https://unpkg.com/three-surfer@0.0.11/dist/bundle.js', // for threeSurfer (freesurfer support in browser)
           'https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/', // needed for ng layer control
           'https://unpkg.com/hbp-connectivity-component@0.6.5/', // needed for connectivity component
@@ -118,6 +119,9 @@ module.exports = {
           ...WHITE_LIST_SRC,
           ...defaultAllowedSites
         ],
+        frameSrc: [
+          "*"
+        ],
         reportUri: CSP_REPORT_URI || '/report-violation'
       },
       reportOnly
diff --git a/deploy/package-lock.json b/deploy/package-lock.json
index 1dac062d78f8ca3d5a23d9456457a913ceef919c..85a12542dc7b71eca0510d03a3d5ce1178f09567 100644
--- a/deploy/package-lock.json
+++ b/deploy/package-lock.json
@@ -10,9 +10,9 @@
       "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
     },
     "@sindresorhus/is": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-1.2.0.tgz",
-      "integrity": "sha512-mwhXGkRV5dlvQc4EgPDxDxO6WuMBVymGFd1CA+2Y+z5dG9MNspoQ+AWjl/Ld1MnpCL8AKbosZlDVohqcIwuWsw=="
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+      "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
     },
     "@sinonjs/commons": {
       "version": "1.7.0",
@@ -924,25 +924,60 @@
       }
     },
     "got": {
-      "version": "10.5.5",
-      "resolved": "https://registry.npmjs.org/got/-/got-10.5.5.tgz",
-      "integrity": "sha512-B13HHkCkTA7KxyxTrFoZfrurBX1fZxjMTKpmIfoVzh0Xfs9aZV7xEfI6EKuERQOIPbomh5LE4xDkfK6o2VXksw==",
+      "version": "11.8.5",
+      "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
+      "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
       "requires": {
-        "@sindresorhus/is": "^1.0.0",
-        "@szmarczak/http-timer": "^4.0.0",
+        "@sindresorhus/is": "^4.0.0",
+        "@szmarczak/http-timer": "^4.0.5",
         "@types/cacheable-request": "^6.0.1",
-        "cacheable-lookup": "^2.0.0",
-        "cacheable-request": "^7.0.1",
-        "decompress-response": "^5.0.0",
-        "duplexer3": "^0.1.4",
-        "get-stream": "^5.0.0",
+        "@types/responselike": "^1.0.0",
+        "cacheable-lookup": "^5.0.3",
+        "cacheable-request": "^7.0.2",
+        "decompress-response": "^6.0.0",
+        "http2-wrapper": "^1.0.0-beta.5.2",
         "lowercase-keys": "^2.0.0",
-        "mimic-response": "^2.0.0",
         "p-cancelable": "^2.0.0",
-        "p-event": "^4.0.0",
-        "responselike": "^2.0.0",
-        "to-readable-stream": "^2.0.0",
-        "type-fest": "^0.9.0"
+        "responselike": "^2.0.0"
+      },
+      "dependencies": {
+        "cacheable-lookup": {
+          "version": "5.0.4",
+          "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+          "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
+        },
+        "cacheable-request": {
+          "version": "7.0.2",
+          "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
+          "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
+          "requires": {
+            "clone-response": "^1.0.2",
+            "get-stream": "^5.1.0",
+            "http-cache-semantics": "^4.0.0",
+            "keyv": "^4.0.0",
+            "lowercase-keys": "^2.0.0",
+            "normalize-url": "^6.0.1",
+            "responselike": "^2.0.0"
+          }
+        },
+        "decompress-response": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+          "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+          "requires": {
+            "mimic-response": "^3.1.0"
+          }
+        },
+        "mimic-response": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+          "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
+        },
+        "normalize-url": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+          "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
+        }
       }
     },
     "growl": {
@@ -1571,9 +1606,9 @@
       },
       "dependencies": {
         "@sindresorhus/is": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz",
-          "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ=="
+          "version": "4.6.0",
+          "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+          "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
         },
         "cacheable-lookup": {
           "version": "5.0.4",
@@ -1589,21 +1624,46 @@
           }
         },
         "got": {
-          "version": "11.8.2",
-          "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
-          "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
+          "version": "11.8.5",
+          "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
+          "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
           "requires": {
             "@sindresorhus/is": "^4.0.0",
             "@szmarczak/http-timer": "^4.0.5",
             "@types/cacheable-request": "^6.0.1",
             "@types/responselike": "^1.0.0",
             "cacheable-lookup": "^5.0.3",
-            "cacheable-request": "^7.0.1",
+            "cacheable-request": "^7.0.2",
             "decompress-response": "^6.0.0",
             "http2-wrapper": "^1.0.0-beta.5.2",
             "lowercase-keys": "^2.0.0",
             "p-cancelable": "^2.0.0",
             "responselike": "^2.0.0"
+          },
+          "dependencies": {
+            "cacheable-request": {
+              "version": "7.0.2",
+              "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
+              "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
+              "requires": {
+                "clone-response": "^1.0.2",
+                "get-stream": "^5.1.0",
+                "http-cache-semantics": "^4.0.0",
+                "keyv": "^4.0.0",
+                "lowercase-keys": "^2.0.0",
+                "normalize-url": "^6.0.1",
+                "responselike": "^2.0.0"
+              }
+            },
+            "http2-wrapper": {
+              "version": "1.0.3",
+              "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+              "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+              "requires": {
+                "quick-lru": "^5.1.1",
+                "resolve-alpn": "^1.0.0"
+              }
+            }
           }
         },
         "lru-cache": {
@@ -1619,6 +1679,11 @@
           "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
           "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
         },
+        "normalize-url": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+          "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
+        },
         "yallist": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -1917,9 +1982,9 @@
       "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
     },
     "resolve-alpn": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz",
-      "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA=="
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+      "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
     },
     "responselike": {
       "version": "2.0.0",
@@ -2229,11 +2294,6 @@
       "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
       "dev": true
     },
-    "type-fest": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.9.0.tgz",
-      "integrity": "sha512-j55pzONIdg7rdtJTRZPKIbV0FosUqYdhHK1aAYJIrUvejv1VVyBokrILE8KQDT4emW/1Ev9tx+yZG+AxuSBMmA=="
-    },
     "type-is": {
       "version": "1.6.16",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
diff --git a/deploy/package.json b/deploy/package.json
index 333568cdfc0b2804ed97319a6b00dd1577c59d50..9470dcf3c16f707ecd0c023fcff17e3e843d2bc8 100644
--- a/deploy/package.json
+++ b/deploy/package.json
@@ -18,7 +18,7 @@
     "express": "^4.16.4",
     "express-rate-limit": "^5.5.1",
     "express-session": "^1.15.6",
-    "got": "^10.5.5",
+    "got": "^11.8.5",
     "hbp-seafile": "^0.2.0",
     "helmet-csp": "^3.4.0",
     "lru-cache": "^5.1.1",
diff --git a/deploy/plugins/index.js b/deploy/plugins/index.js
index 6089e2fe9eb3c54dcb8f4c081f947d4cb719377c..028a0158d9a94b4a353242099c9cfed7eb8f867f 100644
--- a/deploy/plugins/index.js
+++ b/deploy/plugins/index.js
@@ -35,40 +35,45 @@ const getKey = url => `plugin:manifest-cache:${url}`
 router.get('/manifests', async (_req, res) => {
 
   const allManifests = await Promise.all(
-    [...V2_7_PLUGIN_URLS, ...V2_7_STAGING_PLUGIN_URLS].map(async url =>
-      race(
-        async () => {
-          const key = getKey(url)
-          
-          await lruStore._initPr
-          const { store } = lruStore
-          
-          try {
-            const storedManifest = await store.get(key)
-            if (storedManifest) return JSON.parse(storedManifest)
-            else throw `not found`
-          } catch (e) {
-            const resp = await got(url)
-            const json = JSON.parse(resp.body)
-    
-            const { iframeUrl, 'siibra-explorer': flag } = json
-            if (!flag) return null
-            if (!iframeUrl) return null
-            const u = new URL(url)
+    [...V2_7_PLUGIN_URLS, ...V2_7_STAGING_PLUGIN_URLS].map(async url => {
+      try {
+        return await race(
+          async () => {
+            const key = getKey(url)
             
-            let replaceObj = {}
-            if (!/^https?:\/\//.test(iframeUrl)) {
-              u.pathname = path.resolve(path.dirname(u.pathname), iframeUrl)
-              replaceObj['iframeUrl'] = u.toString()
+            await lruStore._initPr
+            const { store } = lruStore
+            
+            try {
+              const storedManifest = await store.get(key)
+              if (storedManifest) return JSON.parse(storedManifest)
+              else throw `not found`
+            } catch (e) {
+              const resp = await got(url)
+              const json = JSON.parse(resp.body)
+      
+              const { iframeUrl, 'siibra-explorer': flag } = json
+              if (!flag) return null
+              if (!iframeUrl) return null
+              const u = new URL(url)
+              
+              let replaceObj = {}
+              if (!/^https?:\/\//.test(iframeUrl)) {
+                u.pathname = path.resolve(path.dirname(u.pathname), iframeUrl)
+                replaceObj['iframeUrl'] = u.toString()
+              }
+              const returnObj = {...json, ...replaceObj}
+              await store.set(key, JSON.stringify(returnObj), { maxAge: 1000 * 60 * 60 })
+              return returnObj
             }
-            const returnObj = {...json, ...replaceObj}
-            await store.set(key, JSON.stringify(returnObj), { maxAge: 1000 * 60 * 60 })
-            return returnObj
-          }
-        },
-        { timeout: 1000 }
-      )
-    )
+          },
+          { timeout: 1000 }
+        )
+      } catch (e) {
+        console.error(`fetching manifest error: ${e}`)
+        return null
+      }
+    })
   )
 
   res.status(200).json(
diff --git a/deploy/saneUrl/index.js b/deploy/saneUrl/index.js
index 3bc0bca9fe6e6eafad1075a0a4b16b2eddddd91e..582113e0c19cc9461c81eb288824e265e0d3bef1 100644
--- a/deploy/saneUrl/index.js
+++ b/deploy/saneUrl/index.js
@@ -104,7 +104,8 @@ router.post('/:name',
   limiterMiddleware(),
   express.json(),
   async (req, res) => {
-    if (req.headers['x-noop']) return res.status(200).end()
+    if (/bot/i.test(req.headers['user-agent'])) return res.status(201).end()
+    if (req.headers['x-noop']) return res.status(201).end()
     const { name } = req.params
     try {
       await proxyStore.set(req, name, req.body)
diff --git a/deploy/util/reconfigPrecomputedServer.js b/deploy/util/reconfigPrecomputedServer.js
deleted file mode 100644
index 98a1932ba74dbdc4361efae7056f876af7141b15..0000000000000000000000000000000000000000
--- a/deploy/util/reconfigPrecomputedServer.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * n.b. trailing slash is required
- * e.g. http://localhost:10080/
- */
-const PRECOMPUTED_SERVER = process.env.PRECOMPUTED_SERVER
-
-const reconfigureFlag = !!PRECOMPUTED_SERVER
-
-exports.reconfigureFlag = reconfigureFlag
-
-exports.reconfigureUrl = (str) => {
-  if (!reconfigureFlag) return str
-  return str.replace(/https?:\/\/.*?\//g, PRECOMPUTED_SERVER)
-}
\ No newline at end of file
diff --git a/deploy_env.md b/deploy_env.md
index bb17c783cbd86d4fa1932a36f44f7a4118012452..1926d4bb3fb5214230399d416954165c7a9b7862 100644
--- a/deploy_env.md
+++ b/deploy_env.md
@@ -8,12 +8,14 @@
 | `HOST_PATHNAME` | pathname to listen on, restrictions: leading slash, no trailing slash | `''` | `/viewer` |
 | `SESSIONSECRET` | session secret for cookie session |
 | `NODE_ENV` | determines where the built viewer will be served from | | `production` |
-| `PRECOMPUTED_SERVER` | redirect data uri to another server. Useful for offline demos | | `http://localhost:8080/precomputed/` |
+| ~~`PRECOMPUTED_SERVER`~~ _deprecated_, use `LOCAL_CDN` instead. | redirect data uri to another server. Useful for offline demos | | `http://localhost:8080/precomputed/` |
 | `LOCAL_CDN` | rewrite cdns to local server. useful for offlnie demo | | `http://localhost:7080/` |
 | `PLUGIN_URLS` | semi colon separated urls to be returned when user queries plugins | `''`
 | `STAGING_PLUGIN_URLS` | semi colon separated urls to be returned when user queries plugins | `''`
 | `USE_LOGO` | possible values are `hbp`, `ebrains`, `fzj` | `hbp` | `ebrains` |
 | `__DEBUG__` | debug flag | 
+| `BUILD_TEXT` | overlay text at bottom right of the viewer. set to `''` to hide. | |
+
 
 ##### ebrains user authentication
 
diff --git a/docs/releases/v2.6.10.md b/docs/releases/v2.6.10.md
new file mode 100644
index 0000000000000000000000000000000000000000..afc4ab3ece8cdef9414ff7ef42ad7a84b4489007
--- /dev/null
+++ b/docs/releases/v2.6.10.md
@@ -0,0 +1,5 @@
+# 2.6.9
+
+## Bugfix
+
+- Remove empty quick tour.
diff --git a/docs/releases/v2.7.1.md b/docs/releases/v2.7.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2d967a9c06c5baa042653168ddded4bbf99c52f
--- /dev/null
+++ b/docs/releases/v2.7.1.md
@@ -0,0 +1,5 @@
+# v2.7.1
+
+## Bugfix
+
+- fixed region detail fetching using duplicated id as endpoint
diff --git a/docs/releases/v2.7.2.md b/docs/releases/v2.7.2.md
new file mode 100644
index 0000000000000000000000000000000000000000..2a5ada611284053bd629c4f6dc0ed4c765889b58
--- /dev/null
+++ b/docs/releases/v2.7.2.md
@@ -0,0 +1,12 @@
+# v2.7.2
+
+## Feature
+
+- (re)introduced the parcellation info button
+
+## Bugfix
+
+- fix the position of quick tour panel of slice view panels
+- fix the atlas selection logic. This should reduce 4xx/5xx calls significantly
+- minor update to parcellation chip appearance
+- clicking on feature badge now properly selects the feature
diff --git a/docs/releases/v2.7.3.md b/docs/releases/v2.7.3.md
new file mode 100644
index 0000000000000000000000000000000000000000..ec24dc460b6f4bc81b0faab9a78fa5eaf29ff948
--- /dev/null
+++ b/docs/releases/v2.7.3.md
@@ -0,0 +1,13 @@
+# v2.7.3
+
+## Bugfix
+
+- fixed matomo visitor counting (broke since 2.7.0 release)
+- fixed sane url generation
+- fixed reset navigation buttons in navigation card
+
+## Under the hood
+
+- minor refactor of unused code
+- added mirrors to siibra-api
+- experimental support for drag and drop swc
diff --git a/docs/releases/v2.7.4.md b/docs/releases/v2.7.4.md
new file mode 100644
index 0000000000000000000000000000000000000000..d214b2f110f3848759ec885f36821072f051b987
--- /dev/null
+++ b/docs/releases/v2.7.4.md
@@ -0,0 +1,6 @@
+# v2.7.4
+
+## Bugfix
+
+- Properly use fallback when detecting fault
+- Minor wording/cosmetic change
diff --git a/docs/releases/v2.7.5.md b/docs/releases/v2.7.5.md
new file mode 100644
index 0000000000000000000000000000000000000000..9ce31069890e6e1b5dea48881aba172bd4f0cc66
--- /dev/null
+++ b/docs/releases/v2.7.5.md
@@ -0,0 +1,24 @@
+# v2.7.5
+
+## Features
+
+- experimental support for parsing some swc files.
+- added toggle crosshair in helper one pager
+- allow navigation to VOI's native orientation plane
+- reworked plugin window
+  - it now longer act as if it is a separate window
+  - there are now three forms of the plugin: normal (default), maximized, minimized.
+
+## Bugfix
+
+- saneUrl were not able to correctly save user requests
+
+## Under the hood
+
+- enhancement in `strict local mode`
+  - hide external links in `strict local mode`
+  - do not try to perform interspace translation
+  - do not try to show feature pane
+- direct support for Leap motion controller
+- set git hash and version during build step (no longer requiring it to be set during build-arg step)
+- restores the functionality to parse `navigation` query param
diff --git a/e2e/checklist.md b/e2e/checklist.md
index b51cd1cdc38ce354cf1bc0ca97a6d06e9b3896b4..fe9d0cb0fa85b4f5eab53e808f4cec5ba91f7bae 100644
--- a/e2e/checklist.md
+++ b/e2e/checklist.md
@@ -14,6 +14,11 @@
 - [ ] Human multilevel atlas
   - [ ] on click from home page, MNI152, Julich v2.9 loads without issue
   - [ ] on hover, show correct region name(s)
+  - [ ] Parcellation smart chip
+    - [ ] show/hide parcellation toggle exists and works
+    - [ ] `q` is a shortcut to show/hide parcellation toggle
+    - [ ] info button exists and works
+    - [ ] info button shows desc, and link to KG
   - [ ] regional is fine :: select hOC1 right
     - [ ] probabilistic map loads fine
     - [ ] segmentation layer hides
@@ -60,6 +65,10 @@
   - [ ] on hover, show correct region name(s)
   - [ ] whole mesh loads
 ## saneURL
+- [ ] saneurl generation functions properly
+  - [ ] try existing key (human), and get unavailable error
+  - [ ] try non existing key, and get available
+  - [ ] create use key `x-tmp-foo` and new url works
 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/bigbrainGreyWhite) redirects to big brain
 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/julichbrain) redirects to julich brain (colin 27)
 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/whs4) redirects to waxholm v4
@@ -72,3 +81,4 @@
 - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/mouse) redirects allen mouse 2017
 ## plugins
 - [ ] jugex plugin works
+- [ ] 1um section works
diff --git a/mkdocs.yml b/mkdocs.yml
index 189e32dd064493291664f3af1487024f0294f79a..5372b5da4bf778115d93f5ac54c3894ab8141b15 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,4 +1,4 @@
-site_name: Interactive Atlas Viewer User Documentation
+site_name: Siibra Explorer User Documentation
 
 theme:
   name: 'material'
@@ -33,7 +33,13 @@ nav:
     - Fetching datasets: 'advanced/datasets.md'
     - Display non-atlas volumes: 'advanced/otherVolumes.md'
   - Release notes:
+    - v2.7.5: 'releases/v2.7.5.md'
+    - v2.7.4: 'releases/v2.7.4.md'
+    - v2.7.3: 'releases/v2.7.3.md'
+    - v2.7.2: 'releases/v2.7.2.md'
+    - v2.7.1: 'releases/v2.7.1.md'
     - v2.7.0: 'releases/v2.7.0.md'
+    - v2.6.10: 'releases/v2.6.10.md'
     - v2.6.9: 'releases/v2.6.9.md'
     - v2.6.8: 'releases/v2.6.8.md'
     - v2.6.7: 'releases/v2.6.7.md'
diff --git a/package.json b/package.json
index 7f4703d9003a841fe409c5fdc906ceab3b9fa160..64e82a9d397bf02c29c67f449ffd8467b89b1f2d 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,7 @@
 {
   "name": "interactive-viewer",
-  "version": "2.7.0",
+  "version": "2.7.5",
   "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular",
-  "version": "2.7.0",
   "scripts": {
     "lint": "eslint src --ext .ts",
     "eslint": "eslint",
diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts
index ec352e5fbd3707b63b4da42ca92e9157c36bfb0c..fd67601ee087cf32ddd230d7e42b3ca9fdaea421 100644
--- a/src/atlasComponents/annotations/annotation.service.ts
+++ b/src/atlasComponents/annotations/annotation.service.ts
@@ -146,7 +146,8 @@ export class AnnotationLayer {
     }
   }
   updateAnnotation(spec: AnnotationSpec) {
-    const localAnnotations = this.nglayer.layer.localAnnotations
+    const localAnnotations = this.nglayer?.layer?.localAnnotations
+    if (!localAnnotations) return
     const ref = localAnnotations.references.get(spec.id)
     const _spec = this.parseNgSpecType(spec)
     if (ref) {
diff --git a/src/atlasComponents/sapi/constants.ts b/src/atlasComponents/sapi/constants.ts
index e9822b4a39c4157b893cef83f6d11082a8475674..b31cfc1b14703de11f2c5e2efaee1cacb369347b 100644
--- a/src/atlasComponents/sapi/constants.ts
+++ b/src/atlasComponents/sapi/constants.ts
@@ -1,13 +1,16 @@
 export const IDS = {
   ATLAES: {
-    HUMAN: "juelich/iav/atlas/v1.0.0/1"
+    HUMAN: "juelich/iav/atlas/v1.0.0/1",
+    RAT: "minds/core/parcellationatlas/v1.0.0/522b368e-49a3-49fa-88d3-0870a307974a",
   },
   TEMPLATES: {
     BIG_BRAIN: "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588",
     MNI152: "minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2",
-    COLIN27: "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992"
+    COLIN27: "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992",
+    WAXHOLM: "minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8",
   },
   PARCELLATION: {
-    JBA29: "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290"
+    JBA29: "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290",
+    WAXHOLMV4: "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4",
   }
 }
diff --git a/src/atlasComponents/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts
index 4767cc2897ab0ed6c6567f3ee727ad2f73fece04..085c5a82b8136901136593aa0b2933d992efb22e 100644
--- a/src/atlasComponents/sapi/core/sapiParcellation.ts
+++ b/src/atlasComponents/sapi/core/sapiParcellation.ts
@@ -1,4 +1,5 @@
 import { Observable } from "rxjs"
+import { switchMap } from "rxjs/operators"
 import { SapiVolumeModel } from ".."
 import { SAPI } from "../sapi.service"
 import {SapiParcellationFeatureModel, SapiParcellationModel, SapiQueryPriorityArg, SapiRegionModel} from "../type"
@@ -20,43 +21,53 @@ export class SAPIParcellation{
   }
 
   getDetail(queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationModel>{
-    return this.sapi.httpGet<SapiParcellationModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`,
-      null,
-      queryParam
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiParcellationModel>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`,
+        null,
+        queryParam
+      ))
     )
   }
 
   getRegions(spaceId: string, queryParam?: SapiQueryPriorityArg): Observable<SapiRegionModel[]> {
-    return this.sapi.httpGet<SapiRegionModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`,
-      {
-        space_id: spaceId
-      },
-      queryParam
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiRegionModel[]>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`,
+        {
+          space_id: spaceId
+        },
+        queryParam
+      ))
     )
   }
   getVolumes(): Observable<SapiVolumeModel[]>{
-    return this.sapi.httpGet<SapiVolumeModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes`
-    )
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiVolumeModel[]>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes`
+      ))
+    ) 
   }
 
   getFeatures(parcPagination?: ParcellationPaginationQuery, queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationFeatureModel[]> {
-    return this.sapi.httpGet<SapiParcellationFeatureModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`,
-      {
-        type: parcPagination?.type,
-        size: parcPagination?.size?.toString() || '5',
-        page: parcPagination?.page.toString() || '0',
-      },
-      queryParam
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiParcellationFeatureModel[]>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`,
+        {
+          type: parcPagination?.type,
+          size: parcPagination?.size?.toString() || '5',
+          page: parcPagination?.page.toString() || '0',
+        },
+        queryParam
+      ))
     )
   }
 
   getFeatureInstance(instanceId: string): Observable<SapiParcellationFeatureModel> {
-    return this.sapi.http.get<SapiParcellationFeatureModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.http.get<SapiParcellationFeatureModel>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
+      ))
     )
   }
 }
diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts
index 8248992c16ef86a1bbd6e28c395b8dd08db9d66f..d9263cea03f0d7045e3a1f5043f8a4edc2209929 100644
--- a/src/atlasComponents/sapi/core/sapiRegion.ts
+++ b/src/atlasComponents/sapi/core/sapiRegion.ts
@@ -2,7 +2,7 @@ import { SAPI } from "..";
 import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset, SapiVolumeModel, PaginatedResponse } from "../type";
 import { strToRgb, hexToRgb } from 'common/util'
 import { merge, Observable, of } from "rxjs";
-import { catchError, map, scan } from "rxjs/operators";
+import { catchError, map, scan, switchMap } from "rxjs/operators";
 
 export class SAPIRegion{
 
@@ -16,7 +16,7 @@ export class SAPIRegion{
     return strToRgb(JSON.stringify(region))
   }
 
-  private prefix: string
+  private prefix$: Observable<string>
 
   constructor(
     private sapi: SAPI,
@@ -24,20 +24,26 @@ export class SAPIRegion{
     public parcId: string,
     public id: string,
   ){
-    this.prefix = `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}`
+    this.prefix$ = SAPI.BsEndpoint$.pipe(
+      map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}`)
+    )
   }
 
   getFeatures(spaceId: string): Observable<(SapiRegionalFeatureModel | CleanedIeegDataset)[]> {
     return merge(
-      this.sapi.httpGet<SapiRegionalFeatureModel[]>(
-        `${this.prefix}/features`,
-        {
-          space_id: spaceId
-        }
-      ).pipe(
-        catchError((err, obs) => {
-          return of([])
-        })
+      this.prefix$.pipe(
+        switchMap(prefix => 
+          this.sapi.httpGet<SapiRegionalFeatureModel[]>(
+            `${prefix}/features`,
+            {
+              space_id: spaceId
+            }
+          ).pipe(
+            catchError((err, obs) => {
+              return of([])
+            })
+          )
+        )
       ),
       spaceId
         ? this.sapi.getSpace(this.atlasId, spaceId).getFeatures({ parcellationId: this.parcId, region: this.id }).pipe(
@@ -56,50 +62,59 @@ export class SAPIRegion{
   }
 
   getFeatureInstance(instanceId: string, spaceId: string = null): Observable<SapiRegionalFeatureModel> {
-    return this.sapi.httpGet<SapiRegionalFeatureModel>(
-      `${this.prefix}/features/${encodeURIComponent(instanceId)}`,
-      {
-        space_id: spaceId
-      }
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiRegionalFeatureModel>(
+        `${prefix}/features/${encodeURIComponent(instanceId)}`,
+        {
+          space_id: spaceId
+        }
+      ))
     )
   }
 
   getMapInfo(spaceId: string): Observable<SapiRegionMapInfoModel> {
-    return this.sapi.http.get<SapiRegionMapInfoModel>(
-      `${this.prefix}/regional_map/info`,
-      {
-        params: {
-          space_id: spaceId
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.http.get<SapiRegionMapInfoModel>(
+        `${prefix}/regional_map/info`,
+        {
+          params: {
+            space_id: spaceId
+          }
         }
-      }
+      ))
     )
   }
 
-  getMapUrl(spaceId: string): string {
-    return `${this.prefix}/regional_map/map?space_id=${encodeURI(spaceId)}`
+  getMapUrl(spaceId: string): Observable<string> {
+    return this.prefix$.pipe(
+      map(prefix => `${prefix}/regional_map/map?space_id=${encodeURI(spaceId)}`)
+    )
   }
 
   getVolumes(): Observable<PaginatedResponse<SapiVolumeModel>>{
-    const url = `${this.prefix}/volumes`
-    return this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>(
-      url
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>(
+        `${prefix}/volumes`
+      ))
     )
   }
 
   getVolumeInstance(volumeId: string): Observable<SapiVolumeModel> {
-    const url = `${this.prefix}/volumes/${encodeURIComponent(volumeId)}`
-    return this.sapi.httpGet<SapiVolumeModel>(
-      url
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiVolumeModel>(
+        `${prefix}/volumes/${encodeURIComponent(volumeId)}`
+      ))
     )
   }
 
   getDetail(spaceId: string): Observable<SapiRegionModel> {
-    const url = `${this.prefix}/${encodeURIComponent(this.id)}`
-    return this.sapi.httpGet<SapiRegionModel>(
-      url,
-      {
-        space_id: spaceId
-      }
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiRegionModel>(
+        prefix,
+        {
+          space_id: spaceId
+        }
+      ))
     )
   }
 }
diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts
index 3effd6f16345aa82eebd3cf94b9f83118fb9546e..5f61ae6f68240437d2a9510b4744d64d16e54977 100644
--- a/src/atlasComponents/sapi/core/sapiSpace.ts
+++ b/src/atlasComponents/sapi/core/sapiSpace.ts
@@ -2,6 +2,7 @@ import { Observable } from "rxjs"
 import { SAPI } from '../sapi.service'
 import { camelToSnake } from 'common/util'
 import {SapiQueryPriorityArg, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel} from "../type"
+import { map, switchMap } from "rxjs/operators"
 
 type FeatureResponse = {
   features: {
@@ -22,13 +23,21 @@ type SpatialFeatureOpts = RegionalSpatialFeatureOpts | BBoxSpatialFEatureOpts
 
 export class SAPISpace{
 
-  constructor(private sapi: SAPI, public atlasId: string, public id: string){}
+  constructor(private sapi: SAPI, public atlasId: string, public id: string){
+    this.prefix$ = SAPI.BsEndpoint$.pipe(
+      map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`)
+    )
+  }
+
+  private prefix$: Observable<string>
 
   getModalities(param?: SapiQueryPriorityArg): Observable<FeatureResponse> {
-    return this.sapi.httpGet<FeatureResponse>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`,
-      null,
-      param
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<FeatureResponse>(
+        `${prefix}/features`,
+        null,
+        param
+      ))
     )
   }
 
@@ -37,9 +46,11 @@ export class SAPISpace{
     for (const [key, value] of Object.entries(opts)) {
       query[camelToSnake(key)] = value
     }
-    return this.sapi.httpGet<SapiSpatialFeatureModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`,
-      query
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel[]>(
+        `${prefix}/features`,
+        query
+      ))
     )
   }
 
@@ -48,23 +59,29 @@ export class SAPISpace{
     for (const [key, value] of Object.entries(opts)) {
       query[camelToSnake(key)] = value
     }
-    return this.sapi.httpGet<SapiSpatialFeatureModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
-      query
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel>(
+        `${prefix}/features/${encodeURIComponent(instanceId)}`,
+        query
+      ))
     )
   }
 
   getDetail(param?: SapiQueryPriorityArg): Observable<SapiSpaceModel>{
-    return this.sapi.httpGet<SapiSpaceModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`,
-      null,
-      param
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiSpaceModel>(
+        `${prefix}`,
+        null,
+        param
+      ))
     )
   }
 
   getVolumes(): Observable<SapiVolumeModel[]>{
-    return this.sapi.httpGet<SapiVolumeModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/volumes`,
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiVolumeModel[]>(
+        `${prefix}/volumes`,
+      ))
     )
   }
 }
diff --git a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts
index 1a0c0c690be0eb7615922a8068ffc86422af54fb..8dff3f67d6bbc69939068718270e487ee3bbeb2c 100644
--- a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts
+++ b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts
@@ -57,6 +57,12 @@ export class InterSpaceCoordXformSvc {
   // see https://github.com/ngrx/platform/issues/498#issuecomment-337465179
   // in order to properly test with marble, use obs instead of promise
   transform(srcTmplName: ValidTemplateSpaceName, targetTmplName: ValidTemplateSpaceName, coordinatesInNm: [number, number, number]): Observable<ITemplateCoordXformResp> {
+    if (environment.STRICT_LOCAL) {
+      return of({
+        status: 'error',
+        statusText: 'STRICT_LOCAL mode on, will not transform'
+      } as ITemplateCoordXformResp)
+    }
     if (!srcTmplName || !targetTmplName) {
       return of({
         status: 'error',
diff --git a/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts b/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts
new file mode 100644
index 0000000000000000000000000000000000000000..44c93c74186567e3967d2224cb4937ee391de215
--- /dev/null
+++ b/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts
@@ -0,0 +1,60 @@
+export const VALID_LINEAR_XFORM_SRC = {
+  CCF: "Allen Common Coordination Framework"
+}
+
+export const VALID_LINEAR_XFORM_DST = {
+  NEHUBA: "nehuba"
+}
+
+export type TVALID_LINEAR_XFORM_SRC = keyof typeof VALID_LINEAR_XFORM_SRC
+export type TVALID_LINEAR_XFORM_DST = keyof typeof VALID_LINEAR_XFORM_DST
+
+type TLinearXform = number[][]
+
+const _linearXformDict: Record<
+  keyof typeof VALID_LINEAR_XFORM_SRC,
+  Record<
+    keyof typeof VALID_LINEAR_XFORM_DST,
+    TLinearXform
+  >> = {
+    CCF: {
+      NEHUBA: [
+        [-1e3, 0, 0, 11400000 - 5737500], //
+        [0, 0, -1e3, 13200000 - 6637500], //
+        [0, -1e3, 0, 8000000 - 4037500], //
+        [0, 0, 0, 1],
+      ]
+    }
+  }
+
+const defaultXform = [
+  [1e3, 0, 0, 0],
+  [0, 1e3, 0, 0],
+  [0, 0, 1e3, 0],
+  [0, 0, 0, 1],
+]
+
+
+const getProxyXform = <T>(obj: Record<string, T>, cb: (value: T) => T) => new Proxy({}, {
+  get: (_target, prop: string, _receiver) => {
+    return cb(obj[prop])
+  }
+})
+
+export const linearXformDict = getProxyXform(_linearXformDict, (value: Record<string, TLinearXform>) => {
+  if (!value) return getProxyXform({}, () => defaultXform)
+  return getProxyXform(value, (v: TLinearXform) => {
+    if (v) return v
+    return defaultXform
+  })
+}) as Record<
+  keyof typeof VALID_LINEAR_XFORM_SRC,
+  Record<
+    keyof typeof VALID_LINEAR_XFORM_DST,
+    TLinearXform
+  >>
+
+
+export const linearTransform = async (srcTmplName: keyof typeof VALID_LINEAR_XFORM_SRC, targetTmplName: keyof typeof VALID_LINEAR_XFORM_DST) => {
+  return linearXformDict[srcTmplName][targetTmplName]
+}
diff --git a/src/atlasComponents/sapi/features/sapiFeature.ts b/src/atlasComponents/sapi/features/sapiFeature.ts
index 5abaa0040f6cb71046c70bf57e7a77a57f2794a8..f2f341acce3aca72ba481f3eda6c68083320cfe1 100644
--- a/src/atlasComponents/sapi/features/sapiFeature.ts
+++ b/src/atlasComponents/sapi/features/sapiFeature.ts
@@ -1,3 +1,4 @@
+import { switchMap } from "rxjs/operators";
 import { SAPI } from "../sapi.service";
 import { SapiFeatureModel } from "../type";
 
@@ -6,8 +7,10 @@ export class SAPIFeature {
 
   }
 
-  public detail$ = this.sapi.httpGet<SapiFeatureModel>(
-    `${SAPI.bsEndpoint}/features/${this.id}`,
-    this.opts
+  public detail$ = SAPI.BsEndpoint$.pipe(
+    switchMap(endpt => this.sapi.httpGet<SapiFeatureModel>(
+      `${endpt}/features/${this.id}`,
+      this.opts
+    ))
   )
 }
diff --git a/src/atlasComponents/sapi/module.ts b/src/atlasComponents/sapi/module.ts
index a64cc8bc817f05c801cde40d58a95d93c1d198a1..9b9efaf0ee92240c0b27b57b5067acf02dcbd972 100644
--- a/src/atlasComponents/sapi/module.ts
+++ b/src/atlasComponents/sapi/module.ts
@@ -1,5 +1,4 @@
 import { NgModule } from "@angular/core";
-import { SAPI } from "./sapi.service";
 import { CommonModule } from "@angular/common";
 import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
 import { PriorityHttpInterceptor } from "src/util/priority";
@@ -16,7 +15,6 @@ import { MatSnackBarModule } from "@angular/material/snack-bar";
   exports: [
   ],
   providers: [
-    SAPI,
     {
       provide: HTTP_INTERCEPTORS,
       useClass: PriorityHttpInterceptor,
diff --git a/src/atlasComponents/sapi/sapi.service.spec.ts b/src/atlasComponents/sapi/sapi.service.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..20233eca63ce2485db0136ee3702cace46659e96
--- /dev/null
+++ b/src/atlasComponents/sapi/sapi.service.spec.ts
@@ -0,0 +1,107 @@
+import { finalize } from "rxjs/operators"
+import * as env from "src/environments/environment"
+import { SAPI } from "./sapi.service"
+
+describe("> sapi.service.ts", () => {
+  describe("> SAPI", () => {
+    describe("#BsEndpoint$", () => {
+      let fetchSpy: jasmine.Spy
+      let environmentSpy: jasmine.Spy
+
+      const endpt1 = 'http://foo-bar'
+      const endpt2 = 'http://buzz-bizz'
+
+      const atlas1 = 'foo'
+      const atlas2 = 'bar'
+
+      let subscribedVal: string
+
+      beforeEach(() => {
+        SAPI.ClearBsEndPoint()
+        fetchSpy = spyOn(window, 'fetch')
+        fetchSpy.and.callThrough()
+
+        environmentSpy = spyOnProperty(env, 'environment')
+        environmentSpy.and.returnValue({
+          SIIBRA_API_ENDPOINTS: `${endpt1},${endpt2}`
+        })
+      })
+
+
+      afterEach(() => {
+        SAPI.ClearBsEndPoint()
+        fetchSpy.calls.reset()
+        environmentSpy.calls.reset()
+        subscribedVal = null
+      })
+
+      describe("> first passes", () => {
+        beforeEach(done => {
+          const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 })
+          fetchSpy.and.callFake(async url => {
+            if (url === `${endpt1}/atlases`) {
+              return resp
+            }
+            throw new Error("controlled throw")
+          })
+          SAPI.BsEndpoint$.pipe(
+            finalize(() => done())
+          ).subscribe(val => {
+            subscribedVal = val
+          })
+        })
+        it("> should call fetch twice", async () => {
+          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          
+          const allArgs = fetchSpy.calls.allArgs()
+          expect(allArgs.length).toEqual(2)
+          expect(allArgs[0]).toEqual([`${endpt1}/atlases`])
+          expect(allArgs[1]).toEqual([`${endpt2}/atlases`])
+        })
+
+        it("> endpoint should be set", async () => {
+          expect(subscribedVal).toBe(endpt1)
+        })
+
+        it("> additional calls should return cached observable", () => {
+
+          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          SAPI.BsEndpoint$.subscribe()
+          SAPI.BsEndpoint$.subscribe()
+
+          expect(fetchSpy).toHaveBeenCalledTimes(2)
+        })
+      })
+
+      describe("> first fails", () => {
+        beforeEach(done => {
+          fetchSpy.and.callFake(async url => {
+            if (url === `${endpt1}/atlases`) {
+              throw new Error(`bla`)
+            }
+            const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 })
+            return resp
+          })
+
+          SAPI.BsEndpoint$.pipe(
+            finalize(() => done())
+          ).subscribe(val => {
+            subscribedVal = val
+          })
+        })
+
+        it("> should call twice", async () => {
+          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          expect(fetchSpy.calls.allArgs()).toEqual([
+            [`${endpt1}/atlases`],
+            [`${endpt2}/atlases`],
+          ])
+        })
+
+        it('> should set endpt2', async () => {
+          expect(subscribedVal).toBe(endpt2)
+        })
+      })
+    })
+  })
+})
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index 62ba0e2528a8633db52bc44acdcadd3e9e851e04..ef3dc46e1e4dd37e1b50206f5b070bc562a270e5 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -1,9 +1,10 @@
 import { Injectable } from "@angular/core";
 import { HttpClient } from '@angular/common/http';
-import { map, shareReplay } from "rxjs/operators";
+import { catchError, filter, map, shareReplay, switchMap, take, tap } from "rxjs/operators";
 import { SAPIAtlas, SAPISpace } from './core'
 import {
-  SapiAtlasModel, SapiModalityModel,
+  SapiAtlasModel,
+  SapiModalityModel,
   SapiParcellationModel,
   SapiQueryPriorityArg,
   SapiRegionalFeatureModel,
@@ -19,20 +20,65 @@ import { MatSnackBar } from "@angular/material/snack-bar";
 import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
 import { EnumColorMapName } from "src/util/colorMaps";
 import { PRIORITY_HEADER } from "src/util/priority";
-import { Observable } from "rxjs";
+import { concat, EMPTY, from, merge, Observable, of } from "rxjs";
 import { SAPIFeature } from "./features";
 import { environment } from "src/environments/environment"
 
 export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version'
-export const SIIBRA_API_VERSION = '0.2.0'
+export const SIIBRA_API_VERSION = '0.2.2'
 
 type RegistryType = SAPIAtlas | SAPISpace | SAPIParcellation
 
-@Injectable()
+let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null
+
+@Injectable({
+  providedIn: 'root'
+})
 export class SAPI{
-  static bsEndpoint = environment.BS_REST_URL || `https://siibra-api-latest.apps-dev.hbp.eu/v2_0`
 
-  public bsEndpoint = SAPI.bsEndpoint
+  /**
+   * Used to clear BsEndPoint, so the next static get BsEndpoints$ will
+   * fetch again. Only used for unit test of BsEndpoint$
+   */
+  static ClearBsEndPoint(){
+    BS_ENDPOINT_CACHED_VALUE = null
+  }
+
+  /**
+   * BsEndpoint$ is designed as a static getter mainly for unit testing purposes.
+   * see usage of BsEndpoint$ and ClearBsEndPoint in sapi.service.spec.ts
+   */
+  static get BsEndpoint$(): Observable<string> {
+    if (!!BS_ENDPOINT_CACHED_VALUE) return BS_ENDPOINT_CACHED_VALUE
+    BS_ENDPOINT_CACHED_VALUE = concat(
+      merge(
+        ...environment.SIIBRA_API_ENDPOINTS.split(',').map(url => {
+          return from((async () => {
+            const resp = await fetch(`${url}/atlases`)
+            const atlases = await resp.json()
+            if (atlases.length == 0) {
+              throw new Error(`atlas length == 0`)
+            }
+            return url
+          })()).pipe(
+            catchError(() => EMPTY)
+          )
+        })
+      ),
+      of(null).pipe(
+        tap(() => {
+          SAPI.ErrorMessage = `It appears all of our mirrors are not working. The viewer may not be working properly...`
+        }),
+        filter(() => false)
+      )
+    ).pipe(
+      take(1),
+      shareReplay(1),
+    )
+    return BS_ENDPOINT_CACHED_VALUE
+  }
+
+  static ErrorMessage = null
   
   registry = {
     _map: {} as Record<string, {
@@ -92,7 +138,9 @@ export class SAPI{
   }
 
   getModalities(): Observable<SapiModalityModel[]> {
-    return this.http.get<SapiModalityModel[]>(`${SAPI.bsEndpoint}/modalities`)
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.http.get<SapiModalityModel[]>(`${endpt}/modalities`))
+    )
   }
 
   httpGet<T>(url: string, params?: Record<string, string>, sapiParam?: SapiQueryPriorityArg){
@@ -109,12 +157,13 @@ export class SAPI{
     )
   }
 
-  public atlases$ = this.http.get<SapiAtlasModel[]>(
-    `${this.bsEndpoint}/atlases`,
-    {
-      observe: "response"
-    }
-  ).pipe(
+  public atlases$ = SAPI.BsEndpoint$.pipe(
+    switchMap(endpt => this.http.get<SapiAtlasModel[]>(
+      `${endpt}/atlases`,
+      {
+        observe: "response"
+      }
+    )),
     map(resp => {
       const respVersion = resp.headers.get(SIIBRA_API_VERSION_HEADER_KEY)
       if (respVersion !== SIIBRA_API_VERSION) {
@@ -133,6 +182,9 @@ export class SAPI{
     private snackbar: MatSnackBar,
     private workerSvc: AtlasWorkerService,
   ){
+    if (SAPI.ErrorMessage) {
+      this.snackbar.open(SAPI.ErrorMessage, 'Dismiss', { duration: 5000 })
+    }
     this.atlases$.subscribe(atlases => {
       for (const atlas of atlases) {
         for (const space of atlas.spaces) {
diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts
index 51fde4f0cd5695f6055398e8bd9ec39ed469c541..57e28f20da83acf96a247117f70fb921998c3bf0 100644
--- a/src/atlasComponents/sapi/stories.base.ts
+++ b/src/atlasComponents/sapi/stories.base.ts
@@ -67,22 +67,27 @@ export const parcId = {
 }
 
 export async function getAtlases(): Promise<SapiAtlasModel[]> {
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases`)).json() as SapiAtlasModel[]
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases`)).json() as SapiAtlasModel[]
 }
 
 export async function getAtlas(id: string): Promise<SapiAtlasModel>{
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${id}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${id}`)).json()
 }
 
 export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/parcellations/${id}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}`)).json()
 }
 export async function getParcRegions(atlasId: string, id: string, spaceId: string): Promise<SapiRegionModel[]>{
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json()
 }
 
 export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> {
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/spaces/${id}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId}/spaces/${id}`)).json()
 }
 
 export async function getHumanAtlas(): Promise<SapiAtlasModel> {
@@ -90,7 +95,8 @@ export async function getHumanAtlas(): Promise<SapiAtlasModel> {
 }
 
 export async function getMni152(): Promise<SapiSpaceModel> {
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json()
 }
 
 export async function getJba29(): Promise<SapiParcellationModel> {
@@ -103,33 +109,41 @@ export async function getJba29Regions(): Promise<SapiRegionModel[]> {
 
 export async function getHoc1Right(spaceId=null): Promise<SapiRegionModel> {
   if (!spaceId) {
-    return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json()
+    const endPt = await SAPI.BsEndpoint$.toPromise()
+    return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json()
   }
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json()
 }
 
 export async function get44Left(spaceId=null): Promise<SapiRegionModel> {
   if (!spaceId) {
-    return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json()
+    const endPt = await SAPI.BsEndpoint$.toPromise()
+    return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json()
   }
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json()
 }
 
 export async function getHoc1RightSpatialFeatures(): Promise<SxplrCleanedFeatureModel[]> {
-  const json: SapiSpatialFeatureModel[] = await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9&region=hoc1%20right`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  const json: SapiSpatialFeatureModel[] = await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9&region=hoc1%20right`)).json()
   return cleanIeegSessionDatasets(json.filter(it => it['@type'] === "siibra/features/ieegSession"))
 }
 
 export async function getHoc1RightFeatures(): Promise<SapiRegionalFeatureModel[]> {
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json()
 }
 
 export async function getHoc1RightFeatureDetail(featId: string): Promise<SapiRegionalFeatureModel>{
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json()
 }
 
 export async function getJba29Features(): Promise<SapiParcellationFeatureModel[]> {
-  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json()
 }
 
 export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureModel[]>{
@@ -137,14 +151,16 @@ export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureMo
     [-1000, -1000, -1000],
     [1000, 1000, 1000]
   ]
-  const url = new URL(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`)
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`)
   url.searchParams.set(`bbox`, JSON.stringify(bbox))
   return await (await fetch(url.toString())).json()
 }
 
 export async function getMni152SpatialFeatureHoc1Right(): Promise<SapiSpatialFeatureModel[]>{
   
-  const url = new URL(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`)
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`)
   url.searchParams.set(`parcellation_id`, parcId.human.jba29)
   url.searchParams.set("region", 'hoc1 right')
   return await (await fetch(url.toString())).json()
diff --git a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts
index 5636ccdd3e36c4930bab5a923f1b20d4ec25507a..58a2151732937cd390d516ace5a2bfab2de87f7b 100644
--- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts
+++ b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts
@@ -104,13 +104,20 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector {
     )
   )
 
-  private showOverlayIntent$ = new Subject()
+  private showOverlayIntentByTemplate$ = new Subject()
+  private showOverlayIntentByParcellation$ = new Subject()
   public showLoadingOverlay$ = merge(
-    this.showOverlayIntent$.pipe(
+    this.showOverlayIntentByTemplate$.pipe(
       mapTo(true)
     ),
     this.selectedTemplate$.pipe(
       mapTo(false)
+    ),
+    this.showOverlayIntentByParcellation$.pipe(
+      mapTo(true)
+    ),
+    this.selectedParcellation$.pipe(
+      mapTo(false)
     )
   ).pipe(
     distinctUntilChanged(),
@@ -180,7 +187,7 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector {
   }
 
   selectTemplate(tmpl: SapiSpaceModel) {
-    this.showOverlayIntent$.next(true)
+    this.showOverlayIntentByTemplate$.next(true)
 
     this.store$.dispatch(
       atlasSelection.actions.selectTemplate({
@@ -190,6 +197,7 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector {
   }
 
   selectParcellation(parc: SapiParcellationModel) {
+    this.showOverlayIntentByParcellation$.next(true)
 
     this.store$.dispatch(
       atlasSelection.actions.selectParcellation({
diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts
index 08bdb9d55bd85a616914cae076797142f3ff1026..22813095a37d1c1b8b6a12b44003995d8b7e8db5 100644
--- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts
+++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts
@@ -1,15 +1,29 @@
-import { Component, Input } from "@angular/core";
+import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from "@angular/core";
 import { SapiDatasetModel } from "src/atlasComponents/sapi";
+import { CONST } from "common/constants"
+
+const RESTRICTED_ACCESS_ID = "https://nexus.humanbrainproject.org/v0/data/minds/core/embargostatus/v1.0.0/3054f80d-96a8-4dce-9b92-55c68a8b5efd"
 
 @Component({
   selector: `sxplr-sapiviews-core-datasets-dataset`,
   templateUrl: './dataset.template.html',
   styleUrls: [
     `./dataset.style.css`
-  ]
+  ],
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 
-export class DatasetView {
+export class DatasetView implements OnChanges{
   @Input('sxplr-sapiviews-core-datasets-dataset-input')
   dataset: SapiDatasetModel
+
+  public isRestricted = false
+  public CONST = CONST
+
+  ngOnChanges(changes: SimpleChanges): void {
+    const { dataset } = changes
+    if (dataset) {
+      this.isRestricted = (dataset.currentValue as SapiDatasetModel)?.metadata?.accessibility?.["@id"] === RESTRICTED_ACCESS_ID
+    }
+  }
 }
diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html
index 68ce4f9a8540b0c1154408328260ed2e00a07b5a..a04996afbb8d961cba682ed0cd1cc9d5b2940d31 100644
--- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html
+++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html
@@ -24,9 +24,16 @@
     <span class="sxplr-m-a">
       EBRAINS dataset
     </span>
+
+    <button *ngIf="isRestricted"
+      [matTooltip]="CONST.GDPR_TEXT"
+      mat-icon-button color="warn">
+      <i class="fas fa-exclamation-triangle"></i>
+    </button>
+
     <mat-divider class="sxplr-pl-1" [vertical]="true"></mat-divider>
 
-    <a mat-icon-button *ngFor="let url of dataset.urls" [href]="url.doi | parseDoi" target="_blank">
+    <a mat-icon-button sxplr-hide-when-local *ngFor="let url of dataset.urls" [href]="url.doi | parseDoi" target="_blank">
       <i class="fas fa-external-link-alt"></i>
     </a>
   </mat-card-subtitle>
diff --git a/src/atlasComponents/sapiViews/core/datasets/module.ts b/src/atlasComponents/sapiViews/core/datasets/module.ts
index b295da6bc25f1a9fdc98031a42810ba907013320..d7b43955b58f954fa02bbbf9000982e5ac382b41 100644
--- a/src/atlasComponents/sapiViews/core/datasets/module.ts
+++ b/src/atlasComponents/sapiViews/core/datasets/module.ts
@@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
 import { NgModule } from "@angular/core";
 import { MarkdownModule } from "src/components/markdown";
 import { AngularMaterialModule } from "src/sharedModules";
+import { StrictLocalModule } from "src/strictLocal";
 import { SapiViewsUtilModule } from "../../util/module";
 import { DatasetView } from "./dataset/dataset.component";
 
@@ -10,7 +11,8 @@ import { DatasetView } from "./dataset/dataset.component";
     CommonModule,
     AngularMaterialModule,
     MarkdownModule,
-    SapiViewsUtilModule
+    SapiViewsUtilModule,
+    StrictLocalModule,
   ],
   declarations: [
     DatasetView,
diff --git a/src/atlasComponents/sapiViews/core/index.ts b/src/atlasComponents/sapiViews/core/index.ts
index 6db4628176e571f0a50adca091dc02c5d51801de..cb3d0ffce18ea93d534e44866392f0fc819b0ae8 100644
--- a/src/atlasComponents/sapiViews/core/index.ts
+++ b/src/atlasComponents/sapiViews/core/index.ts
@@ -1,3 +1,7 @@
 export {
   SapiViewsCoreModule
-} from "./module"
\ No newline at end of file
+} from "./module"
+
+export {
+  SapiViewsCoreSpaceBoundingBox
+} from "./space"
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/core/parcellation/module.ts b/src/atlasComponents/sapiViews/core/parcellation/module.ts
index fca2367997a445d3ccb20593a6ce44cf3f1f9283..1fe2e70c09d9a1bda3e3fd3a991d76eac75be09b 100644
--- a/src/atlasComponents/sapiViews/core/parcellation/module.ts
+++ b/src/atlasComponents/sapiViews/core/parcellation/module.ts
@@ -4,10 +4,14 @@ import { Store } from "@ngrx/store";
 import { ComponentsModule } from "src/components";
 import { AngularMaterialModule } from "src/sharedModules";
 import { atlasAppearance } from "src/state";
+import { StrictLocalModule } from "src/strictLocal";
+import { DialogModule } from "src/ui/dialogInfo/module";
 import { UtilModule } from "src/util";
+import { SapiViewsUtilModule } from "../../util";
 import { SapiViewsCoreParcellationParcellationChip } from "./chip/parcellation.chip.component";
 import { FilterGroupedParcellationPipe } from "./filterGroupedParcellations.pipe";
 import { FilterUnsupportedParcPipe } from "./filterUnsupportedParc.pipe";
+import { ParcellationDoiPipe } from "./parcellationDoi.pipe";
 import { ParcellationIsBaseLayer } from "./parcellationIsBaseLayer.pipe";
 import { ParcellationVisibilityService } from "./parcellationVis.service";
 import { PreviewParcellationUrlPipe } from "./previewParcellationUrl.pipe";
@@ -20,6 +24,9 @@ import { SapiViewsCoreParcellationParcellationTile } from "./tile/parcellation.t
     ComponentsModule,
     AngularMaterialModule,
     UtilModule,
+    SapiViewsUtilModule,
+    DialogModule,
+    StrictLocalModule
   ],
   declarations: [
     SapiViewsCoreParcellationParcellationTile,
@@ -29,6 +36,7 @@ import { SapiViewsCoreParcellationParcellationTile } from "./tile/parcellation.t
     FilterGroupedParcellationPipe,
     FilterUnsupportedParcPipe,
     ParcellationIsBaseLayer,
+    ParcellationDoiPipe,
   ],
   exports: [
     SapiViewsCoreParcellationParcellationTile,
diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationDoi.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationDoi.pipe.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8652365e1bb40d6565ccfd9d88e30eb94492c6d4
--- /dev/null
+++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationDoi.pipe.ts
@@ -0,0 +1,18 @@
+import { Pipe, PipeTransform } from "@angular/core";
+import { SapiParcellationModel } from "src/atlasComponents/sapi/type";
+
+@Pipe({
+  name: 'parcellationDoiPipe',
+  pure: true
+})
+
+export class ParcellationDoiPipe implements PipeTransform {
+  public transform(parc: SapiParcellationModel): string[] {
+    const urls = (parc?.brainAtlasVersions || []).filter(
+      v => v.digitalIdentifier && v.digitalIdentifier['@type'] === 'https://openminds.ebrains.eu/core/DOI'
+    ).map(
+      v => v.digitalIdentifier['@id'] as string
+    )
+    return Array.from(new Set(urls))
+  }
+}
diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts
index 7bd6f31f13409a290cd1adf7e9062eb04cd4e591..2c3c09a9d9b7307a7a1f8b18c193565137e9bd2f 100644
--- a/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts
+++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts
@@ -2,9 +2,31 @@ import { Pipe, PipeTransform } from "@angular/core";
 import { SapiParcellationModel } from "src/atlasComponents/sapi/type";
 
 const baseLayerIds = [
+  /**
+   * julich brain
+   */
   "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290",
   "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25",
   "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579",
+
+  /**
+   * allen mouse
+   */
+  "minds/core/parcellationatlas/v1.0.0/05655b58-3b6f-49db-b285-64b5a0276f83",
+  "minds/core/parcellationatlas/v1.0.0/39a1384b-8413-4d27-af8d-22432225401f",
+
+  /**
+   * waxholm
+   */
+  "minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba",
+  "minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d",
+  "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe",
+  "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4",
+
+  /**
+   * monkey
+   */
+  "minds/core/parcellationatlas/v1.0.0/mebrains-tmp-id",
 ]
 
 @Pipe({
diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts
index 5a7eb1fc32405cd1a4f9128cfda54c018eb45766..1d446f159af3a6cb1e52fc322e560a8eca6ad01b 100644
--- a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts
+++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts
@@ -3,15 +3,19 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service"
 import { SapiParcellationModel } from "src/atlasComponents/sapi/type"
 import { getTraverseFunctions } from "./parcellationVersion.pipe"
 
-describe("parcellationVersion.pipe.ts", () => {
+describe(`parcellationVersion.pipe.ts`, () => {
   describe("getTraverseFunctions", () => {
     let julichBrainParcellations: SapiParcellationModel[] = []
+    let endpoint: string
     beforeAll(async () => {
-      const res = await fetch(`${SAPI.bsEndpoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`)
+      const bsEndPoint = await SAPI.BsEndpoint$.toPromise()
+      endpoint = bsEndPoint
+      const res = await fetch(`${bsEndPoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`)
       const arr: SapiParcellationModel[] = await res.json()
       julichBrainParcellations = arr.filter(it => /Julich-Brain Cytoarchitectonic Maps/.test(it.name))
     })
     it("> should be at least 3 parcellations", () => {
+      console.log(`testing against endpoint: ${endpoint}`)
       expect(julichBrainParcellations.length).toBeGreaterThanOrEqual(3)
     })
 
diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts
index f56ae3fdb4de939dad8b5ac6285f10a81e7cda8b..27d4a53cc941a95164407b86f61825c01df1527f 100644
--- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts
+++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts
@@ -1,9 +1,10 @@
-import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core";
-import { Observable } from "rxjs";
+import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges } from "@angular/core";
+import { BehaviorSubject, concat, Observable, of, timer } from "rxjs";
 import { SapiParcellationModel } from "src/atlasComponents/sapi/type";
 import { ParcellationVisibilityService } from "../parcellationVis.service";
 import { ARIA_LABELS } from "common/constants"
 import { getTraverseFunctions } from "../parcellationVersion.pipe";
+import { mapTo, shareReplay, switchMap } from "rxjs/operators";
 
 @Component({
   selector: `sxplr-sapiviews-core-parcellation-smartchip`,
@@ -37,7 +38,11 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges
 
   otherVersions: SapiParcellationModel[]
 
-  ngOnChanges() {
+  ngOnChanges(changes: SimpleChanges) {
+    const { parcellation } = changes
+    if (parcellation) {
+      this.onDismissClicked$.next(false)
+    }
     this.otherVersions = []
     if (!this.parcellation) {
       return
@@ -64,6 +69,16 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges
     }
   }
 
+  loadingParc$: Observable<SapiParcellationModel> = this.onSelectParcellation.pipe(
+    switchMap(parc => concat(
+      of(parc),
+      timer(5000).pipe(
+        mapTo(null)
+      ),
+    )),
+    shareReplay(1),
+  )
+
   parcellationVisibility$: Observable<boolean> = this.svc.visibility$
 
   toggleParcellationVisibility(){
@@ -71,11 +86,19 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges
   }
 
   dismiss(){
+    if (this.onDismissClicked$.value) return
+    this.onDismissClicked$.next(true)
     this.onDismiss.emit(this.parcellation)
   }
 
   selectParcellation(parc: SapiParcellationModel){
-    if (parc === this.parcellation) return
+    if (this.trackByFn(parc) === this.trackByFn(this.parcellation)) return
     this.onSelectParcellation.emit(parc)
   }
+
+  trackByFn(parc: SapiParcellationModel){
+    return parc["@id"]
+  }
+
+  onDismissClicked$ = new BehaviorSubject<boolean>(false)
 }
diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f83fb1f49984f554b1a3fbcea0852d7f8005949b 100644
--- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css
+++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css
@@ -0,0 +1,34 @@
+.otherversion-wrapper
+{
+  position: relative;
+  overflow: hidden;
+  margin: 0.5rem;
+}
+
+.otherversion-wrapper.loading > .spinner-container
+{
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+
+  display: flex;
+  align-items: center;
+}
+
+.otherversion-wrapper.loading > .spinner-container > spinner-cmp
+{
+  margin: 0.5rem;
+}
+
+.icons-container
+{
+  transform: scale(0.7);
+  margin-right: -1.5rem;
+}
+
+.icons-container > *
+{
+  margin: auto 0.2rem;
+}
diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html
index f792f5ce186c694e5b4d56f3335611c2ecc302b5..4dac117899e6a081d5d4309053c67d51ee9e714f 100644
--- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html
+++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html
@@ -1,21 +1,54 @@
 <mat-menu #otherParcMenu="matMenu"
   [hasBackdrop]="false"
-  class="sxplr-bg-none sxplr-of-x-hidden sxplr-box-shadow-none sxplr-mxw-80vw">
+  class="parc-smart-chip-menu-panel sxplr-bg-none sxplr-of-x-hidden sxplr-box-shadow-none sxplr-mxw-80vw">
   <div (iav-outsideClick)="menuTrigger.closeMenu()">
 
-    <sxplr-sapiviews-core-parcellation-chip *ngFor="let parc of otherVersions"
-      [sxplr-sapiviews-core-parcellation-chip-parcellation]="parc"
-      [sxplr-sapiviews-core-parcellation-chip-color]="parcellation === parc ? 'primary' : 'default'"
-      (sxplr-sapiviews-core-parcellation-chip-onclick)="selectParcellation(parc)">
+    <div *ngFor="let parc of otherVersions"
+      class="otherversion-wrapper"
+      [ngClass]="{
+        'loading': (loadingParc$ | async) === parc
+      }">
 
-    </sxplr-sapiviews-core-parcellation-chip>
+
+      <sxplr-sapiviews-core-parcellation-chip
+        [ngClass]="{
+          'sxplr-blink': (loadingParc$ | async) === parc
+        }"
+        [sxplr-sapiviews-core-parcellation-chip-parcellation]="parc"
+        [sxplr-sapiviews-core-parcellation-chip-color]="(parcellation | equality : parc : trackByFn) ? 'primary' : 'default'"
+        (sxplr-sapiviews-core-parcellation-chip-onclick)="selectParcellation(parc)">
+
+        <div class="sxplr-scale-70"
+          suffix 
+          iav-stop="mousedown click">
+
+          <ng-template #otherParcDesc>
+            <ng-template [ngTemplateOutlet]="parcDescTmpl"
+              [ngTemplateOutletContext]="{ parcellation: parc }">
+            </ng-template>
+          </ng-template>
+
+          <button mat-mini-fab color="default"
+            [sxplr-dialog]="otherParcDesc"
+            [sxplr-dialog-size]="null">
+            <i class="fas fa-info"></i>
+          </button>
+        </div>
+      </sxplr-sapiviews-core-parcellation-chip>
+
+      <div class="spinner-container" *ngIf="(loadingParc$ | async) === parc">
+        <spinner-cmp>
+        </spinner-cmp>
+      </div>
+    </div>
   </div>
   
 </mat-menu>
 
 <sxplr-sapiviews-core-parcellation-chip
   [ngClass]="{
-    'sxplr-muted': !(parcellationVisibility$ | async)
+    'sxplr-muted': !(parcellationVisibility$ | async),
+    'sxplr-blink': onDismissClicked$ | async
   }"
   class="sxplr-d-inline-block"
   [sxplr-sapiviews-core-parcellation-chip-parcellation]="parcellation"
@@ -25,9 +58,25 @@
   #menuTrigger="matMenuTrigger"
   >
 
-  <div prefix class="sxplr-scale-70">
-    <button mat-mini-fab
-      [color]="(parcellationVisibility$ | async) ? 'primary' : 'default'"
+  <div class="icons-container"
+    suffix 
+    iav-stop="mousedown click">
+
+    <ng-template #mainParcDesc>
+      <ng-template [ngTemplateOutlet]="parcDescTmpl"
+        [ngTemplateOutletContext]="{ parcellation: parcellation }">
+      </ng-template>
+    </ng-template>
+
+    <button mat-icon-button
+      color="default"
+      [sxplr-dialog]="mainParcDesc"
+      [sxplr-dialog-size]="null">
+      <i class="fas fa-info"></i>
+    </button>
+
+    <button mat-icon-button
+      color="default"
       [matTooltip]="ARIA_LABELS.TOGGLE_DELINEATION"
       iav-stop="mousedown click"
       [iav-key-listener]="[{'type': 'keydown', 'key': 'q', 'capture': true, 'target': 'document' }]"
@@ -41,16 +90,47 @@
         {{ ARIA_LABELS.TOGGLE_DELINEATION }}
       </span>
     </button>
-  </div>
 
-  <div *ngIf="!(parcellation | parcellationIsBaseLayer)"
-    class="sxplr-scale-70"
-    suffix>
     <button mat-mini-fab
-      color="primary"
-      iav-stop="mousedown click"
+      *ngIf="!(parcellation | parcellationIsBaseLayer)"
+      color="default"
       (click)="dismiss()">
-      <i class="fas fa-times"></i>
+
+      <spinner-cmp class="sxplr-w-100 sxplr-h-100" *ngIf="onDismissClicked$ | async; else defaultDismissIcon"></spinner-cmp>
+      <ng-template #defaultDismissIcon>
+        <i class="fas fa-times"></i>
+      </ng-template>
+
     </button>
   </div>
-</sxplr-sapiviews-core-parcellation-chip>
\ No newline at end of file
+</sxplr-sapiviews-core-parcellation-chip>
+
+<!-- parcellation description template -->
+
+<ng-template #parcDescTmpl let-parc="parcellation">
+  <h1 mat-dialog-title>
+    {{ parc.name }}
+  </h1>
+  <div mat-dialog-content>
+    <markdown-dom
+      *ngIf="parc.brainAtlasVersions.length > 0 && parc.brainAtlasVersions[0].versionInnovation"
+      [markdown]="parc.brainAtlasVersions[0].versionInnovation">
+    </markdown-dom>
+  </div>
+
+  <mat-dialog-actions align="start">
+    <a *ngFor="let url of parc | parcellationDoiPipe"
+      [href]="url"
+      sxplr-hide-when-local
+      target="_blank"
+      mat-raised-button
+      color="primary">
+      <div class="fas fa-external-link-alt"></div>
+      <span>
+        Dataset Detail
+      </span>
+    </a>
+
+    <button mat-button mat-dialog-close>Close</button>
+  </mat-dialog-actions>
+</ng-template>
diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts
index f0e19a9bc9a82c83dc8e3501ce3f2b99ff51b7bd..60fd2425cc9b9f67008b8caa8c386dcf32614bd8 100644
--- a/src/atlasComponents/sapiViews/core/region/module.ts
+++ b/src/atlasComponents/sapiViews/core/region/module.ts
@@ -1,7 +1,9 @@
 import { CommonModule } from "@angular/common";
 import { NgModule } from "@angular/core";
+import { MarkdownModule } from "src/components/markdown";
 import { SpinnerModule } from "src/components/spinner";
 import { AngularMaterialModule } from "src/sharedModules";
+import { StrictLocalModule } from "src/strictLocal";
 import { SapiViewsFeaturesModule } from "../../features";
 import { SapiViewsUtilModule } from "../../util/module";
 import { SapiViewsCoreRegionRegionChip } from "./region/chip/region.chip.component";
@@ -17,6 +19,8 @@ import { SapiViewsCoreRegionRegionRich } from "./region/rich/region.rich.compone
     SapiViewsUtilModule,
     SapiViewsFeaturesModule,
     SpinnerModule,
+    MarkdownModule,
+    StrictLocalModule,
   ],
   declarations: [
     SapiViewsCoreRegionRegionListItem,
diff --git a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts
index 39c1ccc4aa4429380aeae48ded57df5e68dfd3ac..96f374b267ced1fb700333654b7e62a39a9e311a 100644
--- a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts
+++ b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts
@@ -2,7 +2,7 @@ import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core
 import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi";
 import { rgbToHsl } from 'common/util'
 import { SAPI } from "src/atlasComponents/sapi/sapi.service";
-import { Subject } from "rxjs";
+import { BehaviorSubject, Subject } from "rxjs";
 import { SAPIRegion } from "src/atlasComponents/sapi/core";
 
 @Directive({
@@ -13,7 +13,8 @@ export class SapiViewsCoreRegionRegionBase {
 
   @Input('sxplr-sapiviews-core-region-detail-flag')
   shouldFetchDetail = false
-  public fetchInProgress = false
+
+  public fetchInProgress$ = new BehaviorSubject<boolean>(false)
 
   @Input('sxplr-sapiviews-core-region-atlas')
   atlas: SapiAtlasModel
@@ -37,7 +38,7 @@ export class SapiViewsCoreRegionRegionBase {
       this.setupRegionDarkmode()
       return
     }
-    this.fetchInProgress = true
+    this.fetchInProgress$.next(true)
     this._region = null
     
     this.fetchDetail(val)
@@ -49,7 +50,7 @@ export class SapiViewsCoreRegionRegionBase {
         this._region = val
       })
       .finally(() => {
-        this.fetchInProgress = false
+        this.fetchInProgress$.next(false)
         this.setupRegionDarkmode()
       })
   }
diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts
index 79e4d693d5b23cd7a380f26c6cc8e5b922bba450..0d506febda44ca7366f7366a622403b06a488228 100644
--- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts
+++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts
@@ -5,6 +5,7 @@ import { SapiViewsCoreRegionRegionBase } from "../region.base.directive";
 import { ARIA_LABELS, CONST } from 'common/constants'
 import { SapiRegionalFeatureModel } from "src/atlasComponents/sapi";
 import { SAPI } from "src/atlasComponents/sapi/sapi.service";
+import { environment } from "src/environments/environment";
 
 @Component({
   selector: 'sxplr-sapiviews-core-region-region-rich',
@@ -17,7 +18,8 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service";
 
 export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase {
   
-  shouldFetchDetail = true
+  public environment = environment
+  public shouldFetchDetail = true
   public ARIA_LABELS = ARIA_LABELS
   public CONST = CONST
 
diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html
index 22ebc5640a5a7c9787f51d44e4cdfd9068558018..953ac5d04f7a1e89c439c33d952c99ce925f3d48 100644
--- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html
+++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html
@@ -4,128 +4,145 @@
 
 <ng-template [ngIf]="region">
 
-<mat-card class="mat-elevation-z4">
-  <div 
-    [style.backgroundColor]="regionRgbString"
-    class="vanishing-border"
-    [ngClass]="{
-      'darktheme': regionDarkmode === true,
-      'lighttheme': regionDarkmode === false
-    }">
+  <mat-card class="mat-elevation-z4">
+    <div
+      [style.backgroundColor]="regionRgbString"
+      class="vanishing-border"
+      [ngClass]="{
+        'darktheme': regionDarkmode === true,
+        'lighttheme': regionDarkmode === false
+      }">
 
-    <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template>
-
-    <mat-card-title class="sxplr-custom-cmp text">
-      {{ region.name }}
-    </mat-card-title>
-
-
-    <!-- subtitle on what it is -->
-    <mat-card-subtitle class="d-inline-flex align-items-center flex-wrap">
-      <mat-icon fontSet="fas" fontIcon="fa-brain"></mat-icon>
-      <span>
-        Brain region
-      </span>
-
-      <!-- origin datas format -->
-      
-      <mat-divider vertical="true" class="sxplr-pl-2 h-2rem"></mat-divider>
-
-      <!-- position -->
-      <button mat-icon-button *ngIf="regionPosition"
-        (click)="navigateTo(regionPosition)"
-        [matTooltip]="ARIA_LABELS.GO_TO_REGION_CENTROID + ': ' + (regionPosition | numbers | addUnitAndJoin : 'mm')">
-        <mat-icon fontSet="fas" fontIcon="fa-map-marked-alt">
-        </mat-icon>
-      </button>
-
-      <!-- explore doi -->
-      <a *ngFor="let doi of dois"
-        [href]="doi | parseDoi"
-        [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG"
-        target="_blank"
-        mat-icon-button>
-        <i class="fas fa-external-link-alt"></i>
-      </a>
-
-    </mat-card-subtitle>
-
-  </div>
-</mat-card>
-
-
-<!-- kg regional features list -->
-<ng-template #kgRegionalFeatureList>
-  <div sxplr-sapiviews-core-region-regional-feature
-    [sxplr-sapiviews-core-region-atlas]="atlas"
-    [sxplr-sapiviews-core-region-template]="template"
-    [sxplr-sapiviews-core-region-parcellation]="parcellation"
-    [sxplr-sapiviews-core-region-region]="region"
-    #rfDir="sapiViewsRegionalFeature"
-    class="feature-list-container"
-    >
-
-    <spinner-cmp *ngIf="rfDir.busy$ | async"></spinner-cmp>
-
-    <sxplr-sapiviews-features-entry-list-item
-      *ngFor="let feat of rfDir.listOfFeatures$ | async"
-      [sxplr-sapiviews-features-entry-list-item-feature]="feat"
-      (click)="handleRegionalFeatureClicked(feat)">
-    </sxplr-sapiviews-features-entry-list-item>
-  </div>
-  
-</ng-template>
+      <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template>
 
+      <mat-card-title class="sxplr-custom-cmp text">
+        {{ region.name }}
+      </mat-card-title>
 
 
+      <!-- subtitle on what it is -->
+      <mat-card-subtitle class="d-inline-flex align-items-center flex-wrap">
+        <mat-icon fontSet="fas" fontIcon="fa-brain"></mat-icon>
+        <span>
+          Brain region
+        </span>
 
-<mat-accordion class="d-block mt-2">
+        <!-- origin datas format -->
+
+        <mat-divider vertical="true" class="sxplr-pl-2 h-2rem"></mat-divider>
+
+        <!-- position -->
+        <button mat-icon-button *ngIf="regionPosition"
+          (click)="navigateTo(regionPosition)"
+          [matTooltip]="ARIA_LABELS.GO_TO_REGION_CENTROID + ': ' + (regionPosition | numbers | addUnitAndJoin : 'mm')">
+          <mat-icon fontSet="fas" fontIcon="fa-map-marked-alt">
+          </mat-icon>
+        </button>
+
+        <!-- explore doi -->
+        <a *ngFor="let doi of dois"
+          [href]="doi | parseDoi"
+          sxplr-hide-when-local
+          [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG"
+          target="_blank"
+          mat-icon-button>
+          <i class="fas fa-external-link-alt"></i>
+        </a>
+
+      </mat-card-subtitle>
+
+    </div>
+  </mat-card>
+
+
+  <!-- kg regional features list -->
+  <ng-template #kgRegionalFeatureList>
+    <div sxplr-sapiviews-core-region-regional-feature
+      [sxplr-sapiviews-core-region-atlas]="atlas"
+      [sxplr-sapiviews-core-region-template]="template"
+      [sxplr-sapiviews-core-region-parcellation]="parcellation"
+      [sxplr-sapiviews-core-region-region]="region"
+      #rfDir="sapiViewsRegionalFeature"
+      class="feature-list-container"
+      >
+
+      <spinner-cmp *ngIf="rfDir.busy$ | async"></spinner-cmp>
+
+      <sxplr-sapiviews-features-entry-list-item
+        *ngFor="let feat of rfDir.listOfFeatures$ | async | orderFilterFeatures"
+        [sxplr-sapiviews-features-entry-list-item-feature]="feat"
+        (click)="handleRegionalFeatureClicked(feat)">
+      </sxplr-sapiviews-features-entry-list-item>
+    </div>
+    
+  </ng-template>
 
-  <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: {
-    title: CONST.REGIONAL_FEATURES,
-    iconClass: 'fas fa-database',
-    content: kgRegionalFeatureList,
-    desc: '',
-    iconTooltip: 'Regional Features',
-    iavNgIf: true
-  }">
-  </ng-container>
+  <ng-template #regionDesc>
+    <markdown-dom class="sxplr-muted" [markdown]="region?.versionInnovation || 'No description provided.'">
+    </markdown-dom>
+  </ng-template>
 
-</mat-accordion>
+  <mat-accordion class="d-block mt-2">
 
-<mat-accordion class="d-block mt-2">
+    <!-- desc -->
+    <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: {
+      title: CONST.DESCRIPTION,
+      iconClass: 'fas fa-info',
+      content: regionDesc,
+      desc: '',
+      iconTooltip: 'Description',
+      iavNgIf: !!region?.versionInnovation
+    }">
 
-  <!-- connectivity -->
-  <ng-template #sxplrSapiviewsFeaturesConnectivityBrowser>
-    <sxplr-sapiviews-features-connectivity-browser class="pe-all flex-shrink-1"
-                                                   [region]="region"
-                                                   [types]="hasConnectivityDirective.availableModalities"
-                                                   [defaultProfile]="hasConnectivityDirective.defaultProfile"
-                                                   [sxplr-sapiviews-features-connectivity-browser-atlas]="atlas"
-                                                   [sxplr-sapiviews-features-connectivity-browser-parcellation]="parcellation"
-                                                   [accordionExpanded]="expandedPanel === CONST.CONNECTIVITY"
-    >
-    </sxplr-sapiviews-features-connectivity-browser>
-  </ng-template>
+    </ng-container>
+
+    <!-- only show dynamic data when strict-local is set to false -->
+    <ng-template [ngIf]="!environment.STRICT_LOCAL">
+
+      <!-- feature list -->
+      <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: {
+        title: CONST.REGIONAL_FEATURES,
+        iconClass: 'fas fa-database',
+        content: kgRegionalFeatureList,
+        desc: '',
+        iconTooltip: 'Regional Features',
+        iavNgIf: true
+      }">
+      </ng-container>
+
+      <!-- connectivity -->
+      <ng-template #sxplrSapiviewsFeaturesConnectivityBrowser>
+        <sxplr-sapiviews-features-connectivity-browser
+          class="pe-all flex-shrink-1"
+          [region]="region"
+          [types]="hasConnectivityDirective.availableModalities"
+          [defaultProfile]="hasConnectivityDirective.defaultProfile"
+          [sxplr-sapiviews-features-connectivity-browser-atlas]="atlas"
+          [sxplr-sapiviews-features-connectivity-browser-parcellation]="parcellation"
+          [accordionExpanded]="expandedPanel === CONST.CONNECTIVITY"
+        >
+        </sxplr-sapiviews-features-connectivity-browser>
+      </ng-template>
+
+      <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: {
+        title: CONST.CONNECTIVITY,
+        iconClass: 'fab fa-connectdevelop',
+        content: sxplrSapiviewsFeaturesConnectivityBrowser,
+        desc: hasConnectivityDirective.connectivityNumber,
+        iconTooltip: hasConnectivityDirective.connectivityNumber + 'Connections',
+        iavNgIf: hasConnectivityDirective.hasConnectivity
+      }">
+      </ng-container>
+
+      <div sxplr-sapiviews-features-connectivity-check
+          [sxplr-sapiviews-features-connectivity-check-atlas]="atlas"
+          [sxplr-sapiviews-features-connectivity-check-parcellation]="parcellation"
+          [region]="region"
+          #hasConnectivityDirective="hasConnectivityDirective">
+      </div>
+    </ng-template>
 
-  <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: {
-    title: CONST.CONNECTIVITY,
-    iconClass: 'fab fa-connectdevelop',
-    content: sxplrSapiviewsFeaturesConnectivityBrowser,
-    desc: hasConnectivityDirective.connectivityNumber,
-    iconTooltip: hasConnectivityDirective.connectivityNumber + 'Connections',
-    iavNgIf: hasConnectivityDirective.hasConnectivity
-  }">
-  </ng-container>
-
-  <div sxplr-sapiviews-features-connectivity-check
-       [sxplr-sapiviews-features-connectivity-check-atlas]="atlas"
-       [sxplr-sapiviews-features-connectivity-check-parcellation]="parcellation"
-       [region]="region"
-       #hasConnectivityDirective="hasConnectivityDirective">
-  </div>
-
-</mat-accordion>
+  </mat-accordion>
 
 </ng-template>
 
diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts
index 8dbfce6e38bba1201cdd7d94adc310c0381cf1de..5dca7cbc394a3baa2360349fb9aafa7c08d6c5bc 100644
--- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts
+++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts
@@ -117,10 +117,16 @@ const asyncLoader = async () => {
     const atlasDetail = await getAtlas(atlasId[species])
     regionsDict[species] = {}
     
-    for (const parc of atlasDetail.parcellations) {
-      const parcDetail = await getParc(atlasDetail['@id'], parc['@id'])
-      regionsDict[species][parcDetail.name] = await getParcRegions(atlasDetail['@id'], parc['@id'], atlasDetail.spaces[0]["@id"] )
-    }
+    await Promise.all(
+      atlasDetail.parcellations.map(async parc => {
+        try {
+          const parcDetail = await getParc(atlasDetail['@id'], parc['@id'])
+          regionsDict[species][parcDetail.name] = await getParcRegions(atlasDetail['@id'], parc['@id'], atlasDetail.spaces[0]["@id"] )
+        } catch (e) {
+          console.warn(`fetching region detail for ${parc["@id"]} failed... Skipping...`)
+        }
+      })
+    )
   }
 
   return {
diff --git a/src/atlasComponents/sapiViews/core/space/index.ts b/src/atlasComponents/sapiViews/core/space/index.ts
index 46f783b69e03bdae2ef01144ba731e0b264c1c12..26c7eed07b1e454d4eac3bcbdf0c77bb9fe4f162 100644
--- a/src/atlasComponents/sapiViews/core/space/index.ts
+++ b/src/atlasComponents/sapiViews/core/space/index.ts
@@ -1 +1,4 @@
-export { SapiViewsCoreSpaceModule } from "./module"
\ No newline at end of file
+export { SapiViewsCoreSpaceModule } from "./module"
+export {
+  SapiViewsCoreSpaceBoundingBox
+} from "./boundingBox.directive"
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts
index 3889c4c3f20621280a66f92f3e37f2a4521414d0..b1efcd694501bd33796a961449fbb3fec244b28c 100644
--- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts
+++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts
@@ -6,7 +6,6 @@ import {CUSTOM_ELEMENTS_SCHEMA, Directive, Input} from "@angular/core";
 import {provideMockActions} from "@ngrx/effects/testing";
 import {MockStore, provideMockStore} from "@ngrx/store/testing";
 import {Observable, of} from "rxjs";
-import {BS_ENDPOINT} from "src/util/constants";
 import {SAPI} from "src/atlasComponents/sapi";
 import {AngularMaterialModule} from "src/sharedModules";
 
@@ -66,10 +65,6 @@ describe('ConnectivityComponent', () => {
             providers: [
                 provideMockActions(() => actions$),
                 provideMockStore(),
-                {
-                    provide: BS_ENDPOINT,
-                    useValue: MOCK_BS_ENDPOINT
-                },
                 {
                     provide: SAPI,
                     useValue: {
diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts
index 23d89f519a537c5a296ba90bd1dfd771bc17a3d5..2399e2b668ab21186da82384bd0f2138ec095ff2 100644
--- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts
+++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts
@@ -216,10 +216,8 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy {
     // ToDo this temporary fix is for the bug existing on siibra api https://github.com/FZJ-INM1-BDA/siibra-api/issues/100
     private fixDatasetFormat = (ds) =>  ds.name.includes('{')? ({
       ...ds,
-      name: ds.name.substr(0, ds.name.indexOf('{')),
-      dataset: JSON.parse(ds.name.substring(ds.name.indexOf('{')).replace(/'/g, '"'))
+      ...JSON.parse(ds.name.substring(ds.name.indexOf('{')).replace(/'/g, '"'))
     }) : ds
-    
 
     fetchConnectivity() {
       this.sapi.getParcellation(this.atlas["@id"], this.parcellation["@id"]).getFeatureInstance(this.selectedDataset['@id'])
@@ -232,7 +230,7 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy {
           this.fetching = false
         })
     }
-    
+
     setMatrixData(data) {
       const matrixData = data as SapiParcellationFeatureMatrixModel
       this.regionIndexInMatrix = (matrixData.columns as Array<string>).findIndex(md => md === this.regionName)
@@ -248,7 +246,7 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy {
         .then(matrix => {
           const regionProfile = matrix.rawArray[this.regionIndexInMatrix]
 
-          const maxStrength = Math.max(...regionProfile)  
+          const maxStrength = Math.max(...regionProfile)
           this.logChecked = maxStrength > 1
           this.logDisabled = maxStrength <= 1
 
@@ -264,7 +262,7 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy {
         })
     }
 
-    
+
     changeLog(checked: boolean) {
       this.logChecked = checked
       this.connectedAreas.next(this.formatConnections(this.pureConnections))
@@ -272,13 +270,18 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy {
       this.setCustomLayer()
     }
 
-    //ToDo navigateRegion action does not work any more
+    //ToDo bestViewPoint is null for the most cases
     navigateToRegion(region: SapiRegionModel) {
-      this.store$.dispatch(
-        atlasSelection.actions.navigateToRegion({
-          region
-        })
-      )
+      const regionCentroid = this.region.hasAnnotation?.bestViewPoint?.coordinates
+      if (regionCentroid)
+        this.store$.dispatch(
+          atlasSelection.actions.navigateTo({
+            navigation: {
+              position: regionCentroid.map(v => v.value*1e6),
+            },
+            animation: true
+          })
+        )
     }
 
     getRegionWithName(region: string) {
diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html
index 724f131a5ebcb37af35bbee1a3c30f74b57d345a..fae388859e866bb9de1439337ef741d41ede85fa 100644
--- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html
+++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html
@@ -62,10 +62,11 @@
         theme="dark">
     </hbp-connectivity-matrix-row>
     <div *ngIf="noConnectivityForRegion">No connectivity for the region.</div>
+
     <full-connectivity-grid #fullConnectivityGrid
                             [matrix]="matrixString"
-                            [datasetName]="selectedDataset?.dataset?.name"
-                            [datasetDescription]="selectedDataset?.dataset?.description"
+                            [datasetName]="selectedDataset?.name"
+                            [datasetDescription]="selectedDataset?.description"
                             only-export="true">
     </full-connectivity-grid>
 
diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts
index 3b915ff248a21b0cf19d3dc5fbac8131b2d21643..8008affe34a6b783fd1b93ed14ee527279e36d04 100644
--- a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts
+++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input } from "@angular/core";
+import { ChangeDetectionStrategy, Component, Input } from "@angular/core";
 import { SapiFeatureModel } from "src/atlasComponents/sapi";
 import { CleanedIeegDataset, CLEANED_IEEG_DATASET_TYPE, SapiDatasetModel, SapiParcellationFeatureMatrixModel, SapiRegionalFeatureReceptorModel, SapiSerializationErrorModel, SapiVOIDataResponse, SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type";
 
@@ -7,7 +7,8 @@ import { CleanedIeegDataset, CLEANED_IEEG_DATASET_TYPE, SapiDatasetModel, SapiPa
   templateUrl: `./entryListItem.template.html`,
   styleUrls: [
     `./entryListItem.style.css`
-  ]
+  ],
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 
 export class SapiViewsFeaturesEntryListItem{
diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html
index 76f584328b3e46b5a240defb44e98e7cc2abe4a2..fc1ac7920b3599f52aafb071417eb6bb41c0d6e1 100644
--- a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html
+++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html
@@ -3,7 +3,7 @@
 
   <mat-chip-list
     *ngIf="feature | featureBadgeFlag"
-    class="sxplr-scale-80 transform-origin-left-center">
+    class="sxplr-scale-80 transform-origin-left-center sxplr-pe-none">
     <mat-chip
       [color]="feature | featureBadgeColour"
       selected>
diff --git a/src/atlasComponents/sapiViews/features/index.ts b/src/atlasComponents/sapiViews/features/index.ts
index 19e30dcb1873aa37868be5e8944eaad3a9b0ce05..89e1fafbad8108576c708d401326b2d5f4137f6d 100644
--- a/src/atlasComponents/sapiViews/features/index.ts
+++ b/src/atlasComponents/sapiViews/features/index.ts
@@ -1,3 +1,7 @@
 export {
   SapiViewsFeaturesModule
-} from "./module"
\ No newline at end of file
+} from "./module"
+
+export {
+  SapiViewsFeaturesVoiQuery
+} from "./voi"
diff --git a/src/atlasComponents/sapiViews/features/module.ts b/src/atlasComponents/sapiViews/features/module.ts
index cbf7aeacaeccaf721bb6ca55108b01366f3fa685..3df348c72f79be4b53c3050e30627c896e8b93f2 100644
--- a/src/atlasComponents/sapiViews/features/module.ts
+++ b/src/atlasComponents/sapiViews/features/module.ts
@@ -11,6 +11,7 @@ import * as ieeg from "./ieeg"
 import * as receptor from "./receptors"
 import {SapiViewsFeatureConnectivityModule} from "src/atlasComponents/sapiViews/features/connectivity";
 import * as voi from "./voi"
+import { OrderFilterFeaturesPipe } from "./orderFilterFeatureList.pipe"
 
 const {
   SxplrSapiViewsFeaturesIeegModule
@@ -35,6 +36,7 @@ const { SapiViewsFeaturesVoiModule } = voi
     FeatureBadgeColourPipe,
     FeatureBadgeFlagPipe,
     SapiViewsFeaturesEntryListItem,
+    OrderFilterFeaturesPipe,
   ],
   providers: [
     {
@@ -48,6 +50,7 @@ const { SapiViewsFeaturesVoiModule } = voi
     SapiViewsFeaturesEntryListItem,
     SapiViewsFeaturesVoiModule,
     SapiViewsFeatureConnectivityModule,
+    OrderFilterFeaturesPipe,
   ]
 })
 export class SapiViewsFeaturesModule{}
diff --git a/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts b/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts
new file mode 100644
index 0000000000000000000000000000000000000000..382862b0300f4bdaffadce20aeb4e28274a8bef6
--- /dev/null
+++ b/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts
@@ -0,0 +1,37 @@
+import { Pipe, PipeTransform } from "@angular/core";
+import { CLEANED_IEEG_DATASET_TYPE, SapiFeatureModel, SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type";
+import { environment } from "src/environments/environment"
+
+type PipableFeatureType = SapiFeatureModel | SxplrCleanedFeatureModel
+
+type ArrayOperation<T extends boolean | number> = (input: PipableFeatureType) => T
+
+const FILTER_FN: ArrayOperation<boolean> = feature => {
+  return feature["@type"] !== "siibra/features/cells"
+}
+
+const ORDER_LIST: ArrayOperation<number> = feature => {
+  if (feature["@type"] === "siibra/features/receptor") return -4
+  if (feature["@type"] === CLEANED_IEEG_DATASET_TYPE) return -3
+  if (feature['@type'] === "https://openminds.ebrains.eu/core/DatasetVersion") return 2
+  return 0
+}
+
+@Pipe({
+  name: 'orderFilterFeatures',
+  pure: true
+})
+
+export class OrderFilterFeaturesPipe implements PipeTransform{
+  public transform(inputFeatures: PipableFeatureType[]): PipableFeatureType[] {
+    return inputFeatures
+      .filter(f => {
+        /**
+         * if experimental flag is set, do not filter out anything
+         */
+        if (environment.EXPERIMENTAL_FEATURE_FLAG) return true
+        return FILTER_FN(f)
+      })
+      .sort((a, b) => ORDER_LIST(a) - ORDER_LIST(b))
+  }
+}
diff --git a/src/atlasComponents/sapiViews/features/receptors/module.ts b/src/atlasComponents/sapiViews/features/receptors/module.ts
index d5bd04dc6b11f71c1646b4c8b2892c6ddf372f6b..34f29d7f07be5b5df7ddfaad65d9f2c6423fbb04 100644
--- a/src/atlasComponents/sapiViews/features/receptors/module.ts
+++ b/src/atlasComponents/sapiViews/features/receptors/module.ts
@@ -32,21 +32,6 @@ import { Profile } from "./profile/profile.component"
     Profile,
     Entry,
   ],
-  providers: [{
-    provide: APP_INITIALIZER,
-    multi: true,
-    useFactory: (appendScriptFn: (url: string) => Promise<any>) => {
-
-      const libraries = [
-        'https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js',
-        'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.1.2/es5/tex-svg.js'
-      ]
-      return () => Promise.all(libraries.map(appendScriptFn))
-    },
-    deps: [
-      APPEND_SCRIPT_TOKEN
-    ]
-  }],
   schemas: [
     CUSTOM_ELEMENTS_SCHEMA,
   ]
diff --git a/src/atlasComponents/sapiViews/util/equality.pipe.ts b/src/atlasComponents/sapiViews/util/equality.pipe.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8251d641fed51ac99384c6470af5623e0c37451c
--- /dev/null
+++ b/src/atlasComponents/sapiViews/util/equality.pipe.ts
@@ -0,0 +1,16 @@
+import { Pipe } from "@angular/core";
+
+type TTrackBy<T, O> = (input: T) => O
+
+const defaultTrackBy: TTrackBy<unknown, unknown> = i => i
+
+@Pipe({
+  name: 'equality',
+  pure: true
+})
+
+export class EqualityPipe<T>{
+  public transform(c1: T, c2: T, trackBy: TTrackBy<T, unknown> = defaultTrackBy): boolean {
+    return trackBy(c1) === trackBy(c2)
+  }
+}
diff --git a/src/atlasComponents/sapiViews/util/module.ts b/src/atlasComponents/sapiViews/util/module.ts
index 4a9eddaf8ef6cb42a5b0b5b1bec270fac766f82b..53a3a88c549821dfe9a7ee654c93dac7747cd368 100644
--- a/src/atlasComponents/sapiViews/util/module.ts
+++ b/src/atlasComponents/sapiViews/util/module.ts
@@ -1,5 +1,6 @@
 import { NgModule } from "@angular/core";
 import { AddUnitAndJoin } from "./addUnitAndJoin.pipe";
+import { EqualityPipe } from "./equality.pipe";
 import { IncludesPipe } from "./includes.pipe";
 import { NumbersPipe } from "./numbers.pipe";
 import { ParcellationSupportedInCurrentSpace } from "./parcellationSupportedInCurrentSpace.pipe";
@@ -9,6 +10,7 @@ import { SpaceSupportedInCurrentParcellationPipe } from "./spaceSupportedInCurre
 
 @NgModule({
   declarations: [
+    EqualityPipe,
     ParseDoiPipe,
     NumbersPipe,
     AddUnitAndJoin,
@@ -18,6 +20,7 @@ import { SpaceSupportedInCurrentParcellationPipe } from "./spaceSupportedInCurre
     SpaceSupportedInCurrentParcellationPipe,
   ],
   exports: [
+    EqualityPipe,
     ParseDoiPipe,
     NumbersPipe,
     AddUnitAndJoin,
diff --git a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts
index f16e6a0a82d991ca8955e84f9ed4a15fe14e2ab4..42eec193e93fcd29dcf576c45135bf0b0f892eef 100644
--- a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts
+++ b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts
@@ -1,5 +1,5 @@
 import { Pipe, PipeTransform } from "@angular/core";
-import { Observable, of } from "rxjs";
+import { NEVER, Observable, of } from "rxjs";
 import { map } from "rxjs/operators";
 import { SAPIParcellation } from "src/atlasComponents/sapi/core";
 import { SAPI } from "src/atlasComponents/sapi/sapi.service";
@@ -29,6 +29,7 @@ export class ParcellationSupportedInSpacePipe implements PipeTransform{
   constructor(private sapi: SAPI){}
 
   public transform(parc: SapiParcellationModel|string, tmpl: SapiSpaceModel|string): Observable<boolean> {
+    if (!parc) return NEVER
     const parcId = typeof parc === "string"
       ? parc
       : parc["@id"]
diff --git a/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts b/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts
index fb6500281577a0791f6c82b7d53ce6b7aef636a6..d339398c0a55865d2a4fce6e2feb9246e4a5606b 100644
--- a/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts
+++ b/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts
@@ -14,7 +14,7 @@ export class FilterAnnotationsBySpace implements PipeTransform{
   public transform(annotations: IAnnotationGeometry[], space: { '@id': string }, opts?: TOpts): IAnnotationGeometry[]{
     const { reverse = false } = opts || {}
     return reverse
-      ? annotations.filter(ann => ann.space["@id"] !== space["@id"])
-      : annotations.filter(ann => ann.space["@id"] === space["@id"])
+      ? annotations.filter(ann => ann.space?.["@id"] !== space?.["@id"])
+      : annotations.filter(ann => ann.space?.["@id"] === space?.["@id"])
   }
-}
\ No newline at end of file
+}
diff --git a/src/atlasViewer/atlasViewer.workerService.service.ts b/src/atlasViewer/atlasViewer.workerService.service.ts
index 9a17115534cae61a899a29c7d32bc4602599fd19..67a422714822d25e47fdb55b345116cda9db8eba 100644
--- a/src/atlasViewer/atlasViewer.workerService.service.ts
+++ b/src/atlasViewer/atlasViewer.workerService.service.ts
@@ -3,9 +3,6 @@ import { fromEvent } from "rxjs";
 import { filter, take } from "rxjs/operators";
 import { getUuid } from "src/util/fn";
 
-// worker is now exported in angular.json file
-export const worker = new Worker('worker.js')
-
 interface IWorkerMessage {
   method: string
   param: any
@@ -17,7 +14,11 @@ interface IWorkerMessage {
 })
 
 export class AtlasWorkerService {
-  public worker = worker
+  private worker: Worker
+
+  constructor(){
+    this.worker = new Worker('worker.js')
+  }
 
   async sendMessage(_data: IWorkerMessage){
 
diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts
index f4d5c96275db63b6e84e76a7223c597139fba508..129a0fe9e32d74a76fcc365fdc33a48e81604afc 100644
--- a/src/environments/environment.common.ts
+++ b/src/environments/environment.common.ts
@@ -4,7 +4,7 @@ export const environment = {
   VERSION: 'unknown version',
   PRODUCTION: true,
   BACKEND_URL: null,
-  BS_REST_URL: 'https://siibra-api-latest.apps-dev.hbp.eu/v2_0',
+  SIIBRA_API_ENDPOINTS: 'https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0',
   SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu',
   MATOMO_URL: null,
   MATOMO_ID: null,
@@ -17,4 +17,6 @@ export const environment = {
 
   // experimental feature flag
   EXPERIMENTAL_FEATURE_FLAG: false,
+
+  ENABLE_LEAP_MOTION: false,
 }
diff --git a/src/environments/parseEnv.js b/src/environments/parseEnv.js
index b05909d596c85cc1875650602cafb8a5fa2fba3a..a9dd24424e3eb5ad2ef58b2396ac793461d8dd78 100644
--- a/src/environments/parseEnv.js
+++ b/src/environments/parseEnv.js
@@ -2,36 +2,69 @@ const fs = require('fs')
 const path = require('path')
 const { promisify } = require('util')
 const asyncWrite = promisify(fs.writeFile)
+const asyncReadFile = promisify(fs.readFile)
+const process = require("process")
+const { exec } = require("child_process")
+
+
+const getGitHead = () => new Promise((rs, rj) => {
+  exec(`git rev-parse --short HEAD`, (err, stdout, stderr) => {
+    if (err) return rj(err)
+    if (stderr) return rj(stderr)
+    rs(stdout)
+  })
+})
+
+const getVersion = async () => {
+  const content = await asyncReadFile("./package.json", "utf-8")
+  const { version } = JSON.parse(content)
+  return version
+}
 
 const main = async () => {
-  const pathToEnvFile = path.join(__dirname, './environment.prod.ts')
+  const target = process.argv[2] || './environment.prod.ts'
+  const pathToEnvFile = path.join(__dirname, target)
   const {
     BACKEND_URL,
     STRICT_LOCAL,
     MATOMO_URL,
     MATOMO_ID,
-    BS_REST_URL,
-    VERSION,
-    GIT_HASH = 'unknown hash',
-    EXPERIMENTAL_FEATURE_FLAG
+    SIIBRA_API_ENDPOINTS,
+    EXPERIMENTAL_FEATURE_FLAG,
+    ENABLE_LEAP_MOTION,
   } = process.env
   
+  const version = JSON.stringify(
+    await (async () => {
+      try {
+        return await getVersion()
+      } catch (e) {
+        return "unknown version"
+      }
+    })()
+  )
+  const gitHash = JSON.stringify(
+    await (async () => {
+      try {
+        return await getGitHead()
+      } catch (e) {
+        return "unknown git hash"
+      }
+    })()
+  )
+
   console.log(`[parseEnv.js] parse envvar:`, {
     BACKEND_URL,
     STRICT_LOCAL,
     MATOMO_URL,
     MATOMO_ID,
-    BS_REST_URL,
-    VERSION,
-    GIT_HASH,
+    SIIBRA_API_ENDPOINTS,
     EXPERIMENTAL_FEATURE_FLAG,
+    ENABLE_LEAP_MOTION,
+
+    VERSION: version,
+    GIT_HASH: gitHash,
   })
-  const version = JSON.stringify(
-    VERSION || 'unknown version'
-  )
-  const gitHash = JSON.stringify(
-    GIT_HASH || 'unknown hash'
-  )
 
   const outputTxt = `
 import { environment as commonEnv } from './environment.common'
@@ -39,12 +72,13 @@ export const environment = {
   ...commonEnv,
   GIT_HASH: ${gitHash},
   VERSION: ${version},
-  BS_REST_URL: ${JSON.stringify(BS_REST_URL)},
+  SIIBRA_API_ENDPOINTS: ${JSON.stringify(SIIBRA_API_ENDPOINTS)},
   BACKEND_URL: ${JSON.stringify(BACKEND_URL)},
-  STRICT_LOCAL: ${JSON.stringify(STRICT_LOCAL)},
+  STRICT_LOCAL: ${STRICT_LOCAL},
   MATOMO_URL: ${JSON.stringify(MATOMO_URL)},
   MATOMO_ID: ${JSON.stringify(MATOMO_ID)},
-  EXPERIMENTAL_FEATURE_FLAG: ${EXPERIMENTAL_FEATURE_FLAG}
+  EXPERIMENTAL_FEATURE_FLAG: ${EXPERIMENTAL_FEATURE_FLAG},
+  ENABLE_LEAP_MOTION: ${ENABLE_LEAP_MOTION}
 }
 `
   await asyncWrite(pathToEnvFile, outputTxt, 'utf-8')
diff --git a/src/extra_styles.css b/src/extra_styles.css
index a85cfbe825e0d260d99818e059a0927de8ce922a..643ab1e507c31fc6e833ca777748a16714e83982 100644
--- a/src/extra_styles.css
+++ b/src/extra_styles.css
@@ -390,11 +390,6 @@ markdown-dom p
   height: 0px;
 }
 
-.pe-none
-{
-  pointer-events: none!important;
-}
-
 .h-2rem
 {
   height: 2rem!important;
@@ -876,3 +871,8 @@ how-to-cite img
 {
   width: 100%;
 }
+
+.mat-menu-panel.parc-smart-chip-menu-panel
+{
+  max-width: 100vw;
+}
diff --git a/src/index.html b/src/index.html
index a72f7d04781cae91eff5c233709a8059bab8e08d..763f059da834646019a8679113757031d3383804 100644
--- a/src/index.html
+++ b/src/index.html
@@ -16,6 +16,8 @@
   <script src="https://unpkg.com/three-surfer@0.0.11/dist/bundle.js" defer></script>
   <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/ng-layer-tune.esm.js"></script>
   <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" ></script>
+  <script defer src="https://unpkg.com/mathjax@3.1.2/es5/tex-svg.js"></script>
+  <script defer src="https://unpkg.com/d3@6.2.0/dist/d3.min.js"></script>
   <title>Siibra Explorer</title>
 </head>
 <body>
diff --git a/src/main.module.ts b/src/main.module.ts
index 7d26a8ee52400fa2acd8e9cbd0a670ddd06c5670..da570799012cb86e814b3760f110174c943f8f36 100644
--- a/src/main.module.ts
+++ b/src/main.module.ts
@@ -33,7 +33,6 @@ import { CookieModule } from './ui/cookieAgreement/module';
 import { KgTosModule } from './ui/kgtos/module';
 import { AtlasViewerRouterModule } from './routerModule';
 import { MessagingGlue } from './messagingGlue';
-import { BS_ENDPOINT } from './util/constants';
 import { QuickTourModule } from './ui/quickTour';
 import { of } from 'rxjs';
 import { CANCELLABLE_DIALOG, CANCELLABLE_DIALOG_OPTS } from './util/interfaces';
@@ -163,10 +162,6 @@ import { CONST } from "common/constants"
       provide: WINDOW_MESSAGING_HANDLER_TOKEN,
       useClass: MessagingGlue
     },
-    {
-      provide: BS_ENDPOINT,
-      useValue: (environment.BS_REST_URL || `https://siibra-api-stable.apps.hbp.eu/v1_0`).replace(/\/$/, '')
-    },
     {
       provide: DARKTHEME,
       useFactory: (store: Store) => store.pipe(
diff --git a/src/overwrite.scss b/src/overwrite.scss
index a6b3e9f55255d3080d58a94e7be6aedce47fc890..86f98096cead1a68e768e98ebd77aa084859fdf2 100644
--- a/src/overwrite.scss
+++ b/src/overwrite.scss
@@ -264,4 +264,28 @@ $flex-directions: row,column;
 
 .#{$nsp}-flex-static {
   flex: 0 0 auto;
-}
\ No newline at end of file
+}
+
+.#{$nsp}-blink
+{
+  animation: blink 500ms ease-in-out infinite alternate;
+}
+
+@keyframes blink {
+  0% {
+    opacity: 0.8;
+  }
+  100% {
+    opacity: 0.5;
+  }
+}
+
+a[mat-raised-button]
+{
+  text-decoration: none;
+}
+
+.#{$nsp}-pe-none
+{
+  pointer-events: none!important;
+}
diff --git a/src/plugin/const.ts b/src/plugin/const.ts
index 68104badfa030957d049a7acdfb58286fd254680..474bc36bf2cc119f845074735cc3073368db6a13 100644
--- a/src/plugin/const.ts
+++ b/src/plugin/const.ts
@@ -1,3 +1,5 @@
+import { InjectionToken } from "@angular/core"
+
 const PLUGIN_SRC_KEY = "x-plugin-portal-src"
 
 export function setPluginSrc(src: string, record: Record<string, unknown> = {}){
@@ -10,3 +12,5 @@ export function setPluginSrc(src: string, record: Record<string, unknown> = {}){
 export function getPluginSrc(record: Record<string, string> = {}){
   return record[PLUGIN_SRC_KEY]
 }
+
+export const SET_PLUGIN_NAME = new InjectionToken('SET_PLUGIN_NAME')
diff --git a/src/plugin/pluginPortal/pluginPortal.component.ts b/src/plugin/pluginPortal/pluginPortal.component.ts
index 7d5b4d4214babf9c9af7ceda85e6e2e205530851..58bd58993f3c2423ed278fee3c64b330b78b4af2 100644
--- a/src/plugin/pluginPortal/pluginPortal.component.ts
+++ b/src/plugin/pluginPortal/pluginPortal.component.ts
@@ -5,8 +5,7 @@ import { BoothVisitor, JRPCRequest, JRPCSuccessResp, ListenerChannel } from "src
 import { ApiBoothEvents, ApiService, BroadCastingApiEvents, HeartbeatEvents, namespace } from "src/api/service";
 import { getUuid } from "src/util/fn";
 import { WIDGET_PORTAL_TOKEN } from "src/widget/constants";
-import { getPluginSrc } from "../const";
-import { PluginService } from "../service";
+import { getPluginSrc, SET_PLUGIN_NAME } from "../const";
 
 @Component({
   selector: 'sxplr-plugin-portal',
@@ -44,8 +43,8 @@ export class PluginPortal implements AfterViewInit, OnDestroy, ListenerChannel{
 
   constructor(
     private apiService: ApiService,
-    private pluginSvc: PluginService,
     public vcr: ViewContainerRef,
+    @Optional() @Inject(SET_PLUGIN_NAME) private setPluginName: (inst: unknown, pluginName: string) => void,
     @Optional() @Inject(WIDGET_PORTAL_TOKEN) portalData: Record<string, string>
   ){
     if (portalData){
@@ -91,7 +90,7 @@ export class PluginPortal implements AfterViewInit, OnDestroy, ListenerChannel{
             const data = event.data as JRPCSuccessResp<HeartbeatEvents['init']['response']>
 
             this.srcName = data.result.name || 'Untitled Pluging'
-            this.pluginSvc.setPluginName(this, this.srcName)
+            this.setPluginName(this, this.srcName)
             
             while (this.handshakeSub.length > 0) this.handshakeSub.pop().unsubscribe()
 
diff --git a/src/plugin/request.md b/src/plugin/request.md
index 487c1344f3d785978a99eb61f1ec94ca6b31904e..47daf4525b3873b10f93a87d75464e0e8cb97f13 100644
--- a/src/plugin/request.md
+++ b/src/plugin/request.md
@@ -9,7 +9,7 @@ Be it request the user to select a region, a point, navigate to a specific locat
 ```javascript
 
 let parentWindow
-window.addEventListener('message', ev => {
+window.addEventListener('message', msg => {
   const { source, data, origin } = msg
   const { id, method, params, result, error } = data
 
diff --git a/src/plugin/service.ts b/src/plugin/service.ts
index 4f5a5e74f5a6bfae42a6fd445b68fcd3c115e1f2..e4fe49a725535ac36d54daf87b4f79a0aae3f698 100644
--- a/src/plugin/service.ts
+++ b/src/plugin/service.ts
@@ -3,9 +3,10 @@ import { Injectable, Injector, NgZone } from "@angular/core";
 import { WIDGET_PORTAL_TOKEN } from "src/widget/constants";
 import { WidgetService } from "src/widget/service";
 import { WidgetPortal } from "src/widget/widgetPortal/widgetPortal.component";
-import { setPluginSrc } from "./const";
+import { setPluginSrc, SET_PLUGIN_NAME } from "./const";
 import { PluginPortal } from "./pluginPortal/pluginPortal.component";
 import { environment } from "src/environments/environment"
+import { startWith } from "rxjs/operators";
 
 @Injectable({
   providedIn: 'root'
@@ -25,7 +26,9 @@ export class PluginService {
     'siibra-explorer': true
     name: string
     iframeUrl: string
-  }[]>(`${environment.BACKEND_URL || ''}plugins/manifests`)
+  }[]>(`${environment.BACKEND_URL || ''}plugins/manifests`).pipe(
+    startWith([])
+  )
 
   async launchPlugin(htmlSrc: string){
     if (this.loadedPlugins.includes(htmlSrc)) return
@@ -33,6 +36,9 @@ export class PluginService {
       providers: [{
         provide: WIDGET_PORTAL_TOKEN,
         useValue: setPluginSrc(htmlSrc, {})
+      }, {
+        provide: SET_PLUGIN_NAME,
+        useValue: (inst: PluginPortal, pluginName: string) => this.setPluginName(inst, pluginName)
       }],
       parent: this.injector
     })
diff --git a/src/plugin_examples/README.md b/src/plugin_examples/README.md
deleted file mode 100644
index 7f967539c0997992f1dae6e391a72e084d5a33a5..0000000000000000000000000000000000000000
--- a/src/plugin_examples/README.md
+++ /dev/null
@@ -1,118 +0,0 @@
-# Plugin README
-
-A plugin needs to contain three files. 
-- Manifest JSON
-- template HTML
-- script JS
-
-
-These files need to be served by GET requests over HTTP with appropriate CORS header. 
-
----
-
-## Manifest JSON
-
-The manifest JSON file describes the metadata associated with the plugin. 
-
-```json
-{
-  "name":"fzj.xg.helloWorld",
-  "displayName": "Hello World - my first plugin",
-  "templateURL":"http://LINK-TO-YOUR-PLUGIN-TEMPLATE/template.html",
-  "scriptURL":"http://LINK-TO-YOUR-PLUGIN-SCRIPT/script.js",
-  "initState":{
-    "key1": "value1",
-    "key2" : {
-      "nestedKey1" : "nestedValue1"
-    }
-  },
-  "initStateUrl": "http://LINK-TO-PLUGIN-STATE",
-  "persistency": false,
-
-  "description": "Human readable description of the plugin.",
-  "desc": "Same as description. If both present, description takes more priority.",
-  "homepage": "https://HOMEPAGE-URL-TO-YOUR-PLUGIN/doc.html",
-  "authors": "Author <author@example.com>, Author2 <author2@example.org>"
-}
-```
-*NB* 
-- Plugin name must be unique globally. To prevent plugin name clashing, please adhere to the convention of naming your package **AFFILIATION.AUTHORNAME.PACKAGENAME\[.VERSION\]**. 
-- the `initState` object and `initStateUrl` will be available prior to the evaluation of `script.js`, and will populate the objects `interactiveViewer.pluginControl[MANIFEST.name].initState` and `interactiveViewer.pluginControl[MANIFEST.name].initStateUrl` respectively. 
-
----
-
-## Template HTML
-
-The template HTML file describes the HTML view that will be rendered in the widget.
-
-
-```html
-<form>
-  <div class = "input-group">
-    <span class = "input-group-addon">Area 1</span>
-    <input type = "text" id = "fzj.xg.helloWorld.area1" name = "fzj.xg.helloWorld.area1" class = "form-control" placeholder="Select a region" value = "">
-  </div>
-
-  <div class = "input-group">
-    <span class = "input-group-addon">Area 2</span>
-    <input type = "text" id = "fzj.xg.helloWorld.area2" name = "fzj.xg.helloWorld.area2" class = "form-control" placeholder="Select a region" value = "">
-  </div>
-
-  <hr class = "col-md-10">
-
-  <div class = "col-md-12">
-    Select genes of interest:
-  </div>
-  <div class = "input-group">
-    <input type = "text" id = "fzj.xg.helloWorld.genes" name = "fzj.xg.helloWorld.genes" class = "form-control" placeholder = "Genes of interest ...">
-    <span class = "input-group-btn">
-      <button id = "fzj.xg.helloWorld.addgenes" name = "fzj.xg.helloWorld.addgenes" class = "btn btn-default" type = "button">Add</button>
-    </span>
-  </div>
-
-  <hr class = "col-md-10">
-
-  <button id = "fzj.xg.helloWorld.submit" name = "fzj.xg.helloWorld.submit" type = "button" class = "btn btn-default btn-block">Submit</button>
-
-  <hr class = "col-md-10">
-
-  <div class = "col-md-12" id = "fzj.xg.helloWorld.result">
-
-  </div>
-</form>
-```
-
-*NB*
-- *bootstrap 3.3.6* css is already included for templating.
-- keep in mind of the widget width restriction (400px) when crafting the template
-- whilst there are no vertical limits on the widget, contents can be rendered outside the viewport. Consider setting the *max-height* attribute.
-- your template and script will interact with each other likely via *element id*. As a result, it is highly recommended that unique id's are used. Please adhere to the convention: **AFFILIATION.AUTHOR.PACKAGENAME.ELEMENTID** 
-
----
-
-## Script JS
-
-The script will always be appended **after** the rendering of the template. 
-
-```javascript
-(()=>{
-  /* your code here */
-
-  if(interactiveViewer.pluginControl['fzj.xg.helloWorld'].initState){
-    /* init plugin with initState */
-  }
-  
-  const submitButton = document.getElemenById('fzj.xg.helloWorld.submit')
-  submitButton.addEventListener('click',(ev)=>{
-    console.log('submit button was clicked')
-  })
-})()
-```
-*NB*
-- JS is loaded and executed **before** the attachment of DOM (template). This is to allow webcomponents have a chance to be loaded. If your script needs the DOM to be attached, use a `setTimeout` callback to delay script execution.
-- ensure the script is scoped locally, instead of poisoning the global scope
-- for every observable subscription, call *unsubscribe()* in the *onShutdown* callback
-- some frameworks such as *jquery2*, *jquery3*, *react/reactdom* and *webcomponents* can be loaded via *interactiveViewer.pluinControl.loadExternalLibraries([LIBRARY_NAME_1, LIBRARY_NAME_2])*. if the libraries are loaded, remember to hook *interactiveViewer.pluginControl.unloadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])* in the *onShutdown* callback
-- when/if using webcomponents, please be aware that the `connectedCallback()` and `disconnectedCallback()` will be called everytime user toggle between *floating* and *docked* modes. 
-- when user navigate to a new template all existing widgets will be destroyed, unless the `persistency` is set to `true` in `manifest.json`.
-- for a list of APIs, see [plugin_api.md](plugin_api.md)
diff --git a/src/plugin_examples/migrationGuide.md b/src/plugin_examples/migrationGuide.md
deleted file mode 100644
index fcd5e040b9333b262dbbac60b6d3237859fe7f1c..0000000000000000000000000000000000000000
--- a/src/plugin_examples/migrationGuide.md
+++ /dev/null
@@ -1,51 +0,0 @@
-Plugin Migration Guide (v0.1.0 => v0.2.0)
-======
-Plugin APIs have changed drastically from v0.1.0 to v0.2.0. Here is a list of plugin API from v0.1.0, and how it has changed moving to v0.2.0.
-
-**n.b.** `webcomponents-lite.js` is no longer included by default. You will need to request it explicitly with `window.interactiveViewer.pluginControl.loadExternalLibraries()` and unload it once you are done.
-
----
-
-- ~~*window.nehubaUI*~~ removed
-  - ~~*metadata*~~ => **window.interactiveViewer.metadata**
-    - ~~*selectedTemplate* : nullable Object~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead
-    - ~~*availableTemplates* : Array of TemplateDescriptors (empty array if no templates are available)~~ => **window.interactiveViewer.metadata.loadedTemplates**
-    - ~~*selectedParcellation* : nullable Object~~ removed. use **window.interactiveViewer.metadata.selectedParcellationBSubject** instead
-    - ~~*selectedRegions* : Array of Object (empty array if no regions are selected)~~ removed. use **window.interactiveViewer.metadata.selectedRegionsBSubject** instead
-
-- ~~window.pluginControl['YOURPLUGINNAME'] *nb: may be undefined if yourpluginname is incorrect*~~ => **window.interactiveViewer.pluginControl[YOURPLUGINNAME]**
-  - blink(sec?:number) : Function that causes the floating widget to blink, attempt to grab user attention
-  - ~~pushMessage(message:string) : Function that pushes a message that are displayed as a popover if the widget is minimised. No effect if the widget is not miniminised.~~ removed
-  - shutdown() : Function that causes the widget to shutdown dynamically. (triggers onShutdown callback)
-  - onShutdown(callback) : Attaches a callback function, which is called when the plugin is shutdown.
-  
-- ~~*window.viewerHandle*~~ => **window.interactiveViewer.viewerHandle**
-  - ~~*loadTemplate(TemplateDescriptor)* : Function that loads a new template~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead
-  - ~~*onViewerInit(callback)* : Functional that allows a callback function to be called just before a nehuba viewer is initialised~~ removed
-  - ~~*afterViewerInit(callback)* : Function that allows a callback function to be called just after a nehuba viewer is initialised~~ removed
-  - ~~*onViewerDestroy(callback)* : Function that allows a callback function be called just before a nehuba viewer is destroyed~~ removed
-  - ~~*onParcellationLoading(callback)* : Function that allows a callback function to be called just before a parcellation is selected~~ removed
-  - ~~*afterParcellationLoading(callback)* : Function that allows a callback function to be called just after a parcellation is selected~~ removed
-  - *setNavigationLoc(loc,realSpace?)* : Function that teleports to loc : number[3]. Optional argument to determine if the loc is in realspace (default) or voxelspace.
-  - ~~*setNavigationOrientation(ori)* : Function that teleports to ori : number[4]. (Does not work currently)~~ => **setNavigationOri(ori)** (still non-functional)
-  - *moveToNavigationLoc(loc,realSpace?)* : same as *setNavigationLoc(loc,realSpace?)*, except moves to target location over 500ms.
-  - *showSegment(id)* : Function that selectes a segment in the viewer and UI. 
-  - *hideSegment(id)* : Function that deselects a segment in the viewer and UI.
-  - *showAllSegments()* : Function that selects all segments.
-  - *hideAllSegments()* : Function that deselects all segments.
-  - *loadLayer(layerObject)* : Function that loads a custom neuroglancer compatible layer into the viewer (e.g. precomputed, NIFTI, etc). Does not influence UI. 
-  - *mouseEvent* RxJs Observable. Read more at [rxjs doc](http://reactivex.io/rxjs/)
-    - *mouseEvent.filter(filterFn:({eventName : String, event: Event})=>boolean)* returns an Observable. Filters the event stream according to the filter function.
-    - *mouseEvent.map(mapFn:({eventName : String, event: Event})=>any)* returns an Observable. Map the event stream according to the map function.
-    - *mouseEvent.subscribe(callback:({eventName : String , event : Event})=>void)* returns an Subscriber instance. Call *Subscriber.unsubscribe()* when done to avoid memory leak. 
-  - *mouseOverNehuba* RxJs Observable. Read more at [rxjs doc](http://reactivex.io/rxjs)
-    - *mouseOverNehuba.filter* && *mouseOvernehuba.map* see above
-    - *mouseOverNehuba.subscribe(callback:({nehubaOutput : any, foundRegion : any})=>void)*
-
-- ~~*window.uiHandle*~~ => **window.interactiveViewer.uiHandle**
-  - ~~*onTemplateSelection(callback)* : Function that allows a callback function to be called just after user clicks to navigate to a new template, before *selectedTemplate* is updated~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead
-  - ~~*afterTemplateSelection(callback)* : Function that allows a callback function to be called after the template selection process is complete, and *selectedTemplate* is updated~~ removed
-  - ~~*onParcellationSelection(callback)* : Function that attach a callback function to user selecting a different parcellation~~ removed. use **window.interactiveViewer.metadata.selectedParcellationBSubject** instead.
-  - ~~*afterParcellationSelection(callback)* : Function that attach a callback function to be called after the parcellation selection process is complete and *selectedParcellation* is updated.~~ removed
-  - *modalControl*
-    - ~~*getModalHandler()* : Function returning a handler to change/show/hide/listen to a Modal.~~ removed
\ No newline at end of file
diff --git a/src/plugin_examples/plugin1/manifest.json b/src/plugin_examples/plugin1/manifest.json
deleted file mode 100644
index 0813f787c22dc71f22c3d14bbd20de2716d27068..0000000000000000000000000000000000000000
--- a/src/plugin_examples/plugin1/manifest.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "name":"fzj.xg.exmaple.0_0_1",
-  "displayName": "Example Plugin (v0.0.1)",
-  "templateURL": "http://HOSTNAME/test.html",
-  "scriptURL": "http://HOSTNAME/script.js",
-  "initState": {
-    "key1": "val1",
-    "key2": {
-      "key21": "val21"
-    }
-  },
-  "initStateUrl": "http://HOSTNAME/state?id=007",
-  "persistency": false,
-  "description": "description of example plugin",
-  "desc": "desc of example plugin",
-  "homepage": "http://HOSTNAME/home.html",
-  "authors": "Xiaoyun Gui <x.gui@fz-juelich.de>"
-}
\ No newline at end of file
diff --git a/src/plugin_examples/plugin_api.md b/src/plugin_examples/plugin_api.md
deleted file mode 100644
index 6954f15b52d7900e14a493376423c61697dacb82..0000000000000000000000000000000000000000
--- a/src/plugin_examples/plugin_api.md
+++ /dev/null
@@ -1,416 +0,0 @@
-# Plugin APIs
-
-## window.interactiveViewer
-
-### metadata
-
-#### selectedTemplateBSubject
-
-BehaviourSubject that emits a TemplateDescriptor object whenever a template is selected. Emits null onInit.
-
-#### selectedParcellationBSubject
-
-BehaviourSubject that emits a ParcellationDescriptor object whenever a parcellation is selected. n.b. selecting a new template automatically select the first available parcellation. Emits null onInit.
-
-#### selectedRegionsBSubject
-
-BehaviourSubject that emits an Array of RegionDescriptor objects whenever the list of selected regions changes. Emits empty array onInit.
-
-#### loadedTemplates
-
-Array of TemplateDescriptor objects. Loaded asynchronously onInit.
-
-#### layersRegionLabelIndexMap
-
-Map of layer name to Map of labelIndex (used by neuroglancer and nehuba) to the corresponding RegionDescriptor object.
-
-### viewerHandle
-
-> **nb** `viewerHandle` may be undefined at any time (user be yet to select an atlas, user could have unloaded an atlas. ...etc)
-
-#### setNavigationLoc(coordinates, realspace?:boolean)
-
-Function that teleports the navigation state to coordinates : [x:number,y:number,z:number]. Optional arg determine if the set of coordinates is in realspace (default) or voxelspace.
-
-#### moveToNavigationLoc(coordinates,realspace?:boolean)
-
-same as *setNavigationLoc(coordinates,realspace?)*, except the action is carried out over 500ms.
-
-#### setNavigationOri(ori)
-
-(NYI) Function that sets the orientation state of the viewer.
-
-#### moveToNavigationOri(ori)
-
-(NYI) same as *setNavigationOri*, except the action is carried out over 500ms.
-
-#### showSegment(labelIndex)
-
-Function that shows a specific segment. Will trigger *selectedRegionsBSubject*.
-
-#### hideSegment(labelIndex)
-
-Function that hides a specific segment. Will trigger *selectRegionsBSubject*
-
-#### showAllSegments()
-
-Function that shows all segments. Will trigger *selectRegionsBSubject*
-
-#### hideAllSegments()
-Function that hides all segments. Will trigger *selectRegionBSubject*
-
-#### getLayersSegmentColourMap()
-
-Call to get Map of layer name to Map of label index to colour map
-
-#### applyLayersColourMap
-
-Function that applies a custom colour map.
-
-#### loadLayer(layerObject)
-
-Function that loads *ManagedLayersWithSpecification* directly to neuroglancer. Returns the values of the object successfully added. **n.b.** advanced feature, will likely break other functionalities. **n.b.** if the layer name is already taken, the layer will not be added.
-  
-```javascript
-const obj = {
-  'advanced layer' : {
-    type : 'image',
-    source : 'nifti://http://example.com/data/nifti.nii',
-  },
-  'advanced layer 2' : {
-    type : 'mesh',
-    source : 'vtk://http://example.com/data/vtk.vtk'
-  }
-}
-const returnValue = window.interactiveViewer.viewerHandle.loadLayer(obj)
-/* loads two layers, an image nifti layer and a mesh vtk layer */
-
-console.log(returnValue)
-/* prints
-
-[{ 
-  type : 'image', 
-  source : 'nifti...' 
-},
-{
-  type : 'mesh',
-  source : 'vtk...'
-}] 
-*/
-```
-
-#### removeLayer(layerObject)
-
-Function that removes *ManagedLayersWithSpecification*, returns an array of the names of the layers removed. 
-
-**n.b.** advanced feature. may break other functionalities.
-
-```js
-const obj = {
-  'name' : /^PMap/
-}
-const returnValue = window.interactiveViewer.viewerHandle.removeLayer(obj)
-
-console.log(returnValue)
-/* prints
-['PMap 001','PMap 002']
-*/
-```
-
-#### add3DLandmarks(landmarks)
-
-adds landmarks to both the perspective view and slice view. 
-
-_input_
-
-| input | type | desc |
-| --- | --- | --- |
-| landmarks | array | an array of `landmarks` to be rendered by the viewer |
-
-A landmark object consist of the following keys:
-
-| key | type | desc | required |
-| --- | --- | --- | --- |
-| id | string | id of the landmark | yes |
-| name | string | name of the landmark | |
-| position | [number, number, number] | position (in mm) | yes |
-| color | [number, number, number] | rgb of the landmark | |
-
-
-```js
-const landmarks = [{
-  id : `fzj-xg-jugex-1`,
-  position : [0,0,0]
-},{
-  id : `fzj-xg-jugex-2`,
-  position : [22,27,-1],
-  color: [255, 0, 255]
-}]
-window.interactiveViewer.viewerHandle.add3DLandmarks(landmarks)
-
-/* adds landmarks in perspective view and slice view */
-```
-
-#### remove3DLandmarks(IDs)
-
-removes the landmarks by their IDs
-
-```js
-window.interactiveViewer.viewerHandle
-  .remove3DLandmarks(['fzj-xg-jugex-1', 'fzj-xg-jugex-2'])
-/* removes the landmarks added above */
-```
-
-#### setLayerVisibility(layerObject, visible)
-
-Function that sets the visibility of a layer. Returns the names of all the layers that are affected as an Array of string.
-
-```js
-const obj = {
-  'type' : 'segmentation'
-}
-
-window.interactiveViewer.viewerHandle.setLayerVisibility(obj,false)
-
-/* turns off all the segmentation layers */
-```
-
-#### mouseEvent
-
-Subject that emits an object shaped `{ eventName : string, event: event }` when a user triggers a mouse event on the viewer. 
-
-
-#### mouseOverNehuba
-
-**deprecating** use mouseOverNehubaUI instead
-
-BehaviourSubject that emits an object shaped `{ nehubaOutput : number | null, foundRegion : RegionDescriptor | null }`
-
-#### mouseOverNehubaUI
-
-`Observable<{ landmark, segments, customLandmark }>`.
-
-**nb** it is a known issue that if customLandmarks are destroyed/created while user mouse over the custom landmark this observable will emit `{ customLandmark: null }`
-
-### uiHandle
-
-#### getModalHandler()
-
-returns a modalHandler object, which has the following methods/properties:
-
-##### hide()
-
-Dynamically hides the modal
-
-##### show()
-
-Shows the modal
-
-##### title
-
-title of the modal (String)
-
-##### body
-
-body of the modal shown (String)
-
-##### footer
-
-footer of the modal (String)
-
-##### dismissable
-
-whether the modal is dismissable on click backdrop/esc key (Boolean)
-
-*n.b. if true, users will not be able to interact with the viewer unless you specifically call `handler.hide()`*
-
-#### launchNewWidget(manifest)
-
-returns a Promise. expects a JSON object, with the same key value as a plugin manifest. the *name* key must be unique, or the promise will be rejected. 
-
-#### getUserInput(config)
-
-returns a Promise, resolves when user confirms, rejects when user cancels. expects config object object with the following structure:
-
-```javascript
-const config = {
-  "title": "Title of the modal", // default: "Message"
-  "message":"Message to be seen by the user.", // default: ""
-  "placeholder": "Start typing here", // default: "Type your response here"
-  "defaultValue": "42" // default: ""
-  "iconClass":"fas fa-save" // default fas fa-save, set to falsy value to disable
-}
-```
-
-#### getUserConfirmation(config)
-
-returns a Promise, resolves when user confirms, rejects when user cancels. expects config object object with the following structure:
-
-```javascript
-const config = {
-  "title": "Title of the modal", // default: "Message"
-  "message":"Message to be seen by the user." // default: ""
-}
-```
-
-#### getUserToSelectARegion(message)
-
-**To be deprecated**
-
-_input_
-
-| input | type | desc |
-| --- | --- | --- |
-| message | `string` | human readable message displayed to the user | 
-| spec.type | `'POINT'` `'PARCELLATION_REGION'` **default** | type of region to be returned. |
-
-_returns_
-
-`Promise`, resolves to return array of region clicked, rejects with error object `{ userInitiated: boolean }`
-
-Requests user to select a region of interest. Resolving to the region selected by the user. Rejects if either user cancels by pressing `Esc` or `Cancel`, or by developer calling `cancelPromise`
-
-#### getUserToSelectRoi(message, spec)
-
-_input_
-
-| input | type | desc |
-| --- | --- | --- |
-| message | `string` | human readable message displayed to the user | 
-| spec.type | `POINT` `PARCELLATION_REGION` | type of ROI to be returned. |
-
-_returns_
-
-`Promise`
-
-**resolves**: return `{ type, payload }`. `type` is the same as `spec.type`, and `payload` differs depend on the type requested:
-
-| type | payload | example |
-| --- | --- | --- |
-| `POINT` | array of number in mm | `[12.2, 10.1, -0.3]` |
-| `PARCELLATION_REGOIN` | non empty array of region selected | `[{ "layer": { "name" : " viewer specific layer name " }, "segment": {} }]` |
-
-**rejects**: with error object `{ userInitiated: boolean }`
-
-Requests user to select a region of interest. If the `spec.type` input is missing, it is assumed to be `'PARCELLATION_REGION'`. Resolving to the region selected by the user. Rejects if either user cancels by pressing `Esc` or `Cancel`, or by developer calling `cancelPromise`
-
-#### cancelPromise(promise)
-
-returns `void`
-  
-_input_ 
-
-| input | type | desc |
-| --- | --- | --- |
-| promise | `Promise` | Reference to the __exact__ promise returned by `uiHnandle` methods |
-
-Cancel the request to select a parcellation region.
-
-_usage example_
-
-```javascript
-
-(() => {
-  const pr = interactive.uiHandle.getUserToSelectARegion(`webJuGEx would like you to select a region`)
-
-  pr.then(region => {  })
-    .catch(console.warn)
-
-  /*
-    * do NOT do 
-    * 
-    * const pr = interactive.uiHandle.getUserToSelectARegion(`webJuGEx would like you to select a region`)
-    *   .then(region => {  })
-    *   -catch(console.warn)
-    * 
-    * the promise passed to `cancelPromise` must be the exact promise returned.
-    * by chaining then/catch, a new reference is returned
-    */
-
-  setTimeout(() => {
-    try {
-      interactive.uiHandle.cancelPromise(pr)
-    } catch (e) {
-      // if the promise has been fulfilled (by resolving or user cancel), cancelPromise will throw
-    }
-  }, 5000)
-})()
-```
-
-### pluginControl
-
-#### loadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])
-
-Function that loads external libraries. Pass the name of the libraries as an Array of string, and returns a Promise. When promise resolves, the libraries are loaded.
-
-**n.b.** while unlikely, there is a possibility that multiple requests to load external libraries in quick succession can cause the promise to resolve before the library is actually loaded. 
-
-```js
-const currentlySupportedLibraries = ['jquery@2','jquery@3','webcomponentsLite@1.1.0','react@16','reactdom@16','vue@2.5.16']
-
-window.interactivewViewer.loadExternalLibraries(currentlySupportedLibraries)
-  .then(() => {
-    /* loaded */
-  })
-  .catch(e=>console.warn(e))
-
-```
-
-####  unloadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])
-
-unloading the libraries (should be called on shutdown).
-
-#### *[PLUGINNAME]*
-
-returns a plugin handler. This would be how to interface with the plugins.
-
-##### blink()
-
-Function that causes the floating widget to blink, attempt to grab user attention (silently fails if called on startup).
-
-##### setProgressIndicator(val:number|null)
-
-Set the progress of the plugin. Useful for giving user feedbacks on the progress of a long running process. Call the function with null to unset the progress.
-
-##### shutdown()
-
-Function that causes the widget to shutdown dynamically. (triggers onShutdown callback, silently fails if called on startup)
-
-##### onShutdown(callback)
-
-Attaches a callback function, which is called when the plugin is shutdown.
-
-##### initState
-
-passed from `manifest.json`. Useful for setting initial state of the plugin. Can be any JSON valid value (array, object, string).
-
-##### initStateUrl
-
-passed from `manifest.json`. Useful for setting initial state of the plugin.  Can be any JSON valid value (array, object, string).
-
-##### setInitManifestUrl(url|null)
-
-set/unset the url for a manifest json that will be fetched on atlas viewer startup. the argument should be a valid URL, has necessary CORS header, and returns a valid manifest json file. null will unset the search param. Useful for passing/preserving state. If called multiple times, the last one will take effect.
-
-```js
-const pluginHandler = window.interactiveViewer.pluginControl[PLUGINNAME]
-
-const subscription = window.interactiveViewer.metadata.selectedTemplateBSubject.subscribe(template=>console.log(template))
-
-fetch(`http://YOUR_BACKEND.com/API_ENDPOINT`)
-  .then(data=>pluginHandler.blink(20))
-
-pluginHandler.onShutdown(()=>{
-  subscription.unsubscribe()
-})
-```
-
-------
-
-## window.nehubaViewer
-
-nehuba object, exposed if developer would like to use it
-
-## window.viewer
-
-neuroglancer object, exposed if developer would like to use it
\ No newline at end of file
diff --git a/src/routerModule/cipher.ts b/src/routerModule/cipher.ts
index 9ad21bc8c036336c1bc71f4a2dc0122603a3a681..e98d9b3f39fb8f2193c64da6094fbe0db0fae868 100644
--- a/src/routerModule/cipher.ts
+++ b/src/routerModule/cipher.ts
@@ -10,84 +10,22 @@
  * So performance is not really that important (Also, need to learn bitwise operation)
  */
 
-const cipher = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-'
-export const separator = "."
-const negString = '~'
 
-const encodeInt = (number: number) => {
-  if (number % 1 !== 0) { throw new Error('cannot encodeInt on a float. Ensure float flag is set') }
-  if (isNaN(Number(number)) || number === null || number === Number.POSITIVE_INFINITY) { throw new Error('The input is not valid') }
-
-  let residual: number
-  let result = ''
-
-  if (number < 0) {
-    result += negString
-    residual = Math.floor(number * -1)
-  } else {
-    residual = Math.floor(number)
-  }
-
-  /* eslint-disable-next-line no-constant-condition */
-  while (true) {
-    result = cipher.charAt(residual % 64) + result
-    residual = Math.floor(residual / 64)
-
-    if (residual === 0) {
-      break
-    }
-  }
-  return result
-}
-
-interface IB64EncodingOption {
-  float: boolean
+import { sxplrNumB64Enc } from "common/util"
+const {
+  separator,
+  cipher,
+  encodeNumber,
+  decodeToNumber,
+} = sxplrNumB64Enc
+
+export {
+  separator,
+  cipher,
+  encodeNumber,
+  decodeToNumber,
 }
 
-const defaultB64EncodingOption = {
-  float: false,
-}
-
-export const encodeNumber:
-  (number: number, option?: IB64EncodingOption) => string =
-  (number: number, { float = false }: IB64EncodingOption = defaultB64EncodingOption) => {
-    if (!float) { return encodeInt(number) } else {
-      const floatArray = new Float32Array(1)
-      floatArray[0] = number
-      const intArray = new Uint32Array(floatArray.buffer)
-      const castedInt = intArray[0]
-      return encodeInt(castedInt)
-    }
-  }
-
-const decodetoInt = (encodedString: string) => {
-  let _encodedString
-  let negFlag = false
-  if (encodedString.slice(-1) === negString) {
-    negFlag = true
-    _encodedString = encodedString.slice(0, -1)
-  } else {
-    _encodedString = encodedString
-  }
-  return (negFlag ? -1 : 1) * [..._encodedString].reduce((acc, curr) => {
-    const index = cipher.indexOf(curr)
-    if (index < 0) { throw new Error(`Poisoned b64 encoding ${encodedString}`) }
-    return acc * 64 + index
-  }, 0)
-}
-
-export const decodeToNumber:
-  (encodedString: string, option?: IB64EncodingOption) => number =
-  (encodedString: string, {float = false} = defaultB64EncodingOption) => {
-    if (!float) { return decodetoInt(encodedString) } else {
-      const _int = decodetoInt(encodedString)
-      const intArray = new Uint32Array(1)
-      intArray[0] = _int
-      const castedFloat = new Float32Array(intArray.buffer)
-      return castedFloat[0]
-    }
-  }
-
 /**
  * see https://stackoverflow.com/questions/53051415/can-you-disable-auxiliary-secondary-routes-in-angular
  * need to encode brackets
diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts
index 10f003638a6f8bed8c281e40777134572fe42c5c..dc7da45d8a9eed4d3b8c4edbe31c409864d09741 100644
--- a/src/routerModule/routeStateTransform.service.ts
+++ b/src/routerModule/routeStateTransform.service.ts
@@ -146,8 +146,11 @@ export class RouteStateTransformSvc {
     const pluginStates = fullPath.queryParams['pl']
     if (pluginStates) {
       try {
-        const arrPluginStates = JSON.parse(pluginStates)
-        returnState["[state.plugins]"].initManifests = arrPluginStates.map(url => [plugins.INIT_MANIFEST_SRC, url] as [string, string])
+        const arrPluginStates: string[] = JSON.parse(pluginStates)
+        if (arrPluginStates.length > 1) throw new Error(`can only initialise one plugin at a time`)
+        returnState["[state.plugins]"].initManifests = {
+          [plugins.INIT_MANIFEST_SRC]: arrPluginStates
+        }
       } catch (e) {
         /**
          * parsing plugin error
diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts
index 3500cc3dcba1b6d4aa412d38026fd28cdf31ca3a..3359055f7b70b395772c5d27842dffee46b1b374 100644
--- a/src/routerModule/router.service.ts
+++ b/src/routerModule/router.service.ts
@@ -10,9 +10,8 @@ import { scan } from 'rxjs/operators'
 import { RouteStateTransformSvc } from "./routeStateTransform.service";
 import { SAPI } from "src/atlasComponents/sapi";
 import { generalActions } from "src/state";
-/**
- * http://localhost:8080/#/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2/p:minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-290/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIy..0.14gY0~.14gY0..1LSm
- */
+
+
 @Injectable({
   providedIn: 'root'
 })
diff --git a/src/share/saneUrl/saneUrl.component.spec.ts b/src/share/saneUrl/saneUrl.component.spec.ts
index 5ab173950c92148144b7b3025f63cddf95205a30..982ecb76fb47ca954a5da6c9f5c792c69f3118ba 100644
--- a/src/share/saneUrl/saneUrl.component.spec.ts
+++ b/src/share/saneUrl/saneUrl.component.spec.ts
@@ -1,14 +1,13 @@
-import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing'
-import { ShareModule } from '../share.module'
+import { TestBed, fakeAsync, tick, flush, ComponentFixture } from '@angular/core/testing'
 import { SaneUrl } from './saneUrl.component'
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'
 import { By } from '@angular/platform-browser'
 import { BACKENDURL } from 'src/util/constants'
 import { NoopAnimationsModule } from '@angular/platform-browser/animations'
 import { SaneUrlSvc } from './saneUrl.service'
 import { AngularMaterialModule } from 'src/sharedModules'
 import { CUSTOM_ELEMENTS_SCHEMA, Directive } from '@angular/core'
-import { of } from 'rxjs'
+import { of, throwError } from 'rxjs'
+import { NotFoundError } from '../type'
 
 const inputCss = `input[aria-label="Custom link"]`
 const submitCss = `button[aria-label="Create custom link"]`
@@ -25,15 +24,22 @@ class AuthStateDummy {
 
 describe('> saneUrl.component.ts', () => {
   describe('> SaneUrl', () => {
+    const mockSaneUrlSvc = {
+      saneUrlroot: 'saneUrlroot',
+      getKeyVal: jasmine.createSpy('getKeyVal'),
+      setKeyVal: jasmine.createSpy('setKeyVal'),
+    }
     beforeEach(async () => {
       await TestBed.configureTestingModule({
         imports: [
-          HttpClientTestingModule,
           NoopAnimationsModule,
           AngularMaterialModule,
         ],
         providers: [
-          SaneUrlSvc,
+          {
+            provide: SaneUrlSvc,
+            useValue: mockSaneUrlSvc
+          }
         ],
         declarations: [
           SaneUrl,
@@ -43,11 +49,18 @@ describe('> saneUrl.component.ts', () => {
           CUSTOM_ELEMENTS_SCHEMA
         ]
       }).compileComponents()
+
+      mockSaneUrlSvc.getKeyVal.and.returnValue(
+        of('foo-bar')
+      )
+      mockSaneUrlSvc.setKeyVal.and.returnValue(
+        of('OK')
+      )
     })
 
     afterEach(() => {
-      const ctrl = TestBed.inject(HttpTestingController)
-      ctrl.verify()
+      mockSaneUrlSvc.getKeyVal.calls.reset()
+      mockSaneUrlSvc.setKeyVal.calls.reset()
     })
 
     it('> can be created', () => {
@@ -112,215 +125,188 @@ describe('> saneUrl.component.ts', () => {
       const input = fixture.debugElement.query( By.css( inputCss ) )
     })
 
-    it('> on entering string in input, makes debounced GET request', fakeAsync(() => {
-
-      const value = 'test_1'
-
-      const httpTestingController = TestBed.inject(HttpTestingController)
-
-      // Necessary to detectChanges, or formControl will not initialise properly
-      // See https://stackoverflow.com/a/56600762/6059235
-      const fixture = TestBed.createComponent(SaneUrl)
-      fixture.detectChanges()
-
-      // Set value
-      fixture.componentInstance.customUrl.setValue(value)
-
-      tick(500)
-
-      const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`)
-      req.flush(200)
-    }))
-
-    it('> on 200 response, show error', fakeAsync(() => {
-      
-      const value = 'test_1'
-
-      const httpTestingController = TestBed.inject(HttpTestingController)
-
-      // Necessary to detectChanges, or formControl will not initialise properly
-      // See https://stackoverflow.com/a/56600762/6059235
-      const fixture = TestBed.createComponent(SaneUrl)
-      fixture.detectChanges()
-
-      // Set value
-      fixture.componentInstance.customUrl.setValue(value)
-
-      tick(500)
-
-      const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`)
-      req.flush('OK')
-
-      // Expect validator to fail catch it
-      expect(fixture.componentInstance.customUrl.invalid).toEqual(true)
-
-      // on change detection, UI should catch it
-      fixture.detectChanges()
-
-      const input = fixture.debugElement.query( By.css( inputCss ) )
-
-      const submit = fixture.debugElement.query( By.css( submitCss ) )
-      const disabled = !!submit.attributes['disabled']
-      expect(disabled.toString()).toEqual('true')
-    }))
-
-    it('> on 404 response, show available', fakeAsync(() => {
-
-      const value = 'test_1'
-
-      const httpTestingController = TestBed.inject(HttpTestingController)
-
-      // Necessary to detectChanges, or formControl will not initialise properly
-      // See https://stackoverflow.com/a/56600762/6059235
-      const fixture = TestBed.createComponent(SaneUrl)
-      fixture.detectChanges()
-
-      // Set value
-      fixture.componentInstance.customUrl.setValue(value)
-
-      tick(500)
-
-      const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`)
-      req.flush('some reason', { status: 404, statusText: 'Not Found.' })
-
-      // Expect validator to fail catch it
-      expect(fixture.componentInstance.customUrl.invalid).toEqual(false)
-
-      // on change detection, UI should catch it
-      fixture.detectChanges()
-
-      const input = fixture.debugElement.query( By.css( inputCss ) )
-
-      const submit = fixture.debugElement.query( By.css( submitCss ) )
-      const disabled = !!submit.attributes['disabled']
-      expect(disabled.toString()).toEqual('false')
-    }))
-
-    it('> on other error codes, show invalid', fakeAsync(() => {
-
-      const value = 'test_1'
-
-      const httpTestingController = TestBed.inject(HttpTestingController)
-
-      // Necessary to detectChanges, or formControl will not initialise properly
-      // See https://stackoverflow.com/a/56600762/6059235
-      const fixture = TestBed.createComponent(SaneUrl)
-      fixture.detectChanges()
-
-      // Set value
-      fixture.componentInstance.customUrl.setValue(value)
-
-      tick(500)
-
-      const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`)
-      req.flush('some reason', { status: 401, statusText: 'Unauthorised.' })
-
-      // Expect validator to fail catch it
-      expect(fixture.componentInstance.customUrl.invalid).toEqual(true)
-
-      // on change detection, UI should catch it
-      fixture.detectChanges()
-
-      const input = fixture.debugElement.query( By.css( inputCss ) )
-
-      const submit = fixture.debugElement.query( By.css( submitCss ) )
-      const disabled = !!submit.attributes['disabled']
-      expect(disabled.toString()).toEqual('true')
-    }))
-
-    it('> on click create link btn calls correct API', fakeAsync(() => {
-
-      const value = 'test_1'
-
-      const httpTestingController = TestBed.inject(HttpTestingController)
-
-      // Necessary to detectChanges, or formControl will not initialise properly
-      // See https://stackoverflow.com/a/56600762/6059235
-      const fixture = TestBed.createComponent(SaneUrl)
-      fixture.detectChanges()
-
-      // Set value
-      fixture.componentInstance.customUrl.setValue(value)
-
-      tick(500)
-
-      const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`)
-      req.flush('some reason', { status: 404, statusText: 'Not Found.' })
-
-      fixture.detectChanges()
-      flush()
-
-      const submit = fixture.debugElement.query( By.css( submitCss ) )
-      const disabled = !!submit.attributes['disabled']
-      expect(disabled.toString()).toEqual('false')
-
-      submit.triggerEventHandler('click', {})
-
-      fixture.detectChanges()
-
-      const disabledInProgress = !!submit.attributes['disabled']
-      expect(disabledInProgress.toString()).toEqual('true')
-
-      const req2 = httpTestingController.expectOne({
-        method: 'POST',
-        url: `${BACKENDURL}saneUrl/${value}`
+    describe("> on valid input", () => {
+      let saneUrlCmp: SaneUrl
+      let fixture: ComponentFixture<SaneUrl>
+      const stateTobeSaved = 'foo-bar'
+      beforeEach(() => {
+        // Necessary to detectChanges, or formControl will not initialise properly
+        // See https://stackoverflow.com/a/56600762/6059235
+        fixture = TestBed.createComponent(SaneUrl)
+        saneUrlCmp = fixture.componentInstance
+        saneUrlCmp.stateTobeSaved = stateTobeSaved
+        fixture.detectChanges()
       })
-      
-      req2.flush({})
-
-      fixture.detectChanges()
-
-      const disabledAfterComplete = !!submit.attributes['disabled']
-      expect(disabledAfterComplete.toString()).toEqual('true')
-
-      const cpyBtn = fixture.debugElement.query( By.css( copyBtnCss ) )
-      expect(cpyBtn).toBeTruthy()
-    }))
-
-    it('> on click create link btn fails show result', fakeAsync(() => {
-
-      const value = 'test_1'
-
-      const httpTestingController = TestBed.inject(HttpTestingController)
-
-      // Necessary to detectChanges, or formControl will not initialise properly
-      // See https://stackoverflow.com/a/56600762/6059235
-      const fixture = TestBed.createComponent(SaneUrl)
-      fixture.detectChanges()
-
-      // Set value
-      fixture.componentInstance.customUrl.setValue(value)
-
-      tick(500)
-
-      const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`)
-      req.flush('some reason', { status: 404, statusText: 'Not Found.' })
-
-      fixture.detectChanges()
-      flush()
-
-      const submit = fixture.debugElement.query( By.css( submitCss ) )
-      const disabled = !!submit.attributes['disabled']
-      expect(disabled.toString()).toEqual('false')
-
-      submit.triggerEventHandler('click', {})
-
-      fixture.detectChanges()
-
-      const disabledInProgress = !!submit.attributes['disabled']
-      expect(disabledInProgress.toString()).toEqual('true')
-
-      const req2 = httpTestingController.expectOne({
-        method: 'POST',
-        url: `${BACKENDURL}saneUrl/${value}`
+      it('> on entering string in input, makes debounced GET request', fakeAsync(() => {
+
+        const value = 'test_1'
+  
+        // Set value
+        fixture.componentInstance.customUrl.setValue(value)
+  
+        tick(500)
+  
+        expect(mockSaneUrlSvc.getKeyVal).toHaveBeenCalledOnceWith(value)
+      }))
+  
+      describe("> on 200", () => {
+        it("> show error", fakeAsync(() => {
+  
+          const value = 'test_1'
+    
+          // Set value
+          fixture.componentInstance.customUrl.setValue(value)
+    
+          tick(500)
+    
+          // Expect validator to fail catch it
+          expect(fixture.componentInstance.customUrl.invalid).toEqual(true)
+    
+          // on change detection, UI should catch it
+          fixture.detectChanges()
+    
+          const input = fixture.debugElement.query( By.css( inputCss ) )
+    
+          const submit = fixture.debugElement.query( By.css( submitCss ) )
+          const disabled = !!submit.attributes['disabled']
+          expect(disabled.toString()).toEqual('true')
+        }))
+      })
+  
+      describe('> on 404', () => {
+        beforeEach(() => {
+          mockSaneUrlSvc.getKeyVal.and.returnValue(
+            throwError(new NotFoundError('not found'))
+          )
+        })
+        it("> should available", fakeAsync(() => {
+  
+          const value = 'test_1'
+    
+          // Set value
+          fixture.componentInstance.customUrl.setValue(value)
+    
+          tick(500)
+    
+          // Expect validator to fail catch it
+          expect(fixture.componentInstance.customUrl.invalid).toEqual(false)
+    
+          // on change detection, UI should catch it
+          fixture.detectChanges()
+    
+          const input = fixture.debugElement.query( By.css( inputCss ) )
+    
+          const submit = fixture.debugElement.query( By.css( submitCss ) )
+          const disabled = !!submit.attributes['disabled']
+          expect(disabled.toString()).toEqual('false')
+        }))
       })
+  
+      describe("> on other error", () => {
+        beforeEach(() => {
+  
+          mockSaneUrlSvc.getKeyVal.and.returnValue(
+            throwError(new Error('other errors'))
+          )
+        })
+        it("> show invalid", fakeAsync(() => {
+          const value = 'test_1'
+    
+          // Set value
+          fixture.componentInstance.customUrl.setValue(value)
+    
+          tick(500)
+    
+          // Expect validator to fail catch it
+          expect(fixture.componentInstance.customUrl.invalid).toEqual(true)
+    
+          // on change detection, UI should catch it
+          fixture.detectChanges()
+    
+          const input = fixture.debugElement.query( By.css( inputCss ) )
+    
+          const submit = fixture.debugElement.query( By.css( submitCss ) )
+          const disabled = !!submit.attributes['disabled']
+          expect(disabled.toString()).toEqual('true')
+        }))
+      })
+  
+      describe("> on click create link", () => {
+        beforeEach(() => {
+          mockSaneUrlSvc.getKeyVal.and.returnValue(
+            throwError(new NotFoundError('not found'))
+          )
+        })
+        it("> calls correct service function", fakeAsync(() => {
+  
+          const value = 'test_1'
+    
+          // Set value
+          fixture.componentInstance.customUrl.setValue(value)
+    
+          tick(500)
+    
+          fixture.detectChanges()
+          flush()
+    
+          const submit = fixture.debugElement.query( By.css( submitCss ) )
+          const disabled = !!submit.attributes['disabled']
+          expect(disabled.toString()).toEqual('false')
+    
+          submit.triggerEventHandler('click', {})
+    
+          fixture.detectChanges()
+    
+          const disabledInProgress = !!submit.attributes['disabled']
+          expect(disabledInProgress.toString()).toEqual('true')
+    
+          fixture.detectChanges()
+    
+          const disabledAfterComplete = !!submit.attributes['disabled']
+          expect(disabledAfterComplete.toString()).toEqual('true')
+    
+          const cpyBtn = fixture.debugElement.query( By.css( copyBtnCss ) )
+          expect(cpyBtn).toBeTruthy()
+        }))
+  
+        describe("> on fail", () => {
+          beforeEach(() => {
+            mockSaneUrlSvc.setKeyVal.and.returnValue(
+              throwError(new Error(`some error`))
+            )
+          })
+          it("> show result", fakeAsync(() => {
+  
+            const value = 'test_1'
       
-      req2.flush('Something went wrong', { statusText: 'Wrong status text', status: 500 })
-
-      fixture.detectChanges()
-
-      const input = fixture.debugElement.query( By.css( inputCss ) )
-
-    }))
+            // Set value
+            fixture.componentInstance.customUrl.setValue(value)
+      
+            tick(500)
+      
+            fixture.detectChanges()
+      
+            const submit = fixture.debugElement.query( By.css( submitCss ) )
+            const disabled = !!submit.attributes['disabled']
+            expect(disabled.toString()).toEqual('false')
+      
+            submit.triggerEventHandler('click', {})
+      
+            fixture.detectChanges()
+      
+            const disabledInProgress = !!submit.attributes['disabled']
+            expect(disabledInProgress.toString()).toEqual('true')
+      
+            expect(mockSaneUrlSvc.setKeyVal).toHaveBeenCalledOnceWith(value, stateTobeSaved)
+            
+            fixture.detectChanges()
+      
+            const input = fixture.debugElement.query( By.css( inputCss ) )
+      
+          }))
+        })
+      })
+  
+    })
   })
 })
diff --git a/src/share/saneUrl/saneUrl.service.ts b/src/share/saneUrl/saneUrl.service.ts
index 8f3af1f3d55fcb9baec57d44ddbbda76ac24c2df..c5d9849f760ed90b7197ef8206b53db55ab1025d 100644
--- a/src/share/saneUrl/saneUrl.service.ts
+++ b/src/share/saneUrl/saneUrl.service.ts
@@ -4,23 +4,27 @@ import { throwError } from "rxjs";
 import { catchError, mapTo } from "rxjs/operators";
 import { BACKENDURL } from 'src/util/constants'
 import { IKeyValStore, NotFoundError } from '../type'
+import { DISABLE_PRIORITY_HEADER } from "src/util/priority"
 
 @Injectable({
   providedIn: 'root'
 })
 
 export class SaneUrlSvc implements IKeyValStore{
-  public saneUrlRoot = `${BACKENDURL}saneUrl/`
+  public saneUrlRoot = `${BACKENDURL}go/`
   constructor(
     private http: HttpClient
   ){
-
+    if (!BACKENDURL) {
+      const loc = window.location
+      this.saneUrlRoot = `${loc.protocol}//${loc.hostname}${!!loc.port ? (':' + loc.port) : ''}${loc.pathname}go/`
+    }
   }
 
   getKeyVal(key: string) {
     return this.http.get<Record<string, any>>(
       `${this.saneUrlRoot}${key}`,
-      { responseType: 'json' }
+      { responseType: 'json', headers: { [DISABLE_PRIORITY_HEADER]: '1' } }
     ).pipe(
       catchError((err, obs) => {
         const { status } = err
@@ -35,7 +39,8 @@ export class SaneUrlSvc implements IKeyValStore{
   setKeyVal(key: string, value: any) {
     return this.http.post(
       `${this.saneUrlRoot}${key}`,
-      value
+      value,
+      { headers: { [DISABLE_PRIORITY_HEADER]: '1' } }
     ).pipe(
       mapTo(`${this.saneUrlRoot}${key}`)
     )
diff --git a/src/share/share.module.ts b/src/share/share.module.ts
index f3137eaadc83a3aaead101efe1b76cc600301409..ff7011249173857ff344bd58730f7e7881f9e675 100644
--- a/src/share/share.module.ts
+++ b/src/share/share.module.ts
@@ -8,6 +8,7 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms";
 import { AuthModule } from "src/auth";
 import { ShareSheetComponent } from "./shareSheet/shareSheet.component";
 import { ShareDirective } from "./share.directive";
+import { StateModule } from "src/state";
 
 @NgModule({
   imports: [
@@ -17,6 +18,7 @@ import { ShareDirective } from "./share.directive";
     FormsModule,
     ReactiveFormsModule,
     AuthModule,
+    StateModule,
   ],
   declarations: [
     ClipboardCopy,
diff --git a/src/share/shareSheet/shareSheet.component.ts b/src/share/shareSheet/shareSheet.component.ts
index 8d4b9f963ebafc0b83850010bf8b18e70f7d6a15..664ffd89c8fe93bf1a7886d80bc83145e7c8b03a 100644
--- a/src/share/shareSheet/shareSheet.component.ts
+++ b/src/share/shareSheet/shareSheet.component.ts
@@ -1,7 +1,6 @@
-import { Component } from "@angular/core";
+import { Component, TemplateRef } from "@angular/core";
 import { MatDialog } from "@angular/material/dialog";
 import { ARIA_LABELS } from 'common/constants'
-import { SaneUrl } from "../saneUrl/saneUrl.component"
 
 @Component({
   selector: 'sxplr-share-sheet',
@@ -18,7 +17,7 @@ export class ShareSheetComponent{
 
   }
 
-  openShareSaneUrl(){
-    this.dialog.open(SaneUrl, { ariaLabel: ARIA_LABELS.SHARE_CUSTOM_URL })
+  openDialog(templateRef: TemplateRef<unknown>){
+    this.dialog.open(templateRef, { ariaLabel: ARIA_LABELS.SHARE_CUSTOM_URL })
   }
 }
diff --git a/src/share/shareSheet/shareSheet.template.html b/src/share/shareSheet/shareSheet.template.html
index cea5242302cf57ff1cc718ac1b2d98b7a9d79f0c..bc953043ba622d6af44df97a27215346bd03521b 100644
--- a/src/share/shareSheet/shareSheet.template.html
+++ b/src/share/shareSheet/shareSheet.template.html
@@ -17,8 +17,10 @@
   <mat-list-item
     [attr.aria-label]="ARIA_LABELS.SHARE_CUSTOM_URL"
     [attr.tab-index]="10"
-    (click)="openShareSaneUrl()"
+    (click)="openDialog(saneUrlTmpl)"
     [matTooltip]="ARIA_LABELS.SHARE_CUSTOM_URL"
+    iav-state-aggregator
+    #stateAggregator="iavStateAggregator"
     >
     <mat-icon
       class="mr-4"
@@ -32,3 +34,9 @@
 
   </mat-list-item>
 </mat-nav-list>
+
+
+<ng-template #saneUrlTmpl>
+  <iav-sane-url [stateTobeSaved]="stateAggregator.jsonifiedState$ | async">
+  </iav-sane-url>
+</ng-template>
\ No newline at end of file
diff --git a/src/state/atlasSelection/effects.spec.ts b/src/state/atlasSelection/effects.spec.ts
index 61110e26b2d8b746e3c65c04dc89b07ddabd5b73..556e002c56f518ba5957629a178c5249ad37d234 100644
--- a/src/state/atlasSelection/effects.spec.ts
+++ b/src/state/atlasSelection/effects.spec.ts
@@ -1,16 +1,15 @@
-import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"
 import { TestBed } from "@angular/core/testing"
 import { provideMockActions } from "@ngrx/effects/testing"
 import { Action } from "@ngrx/store"
 import { MockStore, provideMockStore } from "@ngrx/store/testing"
 import { hot } from "jasmine-marbles"
 import { Observable, of, throwError } from "rxjs"
-import { SAPI, SAPIModule, SapiRegionModel, SAPIParcellation, SapiAtlasModel, SapiSpaceModel, SapiParcellationModel } from "src/atlasComponents/sapi"
+import { SAPI, SAPIModule, SapiRegionModel, SapiAtlasModel, SapiSpaceModel, SapiParcellationModel } from "src/atlasComponents/sapi"
 import { IDS } from "src/atlasComponents/sapi/constants"
 import { actions, selectors } from "."
 import { Effect } from "./effects"
 import * as mainActions from "../actions"
-import { take } from "rxjs/operators"
+import { atlasSelection } from ".."
 
 describe("> effects.ts", () => {
   describe("> Effect", () => {
@@ -40,7 +39,6 @@ describe("> effects.ts", () => {
 
         const sapisvc = TestBed.inject(SAPI)
         const regions = await sapisvc.getParcRegions(IDS.ATLAES.HUMAN, IDS.PARCELLATION.JBA29, IDS.TEMPLATES.MNI152).toPromise()
-  
         hoc1left = regions.find(r => /hoc1/i.test(r.name) && /left/i.test(r.name))
         if (!hoc1left) throw new Error(`cannot find hoc1 left`)
         hoc1leftCentroid = JSON.parse(JSON.stringify(hoc1left)) 
@@ -94,6 +92,98 @@ describe("> effects.ts", () => {
       })
 
     })
+
+    describe("> onTemplateParcSelectionPostHook", () => {
+      describe("> 0", () => {
+      })
+      describe("> 1", () => {
+        const currNavigation = {
+          orientation: [0, 0, 0, 1],
+          perspectiveOrientation: [0, 0, 0, 1],
+          perspectiveZoom: 1,
+          position: [1, 2, 3], 
+          zoom: 1
+        }
+        beforeEach(() => {
+          const store = TestBed.inject(MockStore)
+          store.overrideSelector(atlasSelection.selectors.navigation, currNavigation)
+        })
+        describe("> when atlas is different", () => {
+          describe("> if no atlas prior", () => {
+
+            it("> navigation should be reset", () => {
+              const effects = TestBed.inject(Effect)
+              const hook = effects.onTemplateParcSelectionPostHook[1]
+              const obs = hook({
+                current: {
+                  atlas: null,
+                  parcellation: null,
+                  template: null
+                },
+                previous: {
+                  atlas: {
+                    "@id": IDS.ATLAES.RAT
+                  } as any,
+                  parcellation: {
+                    "@id": IDS.PARCELLATION.WAXHOLMV4
+                  } as any,
+                  template: {
+                    "@id": IDS.TEMPLATES.WAXHOLM
+                  } as any,
+                }
+              })
+
+              expect(obs).toBeObservable(
+                hot('(a|)', {
+                  a: {
+                    navigation: null
+                  }
+                })
+              )
+            })
+          })
+          describe("> if different atlas prior", () => {
+
+            it("> navigation should be reset", () => {
+              const effects = TestBed.inject(Effect)
+              const hook = effects.onTemplateParcSelectionPostHook[1]
+              const obs = hook({
+                current: {
+                  atlas: {
+                    "@id": IDS.ATLAES.HUMAN
+                  } as any,
+                  parcellation: {
+                    "@id": IDS.PARCELLATION.JBA29
+                  } as any,
+                  template: {
+                    "@id": IDS.TEMPLATES.MNI152
+                  } as any,
+                },
+                previous: {
+                  atlas: {
+                    "@id": IDS.ATLAES.RAT
+                  } as any,
+                  parcellation: {
+                    "@id": IDS.PARCELLATION.WAXHOLMV4
+                  } as any,
+                  template: {
+                    "@id": IDS.TEMPLATES.WAXHOLM
+                  } as any,
+                }
+              })
+
+              expect(obs).toBeObservable(
+                hot('(a|)', {
+                  a: {
+                    navigation: null
+                  }
+                })
+              )
+            })
+          })
+        })
+      })
+    })
   
     describe('> if selected atlas has no matching tmpl space', () => {
 
diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts
index 5461f7dd87b365d9e87028b87a10c69b9ad14342..2c95b75602df2ec483983d910f61ceccdcc7525d 100644
--- a/src/state/atlasSelection/effects.ts
+++ b/src/state/atlasSelection/effects.ts
@@ -52,6 +52,16 @@ export class Effect {
     ({ current, previous }) => {
       const prevSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(previous?.template?.["@id"])
       const currSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(current?.template?.["@id"])
+
+      /**
+       * if trans-species, return default state for navigation
+       */
+      if (previous?.atlas?.["@id"] !== current?.atlas?.["@id"]) {
+        return of({
+          navigation: null
+        })
+      }
+
       /**
        * if either space name is undefined, return default state for navigation
        */
@@ -435,6 +445,5 @@ export class Effect {
     private store: Store,
     private interSpaceCoordXformSvc: InterSpaceCoordXformSvc,
   ){
-
   }
 }
\ No newline at end of file
diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts
index 08848c1efa9b81e649bfc8d7c29d3cc6581f77ed..ebea8a78dd3015d9b0b050c3902e40fb8ab2a0c8 100644
--- a/src/state/atlasSelection/store.ts
+++ b/src/state/atlasSelection/store.ts
@@ -117,9 +117,14 @@ const reducer = createReducer(
   on(
     actions.selectAtlas,
     (state, { atlas }) => {
+      if (atlas?.["@id"] === state?.selectedAtlas?.["@id"]) {
+        return state
+      }
       return {
         ...state,
-        selectedAtlas: atlas
+        selectedAtlas: atlas,
+        selectedTemplate: null,
+        selectedParcellation: null,
       }
     }
   ),
diff --git a/src/state/plugins/actions.ts b/src/state/plugins/actions.ts
index 5fe4fcd15b162f4bf26f5947d124e925dca43552..759c7523082503c38eaed07b9ff92f34b1b3e775 100644
--- a/src/state/plugins/actions.ts
+++ b/src/state/plugins/actions.ts
@@ -7,12 +7,3 @@ export const clearInitManifests = createAction(
     nameSpace: string
   }>()
 )
-
-export const setInitMan = createAction(
-  `${nameSpace} setInitMan`,
-  props<{
-    nameSpace: string
-    url: string
-    internal?: boolean
-  }>()
-)
diff --git a/src/state/plugins/effects.ts b/src/state/plugins/effects.ts
index c27a74669d3dc0b96dd3e0506a41a14c5455220a..5f147b9b17f29011441b3c92fb7c92a992cca1a2 100644
--- a/src/state/plugins/effects.ts
+++ b/src/state/plugins/effects.ts
@@ -6,9 +6,8 @@ import * as constants from "./const"
 import * as selectors from "./selectors"
 import * as actions from "./actions"
 import { DialogService } from "src/services/dialogService.service";
-import { of } from "rxjs";
-import { HttpClient } from "@angular/common/http";
-import { getHttpHeader } from "src/util/constants"
+import { NEVER, of } from "rxjs";
+import { PluginService } from "src/plugin/service";
 
 @Injectable()
 export class Effects{
@@ -16,27 +15,33 @@ export class Effects{
   initMan = this.store.pipe(
     select(selectors.initManfests),
     map(initMan => initMan[constants.INIT_MANIFEST_SRC]),
-    filter(val => !!val),
+    filter(val => val && val.length > 0),
   )
 
+  private pendingList = new Set<string>()
+  private launchedList = new Set<string>()
+  private banList = new Set<string>()
+
   initManLaunch = createEffect(() => this.initMan.pipe(
-    switchMap(val => 
-      this.dialogSvc
-        .getUserConfirm({
-          message: `This URL is trying to open a plugin from ${val}. Proceed?`
-        })
-        .then(() => 
-          this.http.get(val, {
-            headers: getHttpHeader(),
-            responseType: 'json'
-          }).toPromise()
-        )
-        .then(json => {
-          /**
-           * TODO fix init plugin launch
-           * at that time, also restore effects.spec.ts test
-           */
-        })
+    switchMap(val => of(...val)),
+    switchMap(
+      url => {
+        if (this.pendingList.has(url)) return NEVER
+        if (this.launchedList.has(url)) return NEVER
+        if (this.banList.has(url)) return NEVER
+        this.pendingList.add(url)
+        return this.dialogSvc
+          .getUserConfirm({
+            message: `This URL is trying to open a plugin from ${url}. Proceed?`
+          })
+          .then(() => {
+            this.launchedList.add(url)
+            return this.svc.launchPlugin(url)
+          })
+          .finally(() => {
+            this.pendingList.delete(url)
+          })
+      }
     ),
     catchError(() => of(null))
   ), { dispatch: false })
@@ -52,8 +57,8 @@ export class Effects{
   constructor(
     private store: Store,
     private dialogSvc: DialogService,
-    private http: HttpClient,
+    private svc: PluginService,
   ){
     
   }
-}
\ No newline at end of file
+}
diff --git a/src/state/plugins/store.ts b/src/state/plugins/store.ts
index 83bb211ec912966efade6b361faced1dd43ede0c..7283a77a4f74431800af70c0360b0c8dfd2204f1 100644
--- a/src/state/plugins/store.ts
+++ b/src/state/plugins/store.ts
@@ -1,9 +1,8 @@
 import { createReducer, on } from "@ngrx/store";
 import * as actions from "./actions"
-import { INIT_MANIFEST_SRC } from "./const"
 
 export type PluginStore = {
-  initManifests: Record<string, string>
+  initManifests: Record<string, string[]>
 }
 
 export const defaultState: PluginStore = {
@@ -15,8 +14,8 @@ export const reducer = createReducer(
   on(
     actions.clearInitManifests,
     (state, { nameSpace }) => {
-      if (!state[nameSpace]) return state
-      const newMan: Record<string, string> = {}
+      if (!state.initManifests[nameSpace]) return state
+      const newMan: Record<string, string[]> = {}
       const { initManifests } = state
       for (const key in initManifests) {
         if (key === nameSpace) continue
@@ -28,20 +27,4 @@ export const reducer = createReducer(
       }
     }
   ),
-  on(
-    actions.setInitMan,
-    (state, { nameSpace, url, internal }) => {
-      if (!internal) {
-        if (nameSpace === INIT_MANIFEST_SRC) return state
-      }
-      const { initManifests } = state
-      return {
-        ...state,
-        initManifests: {
-          ...initManifests,
-          [nameSpace]: url
-        }
-      }
-    }
-  )
 )
diff --git a/src/strictLocal/index.ts b/src/strictLocal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df38eaa22fe65187cca51189b4442684509278ed
--- /dev/null
+++ b/src/strictLocal/index.ts
@@ -0,0 +1,2 @@
+export { StrictLocalModule } from "./module"
+export { HideWhenLocal } from "./strictLocal.directive"
\ No newline at end of file
diff --git a/src/strictLocal/module.ts b/src/strictLocal/module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..62db4e8d888eb11107503bcb5b1ce8145b8a3d61
--- /dev/null
+++ b/src/strictLocal/module.ts
@@ -0,0 +1,23 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { MatButtonModule } from "@angular/material/button";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { HideWhenLocal } from "./strictLocal.directive";
+import { StrictLocalInfo } from "./strictLocalCmp/strictLocalCmp.component";
+
+@NgModule({
+  declarations: [
+    HideWhenLocal,
+    StrictLocalInfo,
+  ],
+  imports: [
+    CommonModule,
+    MatTooltipModule,
+    MatButtonModule,
+  ],
+  exports: [
+    HideWhenLocal,
+  ]
+})
+
+export class StrictLocalModule{}
diff --git a/src/strictLocal/strictLocal.directive.ts b/src/strictLocal/strictLocal.directive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b5f0c83ad82310ec40b64906c3f6d5c266fb7a2f
--- /dev/null
+++ b/src/strictLocal/strictLocal.directive.ts
@@ -0,0 +1,22 @@
+import { ComponentFactoryResolver, Directive, HostBinding, ViewContainerRef } from "@angular/core";
+import { environment } from "src/environments/environment"
+import { StrictLocalInfo } from "./strictLocalCmp/strictLocalCmp.component";
+
+@Directive({
+  selector: '[sxplr-hide-when-local]',
+  exportAs: 'hideWhenLocal'
+})
+
+export class HideWhenLocal {
+  @HostBinding('style.display')
+  hideWhenLocal = environment.STRICT_LOCAL ? 'none!important' : null
+  constructor(
+    private vc: ViewContainerRef,
+    private cfr: ComponentFactoryResolver,
+  ){
+    if (environment.STRICT_LOCAL) {
+      const cf = this.cfr.resolveComponentFactory(StrictLocalInfo)
+      this.vc.createComponent(cf)
+    }
+  }
+}
diff --git a/src/strictLocal/strictLocalCmp/strictLocalCmp.component.ts b/src/strictLocal/strictLocalCmp/strictLocalCmp.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e75b84c2b782a501996b66ca5a2be98687accda7
--- /dev/null
+++ b/src/strictLocal/strictLocalCmp/strictLocalCmp.component.ts
@@ -0,0 +1,13 @@
+import { Component } from "@angular/core";
+
+@Component({
+  selector: `strict-local-info`,
+  template: `
+  <button mat-icon-button [matTooltip]="tooltip" tabindex="-1">
+    <i class="fas fa-unlink"></i>
+  </button>`,
+})
+
+export class StrictLocalInfo{
+  tooltip = "External links are hidden in strict local mode."
+}
diff --git a/src/ui/dialogInfo/dialog.directive.ts b/src/ui/dialogInfo/dialog.directive.ts
index 1b30c4e59ede96787318fe0f8582a2ee85317072..63d886a6ef5fa172ea1a8cf17abb5587005a5751 100644
--- a/src/ui/dialogInfo/dialog.directive.ts
+++ b/src/ui/dialogInfo/dialog.directive.ts
@@ -52,7 +52,7 @@ export class DialogDirective{
     }
     this.matDialog.open(this.templateRef, {
       data: this.data,
-      ...sizeDict[this.size]
+      ...(sizeDict[this.size] || {})
     })
   }
 }
\ No newline at end of file
diff --git a/src/ui/help/about/about.template.html b/src/ui/help/about/about.template.html
index 9a9169afd9eb25c1897e14b7dd950a5b55e4d212..380dbf4d2d83fbb0693857a00ac2929e13598905 100644
--- a/src/ui/help/about/about.template.html
+++ b/src/ui/help/about/about.template.html
@@ -1,7 +1,7 @@
 <div class="container-fluid">
   <div class="row mt-4 mb-4">
 
-    <a [href]="userDoc" target="_blank">
+    <a sxplr-hide-when-local [href]="userDoc" target="_blank">
       <button mat-raised-button color="primary">
         <i class="fas fa-book-open"></i>
         <span>
@@ -10,7 +10,7 @@
       </button>
     </a>
 
-    <a [href]="repoUrl" target="_blank">
+    <a sxplr-hide-when-local [href]="repoUrl" target="_blank">
       <button mat-flat-button>
         <i class="fab fa-github"></i>
         <span>
diff --git a/src/ui/help/module.ts b/src/ui/help/module.ts
index b99786401f97aad174c2e5db0398223d377be5c7..5dded09484db45d2ff96e81ac3502f11ae69f737 100644
--- a/src/ui/help/module.ts
+++ b/src/ui/help/module.ts
@@ -7,6 +7,7 @@ import { AboutCmp } from './about/about.component'
 import { HelpOnePager } from "./helpOnePager/helpOnePager.component";
 import {QuickTourModule} from "src/ui/quickTour/module";
 import { HowToCite } from "./howToCite/howToCite.component";
+import { StrictLocalModule } from "src/strictLocal";
 
 @NgModule({
   imports: [
@@ -14,7 +15,8 @@ import { HowToCite } from "./howToCite/howToCite.component";
     AngularMaterialModule,
     ComponentsModule,
     UtilModule,
-    QuickTourModule
+    QuickTourModule,
+    StrictLocalModule,
   ],
   declarations: [
     AboutCmp,
diff --git a/src/ui/quickTour/quickTour.service.ts b/src/ui/quickTour/quickTour.service.ts
index 3853a19497de7f3494d77c587b1a1e044cf00f5c..3aed96edf8d1cde0ff32642954be57f735a634a5 100644
--- a/src/ui/quickTour/quickTour.service.ts
+++ b/src/ui/quickTour/quickTour.service.ts
@@ -90,7 +90,7 @@ export class QuickTourService {
         height: '0px',
         width: '0px',
         hasBackdrop: true,
-        backdropClass: ['pe-none', 'cdk-overlay-dark-backdrop'],
+        backdropClass: ['sxplr-pe-none', 'cdk-overlay-dark-backdrop'],
         positionStrategy: this.overlay.position().global(),
       })
     }
diff --git a/src/util/constants.ts b/src/util/constants.ts
index 5c4e7185743a1dca35e0523f076385b797fc21b5..7879151a46014b4576f159e55d118cce75aa748c 100644
--- a/src/util/constants.ts
+++ b/src/util/constants.ts
@@ -32,9 +32,12 @@ export const MIN_REQ_EXPLAINER = `
 
 export const APPEND_SCRIPT_TOKEN: InjectionToken<(url: string) => Promise<HTMLScriptElement>> = new InjectionToken(`APPEND_SCRIPT_TOKEN`)
 
-export const appendScriptFactory = (document: Document) => {
+export const appendScriptFactory = (document: Document, defer: boolean = false) => {
   return src => new Promise((rs, rj) => {
     const scriptEl = document.createElement('script')
+    if (defer) {
+      scriptEl.defer = true
+    }
     scriptEl.src = src
     scriptEl.onload = () => rs(scriptEl)
     scriptEl.onerror = (e) => rj(e)
@@ -115,7 +118,6 @@ export const compareLandmarksChanged: (prevLandmarks: any[], newLandmarks: any[]
 }
 
 export const CYCLE_PANEL_MESSAGE = `[spacebar] to cycle through views`
-export const BS_ENDPOINT = new InjectionToken<string>('BS_ENDPOINT')
 
 export const UNSUPPORTED_PREVIEW = [{
   text: 'Preview of Colin 27 and JuBrain Cytoarchitectonic',
diff --git a/src/util/priority.ts b/src/util/priority.ts
index ab703f76b14e1a299fa2ae017d81038a43c1c858..79ef1b043e4d5bb13a99e157995315e05e94b8d5 100644
--- a/src/util/priority.ts
+++ b/src/util/priority.ts
@@ -24,6 +24,8 @@ type Queue = {
   next: HttpHandler
 }
 
+export const DISABLE_PRIORITY_HEADER = 'x-sxplr-disable-priority'
+
 @Injectable({
   providedIn: 'root'
 })
@@ -137,8 +139,11 @@ export class PriorityHttpInterceptor implements HttpInterceptor{
      * Since the way in which serialization occurs is via path and query param...
      * body is not used.
      */
-    if (this.disablePriority || req.method !== 'GET') {
-      return next.handle(req)
+    if (this.disablePriority || req.method !== 'GET' || !!req.headers.get(DISABLE_PRIORITY_HEADER)) {
+      const newReq = req.clone({
+        headers: req.headers.delete(DISABLE_PRIORITY_HEADER)
+      })
+      return next.handle(newReq)
     }
 
     const { urlWithParams } = req
diff --git a/src/viewerModule/leap/index.ts b/src/viewerModule/leap/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/viewerModule/leap/leapSignal/leapSignal.component.ts b/src/viewerModule/leap/leapSignal/leapSignal.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..046998972b022e782e14199028fe144160174363
--- /dev/null
+++ b/src/viewerModule/leap/leapSignal/leapSignal.component.ts
@@ -0,0 +1,40 @@
+import { Component } from "@angular/core";
+import { map } from "rxjs/operators";
+import { LeapService } from "../service";
+import { HandShape } from "../service"
+
+@Component({
+  selector: 'leap-control-signal',
+  templateUrl: './leapSignal.template.html',
+  styleUrls: [
+    './leapSignal.style.css'
+  ]
+})
+export class LeapSignal{
+  public ready$ = this.svc.leapReady$
+  public handDetected$ = this.svc.hand$.pipe(
+    map(hands => hands && hands.length > 0)
+  )
+  public gestureText$ = this.svc.gesture$.pipe(
+    map(gesture => {
+      if (gesture === HandShape.PINCHING) {
+        return "Pinching"
+      }
+      if (gesture === HandShape.PALM_FORWARD) {
+        return "Zooming"
+      }
+      if (gesture === HandShape.POINTING_ONE_FINGER) {
+        return "Translating"
+      }
+      if (gesture === HandShape.POINTING_TWO_FINGERS) {
+        return "Oblique Cutting"
+      }
+      return null
+    })
+  )
+  constructor(
+    private svc: LeapService
+  ){
+
+  }
+}
diff --git a/src/viewerModule/leap/leapSignal/leapSignal.style.css b/src/viewerModule/leap/leapSignal/leapSignal.style.css
new file mode 100644
index 0000000000000000000000000000000000000000..1ca31c86031d27db3c0013d09af57a1e74cacf9c
--- /dev/null
+++ b/src/viewerModule/leap/leapSignal/leapSignal.style.css
@@ -0,0 +1,19 @@
+.leap-spinner
+{
+  animation: spinning 1400ms linear infinite running;
+}
+
+@keyframes spinning
+{
+  from {
+    transform: rotate(0deg);
+  }
+  to{
+    transform: rotate(359deg);
+  }
+}
+
+.leap-muted
+{
+  opacity: 0.5;
+}
diff --git a/src/viewerModule/leap/leapSignal/leapSignal.template.html b/src/viewerModule/leap/leapSignal/leapSignal.template.html
new file mode 100644
index 0000000000000000000000000000000000000000..3a0c1dfb42086f2a47c227572129d61db50187f2
--- /dev/null
+++ b/src/viewerModule/leap/leapSignal/leapSignal.template.html
@@ -0,0 +1,30 @@
+<ng-template [ngIf]="ready$ | async">
+  <ng-template [ngTemplateOutlet]="leapCtrlTmpl"></ng-template>
+</ng-template>
+
+
+<ng-template #leapCtrlTmpl>
+
+  <mat-card>
+    <span *ngIf="gestureText$ | async as text">
+      {{ text }}
+    </span>
+    <!-- detecting hand -->
+    <ng-template [ngTemplateOutlet]="(handDetected$ | async) ? handFound : noHandFound">
+    </ng-template>
+  </mat-card>
+  
+  <ng-template #handFound>
+    <span class="fa-stack fa-2x">
+      <i class="fas fa-hand-paper fa-stack-1x"></i>
+    </span>
+  </ng-template>
+  
+  <ng-template #noHandFound>
+    <span class="fa-stack fa-2x">
+      <i class="leap-muted fas fa-hand-paper fa-stack-1x"></i>
+      <i class="leap-muted leap-spinner fas fa-circle-notch fa-stack-2x"></i>
+    </span>
+  </ng-template>
+  
+</ng-template>
diff --git a/src/viewerModule/leap/module.ts b/src/viewerModule/leap/module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4e7bcbfc54c984cf6bdfa87aa1aeab57ed40f8b2
--- /dev/null
+++ b/src/viewerModule/leap/module.ts
@@ -0,0 +1,36 @@
+import { CommonModule } from "@angular/common";
+import { APP_INITIALIZER, NgModule } from "@angular/core";
+import { MatCardModule } from "@angular/material/card";
+import { APPEND_SCRIPT_TOKEN } from "src/util/constants";
+import { LeapSignal } from "./leapSignal/leapSignal.component";
+import { LeapService } from "./service";
+import { LeapControlViewRef } from "./signal.directive";
+
+@NgModule({
+  imports: [
+    CommonModule,
+    MatCardModule,
+  ],
+  declarations: [
+    LeapSignal,
+    LeapControlViewRef,
+  ],
+  exports: [
+    LeapSignal,
+    LeapControlViewRef,
+  ],
+  providers: [
+    LeapService,
+    {
+      provide: APP_INITIALIZER,
+      useFactory(appendSrc: (src: string, deferFlag?: boolean) => Promise<void>){
+        return () => appendSrc("leap-0.6.4.js", true)
+      },
+      deps: [
+        APPEND_SCRIPT_TOKEN
+      ],
+      multi: true
+    },
+  ],
+})
+export class LeapModule{}
\ No newline at end of file
diff --git a/src/viewerModule/leap/service.ts b/src/viewerModule/leap/service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8ce47c14dccf14a36431e26153d4db723ccbf365
--- /dev/null
+++ b/src/viewerModule/leap/service.ts
@@ -0,0 +1,252 @@
+import { DOCUMENT } from "@angular/common";
+import { Inject, Injectable, Optional } from "@angular/core";
+import { BehaviorSubject, EMPTY, interval, Observable, Subject } from "rxjs";
+import { distinctUntilChanged, filter, map, switchMap, take, takeUntil } from "rxjs/operators";
+import { NehubaViewerUnit } from "../nehuba";
+import { NEHUBA_INSTANCE_INJTKN } from "../nehuba/util";
+
+const PINCH_THRESHOLD = 0.80
+const PALM_Z_THRESHOLD = 0.55
+const ROTATION_SPEED = 0.0001
+const ZOOM_SPEED = 40
+
+type Finger = {
+  extended: boolean
+}
+
+type Hand = {
+  pinchStrength: number
+  palmNormal: [number, number, number]
+  palmVelocity: [number, number, number]
+
+  thumb: Finger
+  indexFinger: Finger
+  middleFinger: Finger
+  ringFinger: Finger
+  pinky: Finger
+}
+
+export enum HandShape {
+  PINCHING="PINCHING",
+  POINTING_ONE_FINGER="POINTING_ONE_FINGER",
+  POINTING_TWO_FINGERS="POINTING_TWO_FINGERS",
+  PALM_FORWARD="PALM_FORWARD",
+}
+
+function getHandShape(hand: Hand): HandShape {
+  if (!hand) return null
+  if (hand.pinchStrength >= PINCH_THRESHOLD) return HandShape.PINCHING
+  if (
+    hand.thumb.extended &&
+    hand.indexFinger.extended &&
+    !hand.middleFinger.extended &&
+    !hand.ringFinger.extended &&
+    !hand.pinky.extended
+  ) return HandShape.POINTING_ONE_FINGER
+  if (
+    hand.thumb.extended &&
+    hand.indexFinger.extended &&
+    hand.middleFinger.extended &&
+    !hand.ringFinger.extended &&
+    !hand.pinky.extended
+  ) return HandShape.POINTING_TWO_FINGERS
+
+  const palmNormalZ = hand.palmNormal[2]
+  if (
+    hand.thumb.extended &&
+    hand.indexFinger.extended &&
+    hand.middleFinger.extended &&
+    hand.ringFinger.extended &&
+    hand.pinky.extended &&
+    palmNormalZ <= -PALM_Z_THRESHOLD
+  ) return HandShape.PALM_FORWARD
+  return null
+}
+
+@Injectable()
+export class LeapService{
+  static initFlag = false
+
+  private destroy$ = new Subject()
+
+  public leapReady$ = new BehaviorSubject(false)
+
+  public hand$ = new Subject<Hand[]>()
+  public gesture$ = new Subject<HandShape>()
+  public palmVelocity$ = new Subject<[number, number, number]>()
+
+  private sliceLock = false
+  private vec3: any
+  private quat: any
+
+  private rotation: any
+  private WORLD_UP: any
+  private WORLD_RIGHT: any
+
+  constructor(
+    @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaInst$: Observable<NehubaViewerUnit>,
+    @Inject(DOCUMENT) document: Document,
+  ){
+    if (LeapService.initFlag) {
+      console.error(`LeapService already initialised.`)
+    }
+    if (!(window as any).Leap) {
+      console.error(`Leap not found. Terminating`)
+      return
+    }
+    const lop = (window as any).Leap.loop({
+      frame: frame => {
+        this.leapReady$.next(true)
+        this.hand$.next(frame?.hands)
+        if (frame?.hands) {
+          this.emitHand(frame.hands[0])
+        }
+      }
+    })
+
+    document.addEventListener("visibilitychange", () => {
+      if (document.visibilityState === "hidden") {
+        lop.connection.disconnect()
+      } else {
+        lop.connection.reconnect()
+      }
+    })
+
+    interval(160).pipe(
+      filter(() => !!(window as any).export_nehuba),
+      take(1)
+    ).subscribe(() => {
+      this.vec3 = (window as any).export_nehuba.vec3
+      this.quat = (window as any).export_nehuba.quat
+      
+      this.rotation = this.quat.create()
+      this.WORLD_UP = this.vec3.fromValues(0, 1, 0)
+      this.WORLD_RIGHT = this.vec3.fromValues(1, 0, 0)
+    })
+
+    this.nehubaInst$.pipe(
+      switchMap(nehuba => {
+        if (!nehuba) return EMPTY
+        return this.gesture$.pipe(
+          distinctUntilChanged(),
+          switchMap(gesture => {
+            if (!gesture) return EMPTY
+            return this.palmVelocity$.pipe(
+              map(velocity => {
+                return {
+                  nehuba,
+                  gesture,
+                  velocity
+                }
+              })
+            )
+          })
+        )
+      }),
+      takeUntil(this.destroy$)
+    ).subscribe(({ gesture, nehuba, velocity }) => {
+      if (gesture === HandShape.PINCHING) {
+        const vel = velocity
+        const temp = vel[1]
+        vel[1] = -vel[2]
+        vel[2] = temp
+        this.leapToRotation(
+          vel,
+          ROTATION_SPEED * 4,
+          nehuba.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation
+        )
+        return
+      }
+
+      if (gesture === HandShape.PALM_FORWARD) {
+        /**
+         * zooming
+         */
+        const vel = -velocity[2]
+        if (nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value >= 10000) {
+          nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value += ZOOM_SPEED * vel;
+        } else {
+          nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value = 10001;
+        }
+        return
+      }
+
+      if (gesture === HandShape.POINTING_ONE_FINGER) {
+        /**
+         * translating
+         */
+        const SPEED_SCALE = 5 * 8000 * (nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value / 400000)
+        const vel = velocity
+        vel[1] = -vel[1]
+        vel[2] = -vel[2]
+        const cur = nehuba.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation.orientation
+        this.vec3.transformQuat(vel, vel, cur)
+        const { position } = nehuba.nehubaViewer.ngviewer.navigationState.pose
+        this.vec3.scaleAndAdd(
+          position.spatialCoordinates,
+          position.spatialCoordinates,
+          vel,
+          SPEED_SCALE
+        )
+        position.changed.dispatch();
+      }
+
+      if (gesture === HandShape.POINTING_TWO_FINGERS) {
+        const ROTATION_SPEED = 0.0004;
+        if (!this.sliceLock) {
+          const vel = velocity
+          const temp = vel[1]
+          vel[0] = -vel[0]
+          vel[1] = vel[2]
+          vel[2] = -temp
+          this.leapToRotation(
+            vel,
+            ROTATION_SPEED,
+            nehuba.nehubaViewer.ngviewer.navigationState.pose.orientation,
+            nehuba.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation.orientation
+          )
+        }
+      }
+    })
+
+    LeapService.initFlag = true
+  }
+
+  private leapToRotation(vel: [number, number, number], rotSpeed: number, orientationStream: any, camOrientation = null){
+    let cur = orientationStream.orientation;
+    const axis = this.vec3.create()
+    if (camOrientation) {
+      this.vec3.transformQuat(axis, this.WORLD_UP, camOrientation);
+      this.vec3.transformQuat(axis, axis, cur);
+    } else {
+      this.vec3.transformQuat(axis, this.WORLD_UP, cur);
+    }
+    this.quat.setAxisAngle(this.rotation, axis, vel[0] * rotSpeed);
+    this.quat.multiply(orientationStream.orientation, this.rotation, cur);
+    cur = orientationStream.orientation;
+    if (camOrientation) {
+      this.vec3.transformQuat(axis, this.WORLD_RIGHT, camOrientation);
+      this.vec3.transformQuat(axis, axis, cur);
+    } else {
+      this.vec3.transformQuat(axis, this.WORLD_RIGHT, cur);
+    }
+    this.quat.setAxisAngle(this.rotation, axis, vel[2] * rotSpeed);
+    this.quat.multiply(orientationStream.orientation, this.rotation, cur);
+    orientationStream.changed.dispatch();
+  }
+
+  private emitHand(hand: Hand){
+    let handShape: HandShape
+    try{
+      handShape = getHandShape(hand)
+      this.gesture$.next(
+        handShape
+      )
+      this.palmVelocity$.next(
+        hand?.palmVelocity
+      )
+    } catch (e) {
+      console.error(e)
+    }
+  }
+}
diff --git a/src/viewerModule/leap/signal.directive.ts b/src/viewerModule/leap/signal.directive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4add98f470090b81aeabd7a9ef90ec54cd562dde
--- /dev/null
+++ b/src/viewerModule/leap/signal.directive.ts
@@ -0,0 +1,15 @@
+import { ComponentFactoryResolver, Directive, ViewContainerRef } from "@angular/core";
+import { LeapSignal } from "./leapSignal/leapSignal.component";
+
+@Directive({
+  selector: '[leap-control-view-ref]'
+})
+export class LeapControlViewRef {
+  constructor(
+    private vcr: ViewContainerRef,
+    private cfr: ComponentFactoryResolver,
+  ){
+    const cf = this.cfr.resolveComponentFactory(LeapSignal)
+    this.vcr.createComponent(cf)
+  }
+}
diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts
index 14779f2a3f322e0162e88c35a615adf78482aec7..e7107258f1c109ddb6f49266dc710c326ef4cbf9 100644
--- a/src/viewerModule/module.ts
+++ b/src/viewerModule/module.ts
@@ -26,6 +26,9 @@ import { MouseoverModule } from "src/mouseoverModule";
 import { LogoContainer } from "src/ui/logoContainer/logoContainer.component";
 import { FloatingMouseContextualContainerDirective } from "src/util/directives/floatingMouseContextualContainer.directive";
 import { ShareModule } from "src/share";
+import { LeapModule } from "./leap/module";
+
+import { environment } from "src/environments/environment"
 
 @NgModule({
   imports: [
@@ -47,6 +50,7 @@ import { ShareModule } from "src/share";
     DialogModule,
     MouseoverModule,
     ShareModule,
+    ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : [])
   ],
   declarations: [
     ViewerCmp,
diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts
index 7233d0f26bb2bda42bfe3b0141c9c9b64a377c77..8f2b0de2dc144f287815a503dd95e35c741e82fb 100644
--- a/src/viewerModule/nehuba/config.service/util.ts
+++ b/src/viewerModule/nehuba/config.service/util.ts
@@ -409,6 +409,11 @@ export function getNehubaConfig(space: SapiSpaceModel): NehubaConfig {
         "layers": {},
         "navigation": {
           "zoomFactor": 350000 * scale,
+          pose: {
+            position: {
+              voxelSize: [1, 1, 1]
+            }
+          }
         },
         "perspectiveOrientation": [
           0.3140767216682434,
diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts
index 6537a7aef4ea762d6d38c85ac335011551b5767e..ca5ae4be810af998f93318a6348f751555fabff3 100644
--- a/src/viewerModule/nehuba/constants.ts
+++ b/src/viewerModule/nehuba/constants.ts
@@ -64,3 +64,5 @@ export interface IMeshesToLoad {
 }
 
 export const SET_MESHES_TO_LOAD = new InjectionToken<Observable<IMeshesToLoad>>('SET_MESHES_TO_LOAD')
+
+export const PMAP_LAYER_NAME = 'regional-pmap'
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
index 6f6d708876773bd84a484d73226fab01066f731a..6886634b6e6c02a6556dcaa8a18c3f4729b7b71a 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
@@ -11,7 +11,7 @@ import { EnumColorMapName } from "src/util/colorMaps";
 import { getShader } from "src/util/constants";
 import { getNgLayersFromVolumesATP, getRegionLabelIndex } from "../config.service";
 import { ParcVolumeSpec } from "../store/util";
-import { NehubaLayerControlService } from "./layerCtrl.service";
+import { PMAP_LAYER_NAME } from "../constants";
 
 @Injectable()
 export class LayerCtrlEffects {
@@ -22,7 +22,7 @@ export class LayerCtrlEffects {
     ),
     mapTo(
       atlasAppearance.actions.removeCustomLayer({
-        id: NehubaLayerControlService.PMAP_LAYER_NAME
+        id: PMAP_LAYER_NAME
       })
     )
   ))
@@ -37,17 +37,20 @@ export class LayerCtrlEffects {
     ),
     switchMap(([ regions, { atlas, parcellation, template } ]) => {
       const sapiRegion = this.sapi.getRegion(atlas["@id"], parcellation["@id"], regions[0].name)
-      return sapiRegion.getMapInfo(template["@id"]).pipe(
-        map(val => 
+      return forkJoin([
+        sapiRegion.getMapInfo(template["@id"]),
+        sapiRegion.getMapUrl(template["@id"])
+      ]).pipe(
+        map(([mapInfo, mapUrl]) => 
           atlasAppearance.actions.addCustomLayer({
             customLayer: {
               clType: "customlayer/nglayer",
-              id: NehubaLayerControlService.PMAP_LAYER_NAME,
-              source: `nifti://${sapiRegion.getMapUrl(template["@id"])}`,
+              id: PMAP_LAYER_NAME,
+              source: `nifti://${mapUrl}`,
               shader: getShader({
                 colormap: EnumColorMapName.VIRIDIS,
-                highThreshold: val.max,
-                lowThreshold: val.min,
+                highThreshold: mapInfo.max,
+                lowThreshold: mapInfo.min,
                 removeBg: true,
               })
             }
@@ -55,7 +58,7 @@ export class LayerCtrlEffects {
         ),
         catchError(() => of(
           atlasAppearance.actions.removeCustomLayer({
-            id: NehubaLayerControlService.PMAP_LAYER_NAME
+            id: PMAP_LAYER_NAME
           })
         ))
       )
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
index ba8b1d0adbe40c1c00fb794cac66a0b1890c25ae..da9855ecb93b0344c2239776590f0625dd20117a 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
@@ -13,6 +13,9 @@ import { arrayEqual } from "src/util/array";
 import { ColorMapCustomLayer } from "src/state/atlasAppearance";
 import { SapiRegionModel } from "src/atlasComponents/sapi";
 import { AnnotationLayer } from "src/atlasComponents/annotations";
+import { PMAP_LAYER_NAME } from "../constants"
+import { EnumColorMapName, mapKeyColorMap } from "src/util/colorMaps";
+import { getShader } from "src/util/constants";
 
 export const BACKUP_COLOR = {
   red: 255,
@@ -25,8 +28,6 @@ export const BACKUP_COLOR = {
 })
 export class NehubaLayerControlService implements OnDestroy{
 
-  static PMAP_LAYER_NAME = 'regional-pmap'
-
   private selectedRegion$ = this.store$.pipe(
     select(atlasSelection.selectors.selectedRegions),
     shareReplay(1),
@@ -175,14 +176,23 @@ export class NehubaLayerControlService implements OnDestroy{
      * on custom landmarks loaded, set mesh transparency
      */
     this.sub.push(
-      this.store$.pipe(
-        select(annotation.selectors.annotations),
+      merge(
+        this.store$.pipe(
+          select(annotation.selectors.annotations),
+          map(landmarks => landmarks.length > 0),
+        ),
+        this.store$.pipe(
+          select(atlasAppearance.selectors.customLayers),
+          map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer" && /^swc:\/\//.test(l.source)).length > 0),
+        )
+      ).pipe(
+        startWith(false),
         withLatestFrom(this.defaultNgLayers$)
-      ).subscribe(([landmarks, { tmplAuxNgLayers }]) => {
+      ).subscribe(([flag, { tmplAuxNgLayers }]) => {
         const payload: {
           [key: string]: number
         } = {}
-        const alpha = landmarks.length > 0
+        const alpha = flag
           ? 0.2
           : 1.0
         for (const ngId in tmplAuxNgLayers) {
@@ -276,15 +286,20 @@ export class NehubaLayerControlService implements OnDestroy{
 
   private ngLayersRegister: atlasAppearance.NgLayerCustomLayer[] = []
 
-  private updateCustomLayerTransparency$ = this.store$.pipe(
-    select(atlasAppearance.selectors.customLayers),
-    map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
-    pairwise(),
-    map(([ oldCustomLayers, newCustomLayers ]) => {
-      return newCustomLayers.filter(({ id, opacity }) => oldCustomLayers.some(({ id: oldId, opacity: oldOpacity }) => oldId === id && oldOpacity !== opacity))
-    }),
-    filter(arr => arr.length > 0)
-  )
+  private getUpdatedCustomLayer(isSameLayer: (o: atlasAppearance.NgLayerCustomLayer, n: atlasAppearance.NgLayerCustomLayer) => boolean){
+    return this.store$.pipe(
+      select(atlasAppearance.selectors.customLayers),
+      map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
+      pairwise(),
+      map(([ oldCustomLayers, newCustomLayers ]) => {
+        return newCustomLayers.filter(n => oldCustomLayers.some(o => o.id === n.id && !isSameLayer(o, n)))
+      }),
+      filter(arr => arr.length > 0),
+    )
+  }
+
+  private updateCustomLayerTransparency$ = this.getUpdatedCustomLayer((o, n) => o.opacity === n.opacity)
+  private updateCustomLayerColorMap$ = this.getUpdatedCustomLayer((o, n) => o.shader === n.shader)
 
   private ngLayers$ = this.customLayers$.pipe(
     map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
@@ -347,6 +362,19 @@ export class NehubaLayerControlService implements OnDestroy{
         } as TNgLayerCtrl<'setLayerTransparency'>
       })
     ),
+    this.updateCustomLayerColorMap$.pipe(
+      map(layers => {
+        const payload: Record<string, string> = {}
+        for (const layer of layers) {
+          const shader = layer.shader ?? getShader()
+          payload[layer.id] = shader
+        }
+        return {
+          type: 'updateShader',
+          payload
+        } as TNgLayerCtrl<'updateShader'>
+      })
+    ),
     this.manualNgLayersControl$,
   ).pipe(
   )
@@ -367,7 +395,7 @@ export class NehubaLayerControlService implements OnDestroy{
          */
         return customLayers
           .map(l => l.id)
-          .filter(name => name !== NehubaLayerControlService.PMAP_LAYER_NAME)
+          .filter(name => name !== PMAP_LAYER_NAME)
       })
     ),
     this.customLayers$.pipe(
@@ -378,7 +406,7 @@ export class NehubaLayerControlService implements OnDestroy{
       }),
       distinctUntilChanged(),
       map(flag => flag
-        ? [ NehubaLayerControlService.PMAP_LAYER_NAME ]
+        ? [ PMAP_LAYER_NAME ]
         : []
       )
     )
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
index d5a743d67ee5dfe7ab13521b7ae0383d2e9a7524..1fa9f52ff34373fdb76dd62ddeb7acffc71ff091 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
@@ -51,6 +51,9 @@ export interface INgLayerCtrl {
   setLayerTransparency: {
     [key: string]: number
   }
+  updateShader: {
+    [key: string]: string
+  }
 }
 
 export type TNgLayerCtrl<T extends keyof INgLayerCtrl> = {
diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts
index a5f13432b611c3fee0b082338df7c3e3b086cf7d..2fcf3b824328773660a44e725872f1aafd36dfd5 100644
--- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts
+++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, ChangeDetectorRef, Component, Inject, OnDestroy } from "@angular/core";
+import { ChangeDetectorRef, Component, Inject, OnDestroy } from "@angular/core";
 import { select, Store } from "@ngrx/store";
 import { combineLatest, fromEvent, interval, merge, Observable, of, Subject, Subscription } from "rxjs";
 import { userInterface } from "src/state";
@@ -6,7 +6,7 @@ import { NehubaViewerUnit } from "../../nehubaViewer/nehubaViewer.component";
 import { NEHUBA_INSTANCE_INJTKN, takeOnePipe, getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree } from "../../util";
 import { QUICKTOUR_DESC, ARIA_LABELS, IDS } from 'common/constants'
 import { IQuickTourData } from "src/ui/quickTour/constrants";
-import { debounce, debounceTime, filter, mapTo, switchMap, take } from "rxjs/operators";
+import { debounce, debounceTime, distinctUntilChanged, filter, map, mapTo, switchMap, take } from "rxjs/operators";
 
 @Component({
   selector: `nehuba-layout-overlay`,
@@ -16,7 +16,7 @@ import { debounce, debounceTime, filter, mapTo, switchMap, take } from "rxjs/ope
   ]
 })
 
-export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{
+export class NehubaLayoutOverlay implements OnDestroy{
 
   public ARIA_LABELS = ARIA_LABELS
   public IDS = IDS
@@ -44,10 +44,6 @@ export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{
     while(this.nehubaUnitSubs.length > 0) this.nehubaUnitSubs.pop().unsubscribe()
   }
 
-  ngAfterViewInit(): void {
-    this.setQuickTourPos()
-  }
-
   handleCycleViewEvent(): void {
     if (this.currentPanelMode !== "SINGLE_PANEL") return
     this.store$.dispatch(
@@ -124,6 +120,7 @@ export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{
       nehuba$.subscribe(nehuba => {
         this.nehubaUnit = nehuba
         this.onNewNehubaUnit(nehuba)
+        this.setQuickTourPos()
       })
     )
   }
@@ -154,11 +151,17 @@ export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{
       fromEvent<CustomEvent>(
         nehubaUnit.elementRef.nativeElement,
         'sliceRenderEvent'
-      ).subscribe(ev => {
-        const { missingImageChunks, missingChunks } = ev.detail
+      ).pipe(
+        map(ev => {
+          const { missingImageChunks, missingChunks } = ev.detail
+          return { missingImageChunks, missingChunks }
+        }),
+        distinctUntilChanged((o, n) => o.missingChunks === n.missingChunks && o.missingImageChunks === n.missingImageChunks)
+      ).subscribe(({ missingImageChunks, missingChunks }) => {
         this.volumeChunkLoading$.next(
-          missingImageChunks.length === 0 && missingChunks.length === 0
+          missingImageChunks > 0 || missingChunks > 0
         )
+        this.detectChanges()
       }),
 
       /**
diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html
index e716c29fac236a4c7394b76a68a5027ffaf94646..245fb9f07ed2da09550b1bc63a6f1d9497a924cb 100644
--- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html
+++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html
@@ -68,7 +68,7 @@
     [attr.data-viewer-controller-visible]="visible"
     [attr.data-viewer-controller-index]="panelIndex">
 
-    <div class="position-absolute w-100 h-100 pe-none"
+    <div class="position-absolute w-100 h-100 sxplr-pe-none"
       *ngIf="panelIndex === 1"
       quick-tour
       [quick-tour-description]="quickTourIconsSlide.description"
diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts
index 62a4ce09eb3d14682498812aee8e9e90dd01b253..ef3ac53320fbfa6ee86ec80ac140c105a2534af8 100644
--- a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts
+++ b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts
@@ -7,7 +7,7 @@ import { SapiRegionModel } from "src/atlasComponents/sapi"
 import * as configSvc from "../config.service"
 import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects"
 import { NEVER, of, pipe } from "rxjs"
-import { mapTo } from "rxjs/operators"
+import { mapTo, take } from "rxjs/operators"
 import { selectorAuxMeshes } from "../store"
 
 
@@ -51,6 +51,12 @@ describe('> mesh.service.ts', () => {
       )
     )
   })
+
+  afterEach(() => {
+    getParcNgIdSpy.calls.reset()
+    getRegionLabelIndexSpy.calls.reset()
+    getATPSpy.calls.reset()
+  })
   describe('> NehubaMeshService', () => {
     beforeEach(() => {
       TestBed.configureTestingModule({
@@ -72,37 +78,106 @@ describe('> mesh.service.ts', () => {
       expect(service).toBeTruthy()
     })
 
-    it('> mixes in auxillaryMeshIndices', () => {
-      const mockStore = TestBed.inject(MockStore)
-      mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ])
-      mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, [])
-      mockStore.overrideSelector(selectorAuxMeshes, [auxMesh])
+    describe("> loadMeshes$", () => {
 
-      const ngId = 'blabla'
-      const labelIndex = 12
-      getParcNgIdSpy.and.returnValue(ngId)
-      getRegionLabelIndexSpy.and.returnValue(labelIndex)
+      describe("> auxMesh defined", () => {
+
+        const ngId = 'blabla'
+        const labelIndex = 12
+
+        beforeEach(() => {
+
+          const mockStore = TestBed.inject(MockStore)
+          mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ])
+          mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, [])
+          mockStore.overrideSelector(selectorAuxMeshes, [auxMesh])
+    
+          getParcNgIdSpy.and.returnValue(ngId)
+          getRegionLabelIndexSpy.and.returnValue(labelIndex)
 
-      const service = TestBed.inject(NehubaMeshService)
-      
-      expect(
-        service.loadMeshes$
-      ).toBeObservable(
-        hot('(ab)', {
-          a: {
-            layer: {
-              name: ngId
-            },
-            labelIndicies: [ labelIndex ]
-          },
-          b: {
-            layer: {
-              name: auxMesh.ngId,
-            },
-            labelIndicies: auxMesh.labelIndicies
-          }
         })
-      )
+
+        it("> auxMesh ngId labelIndex emitted", () => {
+
+          const service = TestBed.inject(NehubaMeshService)
+          expect(
+            service.loadMeshes$
+          ).toBeObservable(
+            hot('(ab)', {
+              a: {
+                layer: {
+                  name: ngId
+                },
+                labelIndicies: [ labelIndex ]
+              },
+              b: {
+                layer: {
+                  name: auxMesh.ngId,
+                },
+                labelIndicies: auxMesh.labelIndicies
+              }
+            })
+          )
+        })
+      })
+
+      describe("> if multiple ngid and labelindicies are present", () => {
+
+        const ngId1 = 'blabla'
+        const labelIndex1 = 12
+
+        const ngId2 = 'foobar'
+        const labelIndex2 = 13
+
+        beforeEach(() => {
+
+          const mockStore = TestBed.inject(MockStore)
+          mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ])
+          mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, [fits1, fits1])
+          mockStore.overrideSelector(selectorAuxMeshes, [])
+    
+          getParcNgIdSpy.and.returnValues(ngId1, ngId2, ngId2)
+          getRegionLabelIndexSpy.and.returnValues(labelIndex1, labelIndex2, labelIndex2)
+        })
+
+        it('> should call getParcNgIdSpy and getRegionLabelIndexSpy thrice', () => {
+          const service = TestBed.inject(NehubaMeshService)
+          service.loadMeshes$.pipe(
+            take(1)
+          ).subscribe(() => {
+
+            expect(getParcNgIdSpy).toHaveBeenCalledTimes(3)
+            expect(getRegionLabelIndexSpy).toHaveBeenCalledTimes(3)
+          })
+        })
+
+        /**
+         * in the case of julich brain 2.9 in colin 27, we expect selecting a region will hide meshes from all relevant ngIds (both left and right)
+         */
+        it('> expect the emitted value to be incl all ngIds', () => {
+          const service = TestBed.inject(NehubaMeshService)
+          expect(
+            service.loadMeshes$
+          ).toBeObservable(
+            hot('(ab)', {
+              a: {
+                layer: {
+                  name: ngId1
+                },
+                labelIndicies: []
+              },
+              b: {
+                layer: {
+                  name: ngId2
+                },
+                labelIndicies: [ labelIndex2 ]
+              }
+            })
+          )
+
+        })
+      })
+
     })
   })
 })
diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts
index 2585f00224e8729a4434afa568e127a486e66fcb..d372ce460746d01c5cf560518e1532f38616b882 100644
--- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts
+++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts
@@ -47,7 +47,40 @@ export class NehubaMeshService implements OnDestroy {
     ]).pipe(
       switchMap(([{ atlas, template, parcellation }, regions, selectedRegions]) => {
         const ngIdRecord: Record<string, number[]> = {}
+        
+        const tree = new Tree(
+          regions,
+          (c, p) => (c.hasParent || []).some(_p => _p["@id"] === p["@id"])
+        )
+
+        for (const r of regions) {
+          const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r )
+          if (!regionLabelIndex) {
+            continue
+          }
+          if (
+            tree.someAncestor(r, anc => !!getRegionLabelIndex(atlas, template, parcellation, anc))
+          ) {
+            continue
+          }
+          const ngId = getParcNgId(atlas, template, parcellation, r)
+          if (!ngIdRecord[ngId]) {
+            ngIdRecord[ngId] = []
+          }
+          ngIdRecord[ngId].push(regionLabelIndex)
+        }
+
         if (selectedRegions.length > 0) {
+          /**
+           * If regions are selected, reset the meshes
+           */
+          for (const key in ngIdRecord) {
+            ngIdRecord[key] = []
+          }
+
+          /**
+           * only show selected region
+           */
           for (const r of selectedRegions) {
             const ngId = getParcNgId(atlas, template, parcellation, r)
             const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r )
@@ -56,28 +89,6 @@ export class NehubaMeshService implements OnDestroy {
             }
             ngIdRecord[ngId].push(regionLabelIndex)
           }
-        } else {
-          const tree = new Tree(
-            regions,
-            (c, p) => (c.hasParent || []).some(_p => _p["@id"] === p["@id"])
-          )
-  
-          for (const r of regions) {
-            const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r )
-            if (!regionLabelIndex) {
-              continue
-            }
-            if (
-              tree.someAncestor(r, (anc) => !!getRegionLabelIndex(atlas, template, parcellation, anc))
-            ) {
-              continue
-            }
-            const ngId = getParcNgId(atlas, template, parcellation, r)
-            if (!ngIdRecord[ngId]) {
-              ngIdRecord[ngId] = []
-            }
-            ngIdRecord[ngId].push(regionLabelIndex)
-          }  
         }
         const arr: IMeshesToLoad[] = []
 
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts
index c6437fc7c93844af930c70ce2c5ae019227f76cf..6c5bb5aeb0c02634389135fe0056c5b88644d89b 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts
@@ -1,7 +1,6 @@
 import { TestBed, fakeAsync, tick, ComponentFixture } from "@angular/core/testing"
 import { CommonModule } from "@angular/common"
 import { NehubaViewerUnit, IMPORT_NEHUBA_INJECT_TOKEN, scanFn } from "./nehubaViewer.component"
-import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"
 import { LoggingModule, LoggingService } from "src/logging"
 import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants"
 import { Subject } from "rxjs"
@@ -106,7 +105,6 @@ describe('> nehubaViewer.component.ts', () => {
             provide: SET_COLORMAP_OBS,
             useValue: setcolorMap$
           },
-          AtlasWorkerService,
           LoggingService,
         ]
       }).compileComponents()
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
index cfd902f1a76b4dc530b4d30050c96d70e1edff05..4d38f0487b48b24a678e8a34b454d521d102e94e 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
@@ -1,7 +1,6 @@
 import { Component, ElementRef, EventEmitter, OnDestroy, Output, Inject, Optional } from "@angular/core";
-import { fromEvent, Subscription, BehaviorSubject, Observable, Subject, of, interval } from 'rxjs'
-import { debounceTime, filter, map, scan, switchMap, take, distinctUntilChanged, debounce } from "rxjs/operators";
-import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
+import { Subscription, BehaviorSubject, Observable, Subject, of, interval } from 'rxjs'
+import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce } from "rxjs/operators";
 import { LoggingService } from "src/logging";
 import { bufferUntil, getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn";
 import { deserializeSegment, NEHUBA_INSTANCE_INJTKN } from "../util";
@@ -135,7 +134,6 @@ export class NehubaViewerUnit implements OnDestroy {
 
   constructor(
     public elementRef: ElementRef,
-    private workerService: AtlasWorkerService,
     private log: LoggingService,
     @Inject(IMPORT_NEHUBA_INJECT_TOKEN) getImportNehubaPr: () => Promise<any>,
     @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Subject<NehubaViewerUnit>,
@@ -179,67 +177,6 @@ export class NehubaViewerUnit implements OnDestroy {
       })
       .catch(e => this.errorEmitter.emit(e))
 
-
-    /**
-     * TODO move to layerCtrl.service
-     */
-    this.ondestroySubscriptions.push(
-      fromEvent(this.workerService.worker, 'message').pipe(
-        filter((message: any) => {
-
-          if (!message) {
-            // this.log.error('worker response message is undefined', message)
-            return false
-          }
-          if (!message.data) {
-            // this.log.error('worker response message.data is undefined', message.data)
-            return false
-          }
-          if (message.data.type !== 'ASSEMBLED_USERLANDMARKS_VTK') {
-            /* worker responded with not assembled landmark, no need to act */
-            return false
-          }
-          /**
-           * nb url may be undefined
-           * if undefined, user have removed all user landmarks, and all that needs to be done
-           * is remove the user landmark layer
-           *
-           * message.data.url
-           */
-
-          return true
-        }),
-        debounceTime(100),
-        map(e => e.data.url),
-      ).subscribe(url => {
-        this.landmarksLoaded = !!url
-        this.removeuserLandmarks()
-
-        /**
-         * url may be null if user removes all landmarks
-         */
-        if (!url) {
-          /**
-           * remove transparency from meshes in current layer(s)
-           */
-          this.setMeshTransparency(false)
-          return
-        }
-        const _ = {}
-        _[NG_USER_LANDMARK_LAYER_NAME] = {
-          type: 'mesh',
-          source: `vtk://${url}`,
-          shader: this.userLandmarkShader,
-        }
-        this.loadLayer(_)
-
-        /**
-         * adding transparency to meshes in current layer(s)
-         */
-        this.setMeshTransparency(true)
-      }),
-    )
-  
     if (this.setColormap$) {
       this.ondestroySubscriptions.push(
         this.setColormap$.pipe(
@@ -349,6 +286,12 @@ export class NehubaViewerUnit implements OnDestroy {
                 this.setLayerTransparency(key, p.payload[key])
               }
             }
+            if (message.type === "updateShader") {
+              const p = message as TNgLayerCtrl<'updateShader'>
+              for (const key in p.payload) {
+                this.setLayerShader(key, p.payload[key])
+              }
+            }
           }
         })
       )
@@ -540,37 +483,6 @@ export class NehubaViewerUnit implements OnDestroy {
   }
 
   private userLandmarkShader: string = FRAGMENT_MAIN_WHITE
-  
-  // TODO single landmark for user landmark
-  public updateUserLandmarks(landmarks: any[]) {
-    if (!this.nehubaViewer) {
-      return
-    }
-    
-    this.workerService.worker.postMessage({
-      type : 'GET_USERLANDMARKS_VTK',
-      scale: Math.min(...this.dim.map(v => v * NG_LANDMARK_CONSTANT)),
-      landmarks : landmarks.map(lm => lm.position.map(coord => coord * 1e6)),
-    })
-
-    const parseLmColor = lm => {
-      if (!lm) return null
-      const { color } = lm
-      if (!color) return null
-      if (!Array.isArray(color)) return null
-      if (color.length !== 3) return null
-      const parseNum = num => (num >= 0 && num <= 255 ? num / 255 : 1).toFixed(3)
-      return `emitRGB(vec3(${color.map(parseNum).join(',')}));`
-    }
-  
-    const appendConditional = (frag, idx) => frag && `if (label > ${idx - 0.01} && label < ${idx + 0.01}) { ${frag} }`
-
-    if (landmarks.some(parseLmColor)) {
-      this.userLandmarkShader = `void main(){ ${landmarks.map(parseLmColor).map(appendConditional).filter(v => !!v).join('else ')} else {${FRAGMENT_EMIT_WHITE}} }`
-    } else {
-      this.userLandmarkShader = FRAGMENT_MAIN_WHITE  
-    }
-  }
 
   public removeSpatialSearch3DLandmarks() {
     this.removeLayer({
@@ -796,6 +708,11 @@ export class NehubaViewerUnit implements OnDestroy {
     if (layer.layer.opacity) layer.layer.opacity.restoreState(alpha)
   }
 
+  private setLayerShader(layerName: string, shader: string) {
+    const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
+    if (layer?.layer?.fragmentMain) layer.layer.fragmentMain.restoreState(shader)
+  }
+
   public setMeshTransparency(flag: boolean){
 
     /**
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
index 0c7b084eb8dab41764d88d913578e8eeb843478b..5c16b0a065f1f5b2d14be81d724230ed30b7b301 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
@@ -20,6 +20,7 @@ import { SapiRegionModel } from "src/atlasComponents/sapi";
 import { NehubaConfig, getParcNgId, getRegionLabelIndex } from "../config.service";
 import { SET_MESHES_TO_LOAD } from "../constants";
 import { annotation, atlasAppearance, atlasSelection, userInteraction } from "src/state";
+import { linearTransform, TVALID_LINEAR_XFORM_DST, TVALID_LINEAR_XFORM_SRC } from "src/atlasComponents/sapi/core/space/interspaceLinearXform";
 
 export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!`
 
@@ -284,8 +285,45 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni
     /**
      * TODO check extension?
      */
-     
     this.dismissAllAddedLayers()
+
+    if (/\.swc$/i.test(file.name)) {
+      let message = `The swc rendering is experimental. Please contact us on any feedbacks. `
+      const swcText = await file.text()
+      let src: TVALID_LINEAR_XFORM_SRC
+      const dst: TVALID_LINEAR_XFORM_DST = "NEHUBA"
+      if (/ccf/i.test(swcText)) {
+        src = "CCF"
+        message += `CCF detected, applying known transformation.`
+      }
+      if (!src) {
+        message += `no known space detected. Applying default transformation.`
+      }
+
+      const xform = await linearTransform(src, dst)
+      
+      const url = URL.createObjectURL(file)
+      this.droppedLayerNames.push({
+        layerName: randomUuid,
+        resourceUrl: url
+      })
+      this.store$.dispatch(
+        atlasAppearance.actions.addCustomLayer({
+          customLayer: {
+            id: randomUuid,
+            source: `swc://${url}`,
+            segments: ["1"],
+            transform: xform,
+            clType: 'customlayer/nglayer' as const
+          }
+        })
+      )
+      this.snackbar.open(message, "Dismiss", {
+        duration: 10000
+      })
+      return
+    }
+     
     
     // Get file, try to inflate, if files, use original array buffer
     const buf = await file.arrayBuffer()
diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts
index fd216151b9bbbf14d3b498eae8ffc98b2501ba05..ea321e012cccbbe69e27f0e8467b14b945876e00 100644
--- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts
+++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts
@@ -1,44 +1,22 @@
 import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges, OnDestroy } from "@angular/core";
 import { Store } from "@ngrx/store";
 import { isMat4 } from "common/util"
+import { CONST } from "common/constants"
 import { Observable } from "rxjs";
-import { atlasAppearance } from "src/state";
+import { atlasAppearance, atlasSelection } from "src/state";
 import { NehubaViewerUnit } from "..";
 import { NEHUBA_INSTANCE_INJTKN } from "../util";
+import { getExportNehuba } from "src/util/fn";
 
 type Vec4 = [number, number, number, number]
 type Mat4 = [Vec4, Vec4, Vec4, Vec4]
 
-const _VOL_DETAIL_MAP: Record<string, { shader: string, opacity: number }> = {
-  "PLI Fiber Orientation Red Channel": {
-    shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(1.0 * x, x * 0., 0. * x )); } }",
-    opacity: 1
-  },
-  "PLI Fiber Orientation Green Channel": {
-    shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0. * x, x * 1., 0. * x )); } }",
-    opacity: 0.5
-  },
-  "PLI Fiber Orientation Blue Channel": {
-    shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0. * x, x * 0., 1.0 * x )); } }",
-    opacity: 0.25
-  },
-  "Blockface Image": {
-    shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0.8 * x, x * 1., 0.8 * x )); } }",
-    opacity: 1.0
-  },
-  "PLI Transmittance": {
-    shader: "void main(){ float x = toNormalized(getDataValue()); if (x > 0.9) { emitTransparent(); } else { emitRGB(vec3(x * 1., x * 0.8, x * 0.8 )); } }",
-    opacity: 1.0
-  },
-  "T2w MRI": {
-    shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0.8 * x, 0.8 * x, x * 1. )); } }",
-    opacity: 1
-  },
-  "MRI Labels": {
-    shader: null,
-    opacity: 1
-  }
-}
+export const idMat4: Mat4 = [
+  [1, 0, 0, 0],
+  [0, 1, 0, 0],
+  [0, 0, 1, 0],
+  [0, 0, 0, 1],
+]
 
 @Component({
   selector: 'ng-layer-ctl',
@@ -51,9 +29,15 @@ const _VOL_DETAIL_MAP: Record<string, { shader: string, opacity: number }> = {
 
 export class NgLayerCtrlCmp implements OnChanges, OnDestroy{
 
+  public CONST = CONST
+
   private onDestroyCb: (() => void)[] = []
   private removeLayer: () => void
 
+  public showOpacityCtrl = false
+  public hideNgTuneCtrl = 'lower_threshold,higher_threshold,brightness,contrast,colormap,hide-threshold-checkbox'
+  public defaultOpacity = 1
+
   @Input('ng-layer-ctl-name')
   name: string
 
@@ -73,7 +57,8 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{
     this.opacity = Number(val)
   }
   
-  transform: Mat4
+  transform: Mat4 = idMat4
+
   @Input('ng-layer-ctl-transform')
   set _transform(xform: string | Mat4) {
     const parsedResult = typeof xform === "string"
@@ -107,12 +92,6 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{
   }
 
   ngOnChanges(): void {
-    if (this.name in _VOL_DETAIL_MAP) {
-      const { shader, opacity } = _VOL_DETAIL_MAP[this.name]
-      this.shader = shader
-      this.opacity = opacity
-    }
-
     if (this.name && this.source) {
       const { name } = this
       if (this.removeLayer) {
@@ -141,6 +120,28 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{
     }
   }
 
+  setOrientation(): void {
+    const { mat4, quat, vec3 } = getExportNehuba()
+
+    /**
+     * glMatrix seems to store the matrix in transposed format
+     */
+    
+    const incM = mat4.transpose(mat4.create(), mat4.fromValues(...this.transform.reduce((acc, curr) => [...acc, ...curr], [])))
+    const scale = mat4.getScaling(vec3.create(), incM)
+    const scaledM = mat4.scale(mat4.create(), incM, vec3.inverse(vec3.create(), scale))
+    const q = mat4.getRotation(quat.create(0), scaledM)
+
+    this.store.dispatch(
+      atlasSelection.actions.navigateTo({
+        navigation: {
+          orientation: Array.from(q)
+        },
+        animation: true
+      })
+    )
+  }
+
   toggleVisibility(): void{
     this.visible = !this.visible
     this.viewer.nehubaViewer.ngviewer.layerManager.getLayerByName(this.name).setVisible(this.visible)
diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b716f7d93824fd1e223450fde56655398aa91d0b
--- /dev/null
+++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts
@@ -0,0 +1,60 @@
+import { idMat4, NgLayerCtrlCmp } from "./ngLayerCtrl.component"
+import { Meta, moduleMetadata, Story } from "@storybook/angular"
+import { CommonModule } from "@angular/common"
+import { MatButtonModule } from "@angular/material/button"
+import { NEHUBA_INSTANCE_INJTKN } from "../util"
+import { NEVER } from "rxjs"
+import { action } from "@storybook/addon-actions"
+import { MatTooltipModule } from "@angular/material/tooltip"
+import { Store } from "@ngrx/store"
+
+export default {
+  component: NgLayerCtrlCmp,
+  decorators: [
+    moduleMetadata({
+      imports: [
+        CommonModule,
+        MatButtonModule,
+        MatTooltipModule,
+      ],
+      providers: [
+        {
+          provide: NEHUBA_INSTANCE_INJTKN,
+          useValue: NEVER,
+        },
+        {
+          provide: Store,
+          useValue: {
+            dispatch: action('dispatch')
+          }
+        }
+      ]
+    }),
+  ]
+} as Meta
+
+
+const Template: Story<NgLayerCtrlCmp> = (args: any, { parameters }) => {
+
+  const {
+    'ng-layer-ctl-name': name,
+    'ng-layer-ctl-transform': transform
+  } = args
+
+  const {
+    pName,
+    pXform
+  } = parameters
+  
+  return {
+    props: {
+      name: name || pName || 'default name',
+      transform: transform || pXform || idMat4,
+    }
+  }
+}
+
+export const NgLayerTune = Template.bind({})
+NgLayerTune.parameters = {
+  pXform: [[-0.74000001,0,0,38134608],[0,-0.26530117,-0.6908077,13562314],[0,-0.6908077,0.26530117,-3964904],[0,0,0,1]]
+}
diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html
index 2188b17d9f57f496928a0b40f2ba78e3508be9d5..b53ec540ad7fd2bcf7df004327d9cc3f2b3ba51b 100644
--- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html
+++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html
@@ -1,10 +1,34 @@
 <div [ngClass]="{ 'text-muted': !visible }">
 
-  <button mat-icon-button (click)="toggleVisibility()">
+  <button mat-icon-button
+    [matTooltip]="CONST.TOGGLE_LAYER_VISILITY"
+    (click)="toggleVisibility()">
     <i [ngClass]="visible ? 'fa-eye' : 'fa-eye-slash'" class="far"></i>
   </button>
   
   <span>
     {{ name }}
   </span>
+
+  <button
+    mat-icon-button
+    [matTooltip]="CONST.ORIENT_TO_LAYER"
+    (click)="setOrientation()">
+    <i class="iavic iavic-rotation"></i>
+  </button>
+
+  <button
+    mat-icon-button
+    [matTooltip]="CONST.CONFIGURE_LAYER"
+    (click)="showOpacityCtrl = !showOpacityCtrl">
+    <i class="fas fa-cog"></i>
+  </button>
+
+  <ng-template [ngIf]="showOpacityCtrl">
+    <ng-layer-tune
+      [ngLayerName]="name"
+      [hideCtrl]="hideNgTuneCtrl"
+      [opacity]="defaultOpacity">
+    </ng-layer-tune>
+  </ng-template>
 </div>
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts
index ce0afa8fa8632b433b5998eed1107654fdc4dfa2..ab6ac511153553a1bc50365bd2b85c4a239c94f4 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts
+++ b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts
@@ -146,8 +146,8 @@ describe('> statusCard.component.ts', () => {
           initialNgState: {
             navigation: {
               pose: {
-                orientation: [0,0,0,1],
-                position: [10, 20, 30]
+                orientation: [0, 0, 0, 1],
+                position: [0, 0, 0]
               },
               zoomFactor: 1e6
             }
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
index 26d6786e8b7f7ea3723eb47d7a32bc299c46b720..58bbdf911ed843cd5e68123f65869fe5843b3ace 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts
+++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
@@ -177,10 +177,7 @@ export class StatusCardComponent implements OnInit, OnChanges{
    */
   public resetNavigation({rotation: rotationFlag = false, position: positionFlag = false, zoom : zoomFlag = false}: {rotation?: boolean, position?: boolean, zoom?: boolean}): void {
     const config = getNehubaConfig(this.selectedTemplate)
-    const {
-      orientation,
-      position
-    } = config.dataset.initialNgState.navigation.pose
+
     const {
       zoomFactor: zoom
     } = config.dataset.initialNgState.navigation
@@ -189,8 +186,8 @@ export class StatusCardComponent implements OnInit, OnChanges{
       actions.navigateTo({
         navigation: {
           ...this.currentNavigation,
-          ...(rotationFlag ? { orientation: orientation } : {}),
-          ...(positionFlag ? { position: position } : {}),
+          ...(rotationFlag ? { orientation: [0, 0, 0, 1] } : {}),
+          ...(positionFlag ? { position: [0, 0, 0] } : {}),
           ...(zoomFlag ? { zoom: zoom } : {}),
         },
         physical: true,
diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts
index c5042ecfc22e3134ab6af2c967bdad86bf5d4933..0ec1d5125ba83f89dc91ea8e20e33954673030a7 100644
--- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts
+++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts
@@ -22,7 +22,6 @@ describe('> viewerCtrlCmp.component.ts', () => {
     let mockStore: MockStore
 
     let mockNehubaViewer = {
-      updateUserLandmarks: jasmine.createSpy(),
       nehubaViewer: {
         ngviewer: {
           layerManager: {
@@ -42,7 +41,6 @@ describe('> viewerCtrlCmp.component.ts', () => {
     }
 
     afterEach(() => {
-      mockNehubaViewer.updateUserLandmarks.calls.reset()
       mockNehubaViewer.nehubaViewer.ngviewer.layerManager.getLayerByName.calls.reset()
       mockNehubaViewer.nehubaViewer.ngviewer.display.scheduleRedraw.calls.reset()
     })
diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
index a194176aa6d83027be793d3ebe1d098aa75f8c8d..f04ad6cc1b0a5b85875c6bc247150b737eb5605c 100644
--- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
+++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
@@ -328,7 +328,8 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit
      * subscribe to main store and negotiate with relay to set camera
      */
     const navSub = this.store$.pipe(
-      select(atlasSelection.selectors.navigation)
+      select(atlasSelection.selectors.navigation),
+      filter(v => !!v),
     ).subscribe(nav => {
       const { perspectiveOrientation, perspectiveZoom } = nav
       this.mainStoreCameraNav = {
diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts
index d28f45292763e4b62385e8829198849d62b8af58..f0b32efea3c8233a648e0277bfd38ddeaebe2074 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.component.ts
+++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts
@@ -12,6 +12,9 @@ import { SAPI, SapiRegionModel } from "src/atlasComponents/sapi";
 import { atlasSelection, userInteraction, } from "src/state";
 import { SapiSpatialFeatureModel, SapiFeatureModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type";
 import { getUuid } from "src/util/fn";
+import { environment } from "src/environments/environment"
+import { SapiViewsFeaturesVoiQuery } from "src/atlasComponents/sapiViews/features";
+import { SapiViewsCoreSpaceBoundingBox } from "src/atlasComponents/sapiViews/core";
 
 @Component({
   selector: 'iav-cmp-viewer-container',
@@ -62,10 +65,17 @@ export class ViewerCmp implements OnDestroy {
 
   public CONST = CONST
   public ARIA_LABELS = ARIA_LABELS
+  public VOI_QUERY_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG
 
   @ViewChild('genericInfoVCR', { read: ViewContainerRef })
   genericInfoVCR: ViewContainerRef
 
+  @ViewChild('voiFeatures', { read: SapiViewsFeaturesVoiQuery })
+  voiQueryDirective: SapiViewsFeaturesVoiQuery
+
+  @ViewChild('bbox', { read: SapiViewsCoreSpaceBoundingBox })
+  boundingBoxDirective: SapiViewsCoreSpaceBoundingBox
+
   public quickTourRegionSearch: IQuickTourData = {
     order: 7,
     description: QUICKTOUR_DESC.REGION_SEARCH,
@@ -321,6 +331,7 @@ export class ViewerCmp implements OnDestroy {
     switch(event.type) {
     case EnumViewerEvt.VIEWERLOADED:
       this.viewerLoaded = event.data
+      this.cdr.detectChanges()
       break
     case EnumViewerEvt.VIEWER_CTX:
       this.ctxMenuSvc.context$.next(event.data)
diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css
index 671fef08c4faf6d72e1cfd3c9664208fb60261b7..e5a409a28036bfe88dab0ce9457272ed7647cf60 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.style.css
+++ b/src/viewerModule/viewerCmp/viewerCmp.style.css
@@ -118,3 +118,25 @@ mat-list[dense].contextual-block
 {
   background-color : rgba(30,30,30,0.8);
 }
+
+.region-populated
+{
+  overflow: hidden auto;
+}
+
+.region-chip-suffix
+{
+  transform: scale(0.7);
+  margin-right: -0.25rem;
+}
+
+.leap-control-wrapper
+{
+  width: 0;
+  height: 0;
+  overflow: visible;
+  flex-direction: row;
+  display: flex;
+  align-items: flex-end;
+  justify-content: flex-end;
+}
diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html
index 891cdb9f42e5cecfe7732c6004bdaae24a2dab6a..b0e06fdaab9dcdca386cf69563c8334f2313336e 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.template.html
+++ b/src/viewerModule/viewerCmp/viewerCmp.template.html
@@ -4,12 +4,12 @@
 
   <div class="floating-ui">
 
-    <div *ngIf="(media.mediaBreakPoint$ | async) < 3"
-      class="fixed-bottom pe-none mb-2 d-flex justify-content-center">
+    <div *ngIf="(media.mediaBreakPoint$ | async) < 2"
+      class="fixed-bottom sxplr-pe-none mb-2 d-flex justify-content-center">
       <logo-container></logo-container>
     </div>
 
-    <div *ngIf="(media.mediaBreakPoint$ | async) < 3" floatingMouseContextualContainerDirective>
+    <div *ngIf="(media.mediaBreakPoint$ | async) < 2" floatingMouseContextualContainerDirective>
 
       <div class="h-0"
         iav-mouse-hover
@@ -29,7 +29,7 @@
 
         </mat-list-item>
 
-        <ng-template [ngIf]="voiFeatures.onhover | async" let-feat>
+        <ng-template [ngIf]="voiQueryDirective && (voiQueryDirective.onhover | async)" let-feat>
           <mat-list-item>
             <mat-icon
               fontSet="fas"
@@ -99,7 +99,7 @@
   </mat-drawer>
 
   <!-- master content -->
-  <mat-drawer-content class="visible pe-none position-relative">
+  <mat-drawer-content class="visible sxplr-pe-none position-relative">
     <iav-layout-fourcorners>
 
       <!-- top left -->
@@ -148,7 +148,7 @@
 
 
       <!-- bottom left -->
-      <div iavLayoutFourCornersBottomLeft class="ws-no-wrap d-inline-flex w-100vw pe-none align-items-center mb-4">
+      <div iavLayoutFourCornersBottomLeft class="ws-no-wrap d-inline-flex w-100vw sxplr-pe-none align-items-center mb-4">
 
         <!-- special bottom left -->
         <ng-template [ngIf]="viewerMode$ | async" let-mode [ngIfElse]="localBottomLeftTmpl"></ng-template>
@@ -169,6 +169,13 @@
         </ng-template>
         
       </div>
+
+      <!-- buttom right -->
+      <div iavLayoutFourCornersBottomRight>
+        <div class="leap-control-wrapper">
+          <div leap-control-view-ref></div>
+        </div>
+      </div>
     </iav-layout-fourcorners>
   </mat-drawer-content>
 </mat-drawer-container>
@@ -223,9 +230,11 @@
       <ng-container *ngTemplateOutlet="autocompleteTmpl; context: { showTour: true }">
       </ng-container>
       
-      <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-2 w-100">
-        <ng-container *ngTemplateOutlet="spatialFeatureListViewTmpl"></ng-container>
-      </div>
+      <ng-template [ngIf]="VOI_QUERY_FLAG">
+        <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-2 w-100">
+          <ng-container *ngTemplateOutlet="spatialFeatureListViewTmpl"></ng-container>
+        </div>
+      </ng-template>
     </div>
 
     <!-- such a gross implementation -->
@@ -264,7 +273,7 @@
       isOpen: minTrayVisSwitch.switchState$ | async,
       regionSelected: selectedRegions$ | async,
       click: minTrayVisSwitch.toggle.bind(minTrayVisSwitch),
-      badge: (voiFeatures.features$ | async).length || null
+      badge: voiQueryDirective && (voiQueryDirective.features$ | async).length || null
     }">
     </ng-container>
   </div>
@@ -434,7 +443,7 @@
       <div prefix>
       </div>
 
-      <div suffix class="sxplr-scale-70">
+      <div suffix class="region-chip-suffix">
         <button mat-mini-fab
           color="primary"
           iav-stop="mousedown click"
@@ -543,7 +552,7 @@
       <i class="fas fa-sitemap"></i>
     </button>
 
-    <div class="w-100 h-100 position-absolute pe-none" *ngIf="showTour">
+    <div class="w-100 h-100 position-absolute sxplr-pe-none" *ngIf="showTour">
     </div>
 
   </div>
@@ -683,7 +692,7 @@
         <!-- see https://github.com/HumanBrainProject/interactive-viewer/issues/698 -->
 
 
-        <ng-template [ngIf]="regionDirective.fetchInProgress">
+        <ng-template [ngIf]="regionDirective.fetchInProgress$ | async">
           <spinner-cmp class="sxplr-mt-10 fs-200"></spinner-cmp>
         </ng-template>
         <sxplr-sapiviews-core-region-region-rich
@@ -983,18 +992,18 @@
 </ng-template>
 
 <ng-template #spatialFeatureListViewTmpl>
-  <div *ngIf="voiFeatures.busy$ | async; else notBusyTmpl" class="fs-200">
+  <div *ngIf="voiQueryDirective && (voiQueryDirective.busy$ | async); else notBusyTmpl" class="fs-200">
     <spinner-cmp></spinner-cmp>
   </div>
 
   <ng-template #notBusyTmpl>
-    <mat-card *ngIf="(voiFeatures.features$ | async).length > 0" class="pe-all mat-elevation-z4">
+    <mat-card *ngIf="voiQueryDirective && (voiQueryDirective.features$ | async).length > 0" class="pe-all mat-elevation-z4">
       <mat-card-title>
         Volumes of interest
       </mat-card-title>
       <mat-card-subtitle class="overflow-hidden">
         <!-- TODO in future, perhaps encapsulate this as a component? seems like a nature fit in sapiView/space/boundingbox -->
-        <ng-template let-bbox [ngIf]="bbox.bbox$ | async | getProperty : 'bbox'" [ngIfElse]="bboxFallbackTmpl">
+        <ng-template let-bbox [ngIf]="boundingBoxDirective && (boundingBoxDirective.bbox$ | async | getProperty : 'bbox')" [ngIfElse]="bboxFallbackTmpl">
           Bounding box: {{ bbox[0] | numbers | json }} - {{ bbox[1] | numbers | json }} mm
         </ng-template>
         <ng-template #bboxFallbackTmpl>
@@ -1005,17 +1014,21 @@
 
       <mat-divider></mat-divider>
 
-      <div *ngFor="let feature of voiFeatures.features$ | async"
-        mat-ripple
-        (click)="showDataset(feature)"
-        class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses">
-        {{ feature.metadata.fullName }}
-      </div>
+      <ng-template [ngIf]="voiQueryDirective">
+
+        <div *ngFor="let feature of voiQueryDirective.features$ | async"
+          mat-ripple
+          (click)="showDataset(feature)"
+          class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses">
+          {{ feature.metadata.fullName }}
+        </div>
+      </ng-template>
     </mat-card>
   </ng-template>
 </ng-template>
 
 <div class="d-none"
+  *ngIf="VOI_QUERY_FLAG"
   sxplr-sapiviews-core-space-boundingbox
   [sxplr-sapiviews-core-space-boundingbox-atlas]="selectedAtlas$ | async"
   [sxplr-sapiviews-core-space-boundingbox-space]="templateSelected$ | async"
diff --git a/src/widget/constants.ts b/src/widget/constants.ts
index 412bccd405d746982f24081f8d405cd7011aeb53..53cc6568a34d662fa229e00592ca9b92ba0a3e08 100644
--- a/src/widget/constants.ts
+++ b/src/widget/constants.ts
@@ -21,3 +21,11 @@ interface TypeActionWidgetReturnVal<T>{
 export type TypeActionToWidget<T> = (type: EnumActionToWidget, obj: T, option: IActionWidgetOption) => TypeActionWidgetReturnVal<T>
 
 export const WIDGET_PORTAL_TOKEN = new InjectionToken<Record<string, unknown>>("WIDGET_PORTAL_TOKEN")
+
+export const RM_WIDGET = new InjectionToken('RM_WIDGET')
+
+export enum EnumWidgetState {
+  MINIMIZED,
+  NORMAL,
+  MAXIMIZED,
+}
diff --git a/src/widget/service.ts b/src/widget/service.ts
index 75be508867a61aa48951701ff91a43dbab8045cd..9430269418731ceb3b57810b54b26ef155ef7ef0 100644
--- a/src/widget/service.ts
+++ b/src/widget/service.ts
@@ -1,5 +1,6 @@
 import { ComponentPortal } from "@angular/cdk/portal";
 import { ComponentFactory, ComponentFactoryResolver, ComponentRef, Injectable, Injector, ViewContainerRef } from "@angular/core";
+import { RM_WIDGET } from "./constants";
 import { WidgetPortal } from "./widgetPortal/widgetPortal.component";
 
 @Injectable({
@@ -18,8 +19,15 @@ export class WidgetService {
   }
 
   public addNewWidget<T>(Component: new (...arg: any) => T, injector: Injector): WidgetPortal<T> {
-    const widgetPortal = this.vcr.createComponent(this.cf, 0, injector) as ComponentRef<WidgetPortal<T>>
-    const cmpPortal = new ComponentPortal<T>(Component, this.vcr, injector)
+    const inj = Injector.create({
+      providers: [{
+        provide: RM_WIDGET,
+        useValue: (cmp: WidgetPortal<T>) => this.rmWidget(cmp)
+      }],
+      parent: injector
+    })
+    const widgetPortal = this.vcr.createComponent(this.cf, 0, inj) as ComponentRef<WidgetPortal<T>>
+    const cmpPortal = new ComponentPortal<T>(Component, this.vcr, inj)
     
     this.viewRefMap.set(widgetPortal.instance, widgetPortal)
 
diff --git a/src/widget/widget.module.ts b/src/widget/widget.module.ts
index 138ed66482a78ad917959b416aca333c415c09b0..62b9d65ba70505d09aef098617ec2681bb657294 100644
--- a/src/widget/widget.module.ts
+++ b/src/widget/widget.module.ts
@@ -2,17 +2,20 @@ import { NgModule } from "@angular/core";
 import { CommonModule } from "@angular/common";
 import { ComponentsModule } from "src/components";
 import { WidgetCanvas } from "./widgetCanvas.directive";
-import { WidgetPortal } from "./widgetPortal/widgetPortal.component";
+import { WidgetPortal } from "./widgetPortal/widgetPortal.component"
 import { MatCardModule } from "@angular/material/card";
 import { DragDropModule } from "@angular/cdk/drag-drop";
 import { MatButtonModule } from "@angular/material/button";
 import { PortalModule } from "@angular/cdk/portal";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { WidgetStateIconPipe } from "./widgetStateIcon.pipe";
 
 @NgModule({
   imports:[
     MatCardModule,
     DragDropModule,
     MatButtonModule,
+    MatTooltipModule,
     PortalModule,
     CommonModule,
     ComponentsModule,
@@ -20,6 +23,7 @@ import { PortalModule } from "@angular/cdk/portal";
   declarations: [
     WidgetCanvas,
     WidgetPortal,
+    WidgetStateIconPipe,
   ],
   providers: [],
   exports: [
diff --git a/src/widget/widgetPortal/widgetPortal.component.ts b/src/widget/widgetPortal/widgetPortal.component.ts
index a1351e15e370318c5e9bf363c4c36f2f0b8d37e9..cf7f0ea9a67e49c27812e85bef586397e84e2905 100644
--- a/src/widget/widgetPortal/widgetPortal.component.ts
+++ b/src/widget/widgetPortal/widgetPortal.component.ts
@@ -1,6 +1,26 @@
 import { ComponentPortal } from "@angular/cdk/portal";
-import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from "@angular/core";
-import { WidgetService } from "../service";
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Inject, Optional } from "@angular/core";
+import { RM_WIDGET, EnumWidgetState } from "../constants";
+
+type TWidgetCss = {
+  transform: string
+  width: string
+}
+
+const widgetStateToTransform: Record<EnumWidgetState, TWidgetCss> = {
+  [EnumWidgetState.MINIMIZED]: {
+    transform: `translate(100vw, 5vh)`,
+    width: '24rem'
+  },
+  [EnumWidgetState.NORMAL]: {
+    transform: `translate(calc(100vw - 24rem), 5vh)`,
+    width: '24rem'
+  },
+  [EnumWidgetState.MAXIMIZED]: {
+    transform: `translate(5vw, 5vh)`,
+    width: '90vw'
+  },
+}
 
 @Component({
   selector: 'sxplr-widget-portal',
@@ -13,6 +33,8 @@ import { WidgetService } from "../service";
 
 export class WidgetPortal<T>{
 
+  EnumWidgetState = EnumWidgetState
+
   portal: ComponentPortal<T>
   
   private _name: string
@@ -24,18 +46,38 @@ export class WidgetPortal<T>{
     this.cdr.markForCheck()
   }
 
-  defaultPosition = {
-    x: 200,
-    y: 200,
+  minimizeReturnState: EnumWidgetState.NORMAL | EnumWidgetState.MAXIMIZED
+
+  private _state: EnumWidgetState = EnumWidgetState.NORMAL
+  get state() {
+    return this._state
   }
+  set state(val: EnumWidgetState) {
+    if (val === EnumWidgetState.MINIMIZED) {
+      this.minimizeReturnState = this._state !== EnumWidgetState.MINIMIZED
+        ? this._state
+        : EnumWidgetState.NORMAL
+    }
+    this._state = val
+    this.transform = widgetStateToTransform[this._state]?.transform || widgetStateToTransform[EnumWidgetState.NORMAL].transform
+    this.width = widgetStateToTransform[this._state]?.width || widgetStateToTransform[EnumWidgetState.NORMAL].width
+
+    this.cdr.markForCheck()
+  }
+
+  @HostBinding('style.transform')
+  transform = widgetStateToTransform[ EnumWidgetState.NORMAL ].transform
+
+  @HostBinding('style.width')
+  width = widgetStateToTransform[ EnumWidgetState.NORMAL ].width
 
   constructor(
-    private wSvc: WidgetService,
     private cdr: ChangeDetectorRef,
+    @Optional() @Inject(RM_WIDGET) private rmWidget: (inst: unknown) => void
   ){
     
   }
   exit(){
-    this.wSvc.rmWidget(this)
+    if (this.rmWidget) this.rmWidget(this)
   }
 }
diff --git a/src/widget/widgetPortal/widgetPortal.style.css b/src/widget/widgetPortal/widgetPortal.style.css
index 12e9c809623d27f95eef3986c869ed9966496328..3b374eee2ca51e3338e64c35f48593e33f66623f 100644
--- a/src/widget/widgetPortal/widgetPortal.style.css
+++ b/src/widget/widgetPortal/widgetPortal.style.css
@@ -2,15 +2,18 @@
 {
   pointer-events: none;
   display: block;
-  max-width: 24rem;
+  
+  width: 24rem;
+  height: 90vh;
+
+  transition: all 160ms cubic-bezier(0.35, 0, 0.25, 1);
 }
 
 mat-card
 {
   pointer-events: all;
-  max-width: 36vw;
-  height: 36rem;
-  max-height: 90vh;
+  width: 100%;
+  height: 100%;
 }
 
 mat-card-content
@@ -24,7 +27,7 @@ mat-card-content
 .widget-portal-header
 {
   display: flex;
-  justify-content: space-between;
+  justify-content: flex-end;
   align-items: center;
 }
 
@@ -33,24 +36,13 @@ mat-card-content
   flex-grow: 1;
 }
 
-.hover-grab
-{
-  opacity: 0.5;
-  transition: opacity 200ms ease-in-out;
-  cursor: move;
-}
-
-.hover-grab:hover
-{
-  opacity: 1.0;
-}
-
-.widget-grab-handle
-{
-  margin-right:1rem;
-}
-
 .widget-name
 {
   flex-grow: 1;
 }
+
+.when-minimized-nub
+{
+  position: absolute;
+  transform: translate(-5rem, 5rem);
+}
\ No newline at end of file
diff --git a/src/widget/widgetPortal/widgetPortal.template.html b/src/widget/widgetPortal/widgetPortal.template.html
index 7aff890d9ecab44127f5b70a0df3a5ba0f5c305c..5e4e495fa8894b2b9f8e81e0ddb67b5120f4c081 100644
--- a/src/widget/widgetPortal/widgetPortal.template.html
+++ b/src/widget/widgetPortal/widgetPortal.template.html
@@ -1,13 +1,41 @@
-<mat-card cdkDrag [cdkDragFreeDragPosition]="defaultPosition">
+<div *ngIf="state === EnumWidgetState.MINIMIZED"
+  class="when-minimized-nub">
+
+  <button mat-mini-fab
+    [matTooltip]="name"
+    color="primary"
+    class="sxplr-pe-all"
+    (click)="state = minimizeReturnState">
+    <i [class]="minimizeReturnState | widgetStateIcon"></i>
+  </button>
+</div>
+
+<mat-card>
   <mat-card-content>
-    <div class="widget-portal-header" cdkDragHandle>
-      <span class="hover-grab widget-grab-handle">
-        <i class="fas fa-grip-vertical"></i>
-      </span>
+    <div class="widget-portal-header">
 
       <span *ngIf="name" class="widget-name">
         {{ name }}
       </span>
+      
+      <!-- state changer -->
+      <ng-template [ngTemplateOutlet]="stateBtnTmpl"
+        [ngTemplateOutletContext]="{
+          $implicit: EnumWidgetState.MINIMIZED
+        }">
+      </ng-template>
+
+      <ng-template [ngTemplateOutlet]="stateBtnTmpl"
+        [ngTemplateOutletContext]="{
+          $implicit: EnumWidgetState.NORMAL
+        }">
+      </ng-template>
+
+      <ng-template [ngTemplateOutlet]="stateBtnTmpl"
+        [ngTemplateOutletContext]="{
+          $implicit: EnumWidgetState.MAXIMIZED
+        }">
+      </ng-template>
 
       <button mat-icon-button (click)="exit()">
         <i class="fas fa-times"></i>
@@ -20,3 +48,13 @@
     </div>
   </mat-card-content>
 </mat-card>
+
+<!-- template for plugin state -->
+<ng-template #stateBtnTmpl let-btnstate>
+  <button
+    *ngIf="state !== btnstate"
+    (click)="state = btnstate"
+    mat-icon-button>
+    <i [class]="btnstate | widgetStateIcon"></i>
+  </button>
+</ng-template>
diff --git a/src/widget/widgetStateIcon.pipe.ts b/src/widget/widgetStateIcon.pipe.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b7af88175fe3d66886e68610a296db89c7e79fc6
--- /dev/null
+++ b/src/widget/widgetStateIcon.pipe.ts
@@ -0,0 +1,26 @@
+import { Pipe, PipeTransform } from "@angular/core";
+import { EnumWidgetState } from "./constants"
+
+@Pipe({
+  name: 'widgetStateIcon',
+  pure: true
+})
+
+export class WidgetStateIconPipe implements PipeTransform{
+  public transform(state: EnumWidgetState): string {
+    switch (state) {
+    case EnumWidgetState.MINIMIZED: {
+      return 'fas fa-window-minimize'
+    }
+    case EnumWidgetState.NORMAL: {
+      return 'fas fa-window-restore'
+    }
+    case EnumWidgetState.MAXIMIZED: {
+      return 'fas fa-window-maximize'
+    }
+    default: {
+      return 'fas fa-window-restore'
+    }
+    }
+  }
+}
diff --git a/third_party/leap-0.6.4.js b/third_party/leap-0.6.4.js
new file mode 100644
index 0000000000000000000000000000000000000000..575679f9535222b58114e1617cdd714953e2230a
--- /dev/null
+++ b/third_party/leap-0.6.4.js
@@ -0,0 +1,9420 @@
+/*!                                                              
+ * LeapJS v0.6.4                                                  
+ * http://github.com/leapmotion/leapjs/                                        
+ *                                                                             
+ * Copyright 2013 LeapMotion, Inc. and other contributors                      
+ * Released under the Apache-2.0 license                                     
+ * http://github.com/leapmotion/leapjs/blob/master/LICENSE.txt                 
+ */
+;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
+var Pointable = require('./pointable'),
+  glMatrix = require("gl-matrix")
+  , vec3 = glMatrix.vec3
+  , mat3 = glMatrix.mat3
+  , mat4 = glMatrix.mat4
+  , _ = require('underscore');
+
+
+var Bone = module.exports = function(finger, data) {
+  this.finger = finger;
+
+  this._center = null, this._matrix = null;
+
+  /**
+  * An integer code for the name of this bone.
+  *
+  * * 0 -- metacarpal
+  * * 1 -- proximal
+  * * 2 -- medial
+  * * 3 -- distal
+  * * 4 -- arm
+  *
+  * @member type
+  * @type {number}
+  * @memberof Leap.Bone.prototype
+  */
+  this.type = data.type;
+
+  /**
+   * The position of the previous, or base joint of the bone closer to the wrist.
+   * @type {vector3}
+   */
+  this.prevJoint = data.prevJoint;
+
+  /**
+   * The position of the next joint, or the end of the bone closer to the finger tip.
+   * @type {vector3}
+   */
+  this.nextJoint = data.nextJoint;
+
+  /**
+   * The estimated width of the tool in millimeters.
+   *
+   * The reported width is the average width of the visible portion of the
+   * tool from the hand to the tip. If the width isn't known,
+   * then a value of 0 is returned.
+   *
+   * Pointable objects representing fingers do not have a width property.
+   *
+   * @member width
+   * @type {number}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.width = data.width;
+
+  var displacement = new Array(3);
+  vec3.sub(displacement, data.nextJoint, data.prevJoint);
+
+  this.length = vec3.length(displacement);
+
+
+  /**
+   *
+   * These fully-specify the orientation of the bone.
+   * See examples/threejs-bones.html for more info
+   * Three vec3s:
+   *  x (red): The rotation axis of the finger, pointing outwards.  (In general, away from the thumb )
+   *  y (green): The "up" vector, orienting the top of the finger
+   *  z (blue): The roll axis of the bone.
+   *
+   *  Most up vectors will be pointing the same direction, except for the thumb, which is more rightwards.
+   *
+   *  The thumb has one fewer bones than the fingers, but there are the same number of joints & joint-bases provided
+   *  the first two appear in the same position, but only the second (proximal) rotates.
+   *
+   *  Normalized.
+   */
+  this.basis = data.basis;
+};
+
+Bone.prototype.left = function(){
+
+  if (this._left) return this._left;
+
+  this._left =  mat3.determinant(this.basis[0].concat(this.basis[1]).concat(this.basis[2])) < 0;
+
+  return this._left;
+
+};
+
+
+/**
+ * The Affine transformation matrix describing the orientation of the bone, in global Leap-space.
+ * It contains a 3x3 rotation matrix (in the "top left"), and center coordinates in the fourth column.
+ *
+ * Unlike the basis, the right and left hands have the same coordinate system.
+ *
+ */
+Bone.prototype.matrix = function(){
+
+  if (this._matrix) return this._matrix;
+
+  var b = this.basis,
+      t = this._matrix = mat4.create();
+
+  // open transform mat4 from rotation mat3
+  t[0] = b[0][0], t[1] = b[0][1], t[2]  = b[0][2];
+  t[4] = b[1][0], t[5] = b[1][1], t[6]  = b[1][2];
+  t[8] = b[2][0], t[9] = b[2][1], t[10] = b[2][2];
+
+  t[3] = this.center()[0];
+  t[7] = this.center()[1];
+  t[11] = this.center()[2];
+
+  if ( this.left() ) {
+    // flip the basis to be right-handed
+    t[0] *= -1;
+    t[1] *= -1;
+    t[2] *= -1;
+  }
+
+  return this._matrix;
+};
+
+/**
+ * Helper method to linearly interpolate between the two ends of the bone.
+ *
+ * when t = 0, the position of prevJoint will be returned
+ * when t = 1, the position of nextJoint will be returned
+ */
+Bone.prototype.lerp = function(out, t){
+
+  vec3.lerp(out, this.prevJoint, this.nextJoint, t);
+
+};
+
+/**
+ *
+ * The center position of the bone
+ * Returns a vec3 array.
+ *
+ */
+Bone.prototype.center = function(){
+
+  if (this._center) return this._center;
+
+  var center = vec3.create();
+  this.lerp(center, 0.5);
+  this._center = center;
+  return center;
+
+};
+
+// The negative of the z-basis
+Bone.prototype.direction = function(){
+
+ return [
+   this.basis[2][0] * -1,
+   this.basis[2][1] * -1,
+   this.basis[2][2] * -1
+ ];
+
+};
+
+},{"./pointable":14,"gl-matrix":23,"underscore":24}],2:[function(require,module,exports){
+var CircularBuffer = module.exports = function(size) {
+  this.pos = 0;
+  this._buf = [];
+  this.size = size;
+}
+
+CircularBuffer.prototype.get = function(i) {
+  if (i == undefined) i = 0;
+  if (i >= this.size) return undefined;
+  if (i >= this._buf.length) return undefined;
+  return this._buf[(this.pos - i - 1) % this.size];
+}
+
+CircularBuffer.prototype.push = function(o) {
+  this._buf[this.pos % this.size] = o;
+  return this.pos++;
+}
+
+},{}],3:[function(require,module,exports){
+var chooseProtocol = require('../protocol').chooseProtocol
+  , EventEmitter = require('events').EventEmitter
+  , _ = require('underscore');
+
+var BaseConnection = module.exports = function(opts) {
+  this.opts = _.defaults(opts || {}, {
+    host : '127.0.0.1',
+    enableGestures: false,
+    scheme: this.getScheme(),
+    port: this.getPort(),
+    background: false,
+    optimizeHMD: false,
+    requestProtocolVersion: BaseConnection.defaultProtocolVersion
+  });
+  this.host = this.opts.host;
+  this.port = this.opts.port;
+  this.scheme = this.opts.scheme;
+  this.protocolVersionVerified = false;
+  this.background = null;
+  this.optimizeHMD = null;
+  this.on('ready', function() {
+    this.enableGestures(this.opts.enableGestures);
+    this.setBackground(this.opts.background);
+    this.setOptimizeHMD(this.opts.optimizeHMD);
+
+    if (this.opts.optimizeHMD){
+      console.log("Optimized for head mounted display usage.");
+    }else {
+      console.log("Optimized for desktop usage.");
+    }
+
+  });
+};
+
+// The latest available:
+BaseConnection.defaultProtocolVersion = 6;
+
+BaseConnection.prototype.getUrl = function() {
+  return this.scheme + "//" + this.host + ":" + this.port + "/v" + this.opts.requestProtocolVersion + ".json";
+}
+
+
+BaseConnection.prototype.getScheme = function(){
+  return 'ws:'
+}
+
+BaseConnection.prototype.getPort = function(){
+  return 6437
+}
+
+
+BaseConnection.prototype.setBackground = function(state) {
+  this.opts.background = state;
+  if (this.protocol && this.protocol.sendBackground && this.background !== this.opts.background) {
+    this.background = this.opts.background;
+    this.protocol.sendBackground(this, this.opts.background);
+  }
+}
+
+BaseConnection.prototype.setOptimizeHMD = function(state) {
+  this.opts.optimizeHMD = state;
+  if (this.protocol && this.protocol.sendOptimizeHMD && this.optimizeHMD !== this.opts.optimizeHMD) {
+    this.optimizeHMD = this.opts.optimizeHMD;
+    this.protocol.sendOptimizeHMD(this, this.opts.optimizeHMD);
+  }
+}
+
+BaseConnection.prototype.handleOpen = function() {
+  if (!this.connected) {
+    this.connected = true;
+    this.emit('connect');
+  }
+}
+
+BaseConnection.prototype.enableGestures = function(enabled) {
+  this.gesturesEnabled = enabled ? true : false;
+  this.send(this.protocol.encode({"enableGestures": this.gesturesEnabled}));
+}
+
+BaseConnection.prototype.handleClose = function(code, reason) {
+  if (!this.connected) return;
+  this.disconnect();
+
+  // 1001 - an active connection is closed
+  // 1006 - cannot connect
+  if (code === 1001 && this.opts.requestProtocolVersion > 1) {
+    if (this.protocolVersionVerified) {
+      this.protocolVersionVerified = false;
+    }else{
+      this.opts.requestProtocolVersion--;
+    }
+  }
+  this.startReconnection();
+}
+
+BaseConnection.prototype.startReconnection = function() {
+  var connection = this;
+  if(!this.reconnectionTimer){
+    (this.reconnectionTimer = setInterval(function() { connection.reconnect() }, 500));
+  }
+}
+
+BaseConnection.prototype.stopReconnection = function() {
+  this.reconnectionTimer = clearInterval(this.reconnectionTimer);
+}
+
+// By default, disconnect will prevent auto-reconnection.
+// Pass in true to allow the reconnection loop not be interrupted continue
+BaseConnection.prototype.disconnect = function(allowReconnect) {
+  if (!allowReconnect) this.stopReconnection();
+  if (!this.socket) return;
+  this.socket.close();
+  delete this.socket;
+  delete this.protocol;
+  delete this.background; // This is not persisted when reconnecting to the web socket server
+  delete this.optimizeHMD;
+  delete this.focusedState;
+  if (this.connected) {
+    this.connected = false;
+    this.emit('disconnect');
+  }
+  return true;
+}
+
+BaseConnection.prototype.reconnect = function() {
+  if (this.connected) {
+    this.stopReconnection();
+  } else {
+    this.disconnect(true);
+    this.connect();
+  }
+}
+
+BaseConnection.prototype.handleData = function(data) {
+  var message = JSON.parse(data);
+
+  var messageEvent;
+  if (this.protocol === undefined) {
+    messageEvent = this.protocol = chooseProtocol(message);
+    this.protocolVersionVerified = true;
+    this.emit('ready');
+  } else {
+    messageEvent = this.protocol(message);
+  }
+  this.emit(messageEvent.type, messageEvent);
+}
+
+BaseConnection.prototype.connect = function() {
+  if (this.socket) return;
+  this.socket = this.setupSocket();
+  return true;
+}
+
+BaseConnection.prototype.send = function(data) {
+  this.socket.send(data);
+}
+
+BaseConnection.prototype.reportFocus = function(state) {
+  if (!this.connected || this.focusedState === state) return;
+  this.focusedState = state;
+  this.emit(this.focusedState ? 'focus' : 'blur');
+  if (this.protocol && this.protocol.sendFocused) {
+    this.protocol.sendFocused(this, this.focusedState);
+  }
+}
+
+_.extend(BaseConnection.prototype, EventEmitter.prototype);
+},{"../protocol":15,"events":21,"underscore":24}],4:[function(require,module,exports){
+var BaseConnection = module.exports = require('./base')
+  , _ = require('underscore');
+
+
+var BrowserConnection = module.exports = function(opts) {
+  BaseConnection.call(this, opts);
+  var connection = this;
+  this.on('ready', function() { connection.startFocusLoop(); })
+  this.on('disconnect', function() { connection.stopFocusLoop(); })
+}
+
+_.extend(BrowserConnection.prototype, BaseConnection.prototype);
+
+BrowserConnection.__proto__ = BaseConnection;
+
+BrowserConnection.prototype.useSecure = function(){
+  return location.protocol === 'https:'
+}
+
+BrowserConnection.prototype.getScheme = function(){
+  return this.useSecure() ? 'wss:' : 'ws:'
+}
+
+BrowserConnection.prototype.getPort = function(){
+  return this.useSecure() ? 6436 : 6437
+}
+
+BrowserConnection.prototype.setupSocket = function() {
+  var connection = this;
+  var socket = new WebSocket(this.getUrl());
+  socket.onopen = function() { connection.handleOpen(); };
+  socket.onclose = function(data) { connection.handleClose(data['code'], data['reason']); };
+  socket.onmessage = function(message) { connection.handleData(message.data) };
+  socket.onerror = function(error) {
+
+    // attempt to degrade to ws: after one failed attempt for older Leap Service installations.
+    if (connection.useSecure() && connection.scheme === 'wss:'){
+      connection.scheme = 'ws:';
+      connection.port = 6437;
+      connection.disconnect();
+      connection.connect();
+    }
+
+  };
+  return socket;
+}
+
+BrowserConnection.prototype.startFocusLoop = function() {
+  if (this.focusDetectorTimer) return;
+  var connection = this;
+  var propertyName = null;
+  if (typeof document.hidden !== "undefined") {
+    propertyName = "hidden";
+  } else if (typeof document.mozHidden !== "undefined") {
+    propertyName = "mozHidden";
+  } else if (typeof document.msHidden !== "undefined") {
+    propertyName = "msHidden";
+  } else if (typeof document.webkitHidden !== "undefined") {
+    propertyName = "webkitHidden";
+  } else {
+    propertyName = undefined;
+  }
+
+  if (connection.windowVisible === undefined) {
+    connection.windowVisible = propertyName === undefined ? true : document[propertyName] === false;
+  }
+
+  var focusListener = window.addEventListener('focus', function(e) {
+    connection.windowVisible = true;
+    updateFocusState();
+  });
+
+  var blurListener = window.addEventListener('blur', function(e) {
+    connection.windowVisible = false;
+    updateFocusState();
+  });
+
+  this.on('disconnect', function() {
+    window.removeEventListener('focus', focusListener);
+    window.removeEventListener('blur', blurListener);
+  });
+
+  var updateFocusState = function() {
+    var isVisible = propertyName === undefined ? true : document[propertyName] === false;
+    connection.reportFocus(isVisible && connection.windowVisible);
+  }
+
+  // save 100ms when resuming focus
+  updateFocusState();
+
+  this.focusDetectorTimer = setInterval(updateFocusState, 100);
+}
+
+BrowserConnection.prototype.stopFocusLoop = function() {
+  if (!this.focusDetectorTimer) return;
+  clearTimeout(this.focusDetectorTimer);
+  delete this.focusDetectorTimer;
+}
+
+},{"./base":3,"underscore":24}],5:[function(require,module,exports){
+var process=require("__browserify_process");var Frame = require('./frame')
+  , Hand = require('./hand')
+  , Pointable = require('./pointable')
+  , Finger = require('./finger')
+  , CircularBuffer = require("./circular_buffer")
+  , Pipeline = require("./pipeline")
+  , EventEmitter = require('events').EventEmitter
+  , gestureListener = require('./gesture').gestureListener
+  , Dialog = require('./dialog')
+  , _ = require('underscore');
+
+/**
+ * Constructs a Controller object.
+ *
+ * When creating a Controller object, you may optionally pass in options
+ * to set the host , set the port, enable gestures, or select the frame event type.
+ *
+ * ```javascript
+ * var controller = new Leap.Controller({
+ *   host: '127.0.0.1',
+ *   port: 6437,
+ *   enableGestures: true,
+ *   frameEventName: 'animationFrame'
+ * });
+ * ```
+ *
+ * @class Controller
+ * @memberof Leap
+ * @classdesc
+ * The Controller class is your main interface to the Leap Motion Controller.
+ *
+ * Create an instance of this Controller class to access frames of tracking data
+ * and configuration information. Frame data can be polled at any time using the
+ * [Controller.frame]{@link Leap.Controller#frame}() function. Call frame() or frame(0) to get the most recent
+ * frame. Set the history parameter to a positive integer to access previous frames.
+ * A controller stores up to 60 frames in its frame history.
+ *
+ * Polling is an appropriate strategy for applications which already have an
+ * intrinsic update loop, such as a game.
+ *
+ * loopWhileDisconnected defaults to true, and maintains a 60FPS frame rate even when Leap Motion is not streaming
+ * data at that rate (such as no hands in frame).  This is important for VR/WebGL apps which rely on rendering for
+ * regular visual updates, including from other input devices.  Flipping this to false should be considered an
+ * optimization for very specific use-cases.
+ *
+ *
+ */
+
+
+var Controller = module.exports = function(opts) {
+  var inNode = (typeof(process) !== 'undefined' && process.versions && process.versions.node),
+    controller = this;
+
+  opts = _.defaults(opts || {}, {
+    inNode: inNode
+  });
+
+  this.inNode = opts.inNode;
+
+  opts = _.defaults(opts || {}, {
+    frameEventName: this.useAnimationLoop() ? 'animationFrame' : 'deviceFrame',
+    suppressAnimationLoop: !this.useAnimationLoop(),
+    loopWhileDisconnected: true,
+    useAllPlugins: false,
+    checkVersion: true
+  });
+
+  this.animationFrameRequested = false;
+  this.onAnimationFrame = function(timestamp) {
+    if (controller.lastConnectionFrame.valid){
+      controller.emit('animationFrame', controller.lastConnectionFrame);
+    }
+    controller.emit('frameEnd', timestamp);
+    if (
+      controller.loopWhileDisconnected &&
+      ( ( controller.connection.focusedState !== false )  // loop while undefined, pre-ready.
+        || controller.connection.opts.background) ){
+      window.requestAnimationFrame(controller.onAnimationFrame);
+    }else{
+      controller.animationFrameRequested = false;
+    }
+  };
+  this.suppressAnimationLoop = opts.suppressAnimationLoop;
+  this.loopWhileDisconnected = opts.loopWhileDisconnected;
+  this.frameEventName = opts.frameEventName;
+  this.useAllPlugins = opts.useAllPlugins;
+  this.history = new CircularBuffer(200);
+  this.lastFrame = Frame.Invalid;
+  this.lastValidFrame = Frame.Invalid;
+  this.lastConnectionFrame = Frame.Invalid;
+  this.accumulatedGestures = [];
+  this.checkVersion = opts.checkVersion;
+  if (opts.connectionType === undefined) {
+    this.connectionType = (this.inBrowser() ? require('./connection/browser') : require('./connection/node'));
+  } else {
+    this.connectionType = opts.connectionType;
+  }
+  this.connection = new this.connectionType(opts);
+  this.streamingCount = 0;
+  this.devices = {};
+  this.plugins = {};
+  this._pluginPipelineSteps = {};
+  this._pluginExtendedMethods = {};
+  if (opts.useAllPlugins) this.useRegisteredPlugins();
+  this.setupFrameEvents(opts);
+  this.setupConnectionEvents();
+  
+  this.startAnimationLoop(); // immediately when started
+}
+
+Controller.prototype.gesture = function(type, cb) {
+  var creator = gestureListener(this, type);
+  if (cb !== undefined) {
+    creator.stop(cb);
+  }
+  return creator;
+}
+
+/*
+ * @returns the controller
+ */
+Controller.prototype.setBackground = function(state) {
+  this.connection.setBackground(state);
+  return this;
+}
+
+Controller.prototype.setOptimizeHMD = function(state) {
+  this.connection.setOptimizeHMD(state);
+  return this;
+}
+
+Controller.prototype.inBrowser = function() {
+  return !this.inNode;
+}
+
+Controller.prototype.useAnimationLoop = function() {
+  return this.inBrowser() && !this.inBackgroundPage();
+}
+
+Controller.prototype.inBackgroundPage = function(){
+  // http://developer.chrome.com/extensions/extension#method-getBackgroundPage
+  return (typeof(chrome) !== "undefined") &&
+    chrome.extension &&
+    chrome.extension.getBackgroundPage &&
+    (chrome.extension.getBackgroundPage() === window)
+}
+
+/*
+ * @returns the controller
+ */
+Controller.prototype.connect = function() {
+  this.connection.connect();
+  return this;
+}
+
+Controller.prototype.streaming = function() {
+  return this.streamingCount > 0;
+}
+
+Controller.prototype.connected = function() {
+  return !!this.connection.connected;
+}
+
+Controller.prototype.startAnimationLoop = function(){
+  if (!this.suppressAnimationLoop && !this.animationFrameRequested) {
+    this.animationFrameRequested = true;
+    window.requestAnimationFrame(this.onAnimationFrame);
+  }
+}
+
+/*
+ * @returns the controller
+ */
+Controller.prototype.disconnect = function() {
+  this.connection.disconnect();
+  return this;
+}
+
+/**
+ * Returns a frame of tracking data from the Leap.
+ *
+ * Use the optional history parameter to specify which frame to retrieve.
+ * Call frame() or frame(0) to access the most recent frame; call frame(1) to
+ * access the previous frame, and so on. If you use a history value greater
+ * than the number of stored frames, then the controller returns an invalid frame.
+ *
+ * @method frame
+ * @memberof Leap.Controller.prototype
+ * @param {number} history The age of the frame to return, counting backwards from
+ * the most recent frame (0) into the past and up to the maximum age (59).
+ * @returns {Leap.Frame} The specified frame; or, if no history
+ * parameter is specified, the newest frame. If a frame is not available at
+ * the specified history position, an invalid Frame is returned.
+ **/
+Controller.prototype.frame = function(num) {
+  return this.history.get(num) || Frame.Invalid;
+}
+
+Controller.prototype.loop = function(callback) {
+  if (callback) {
+    if (typeof callback === 'function'){
+      this.on(this.frameEventName, callback);
+    }else{
+      // callback is actually of the form: {eventName: callback}
+      this.setupFrameEvents(callback);
+    }
+  }
+
+  return this.connect();
+}
+
+Controller.prototype.addStep = function(step) {
+  if (!this.pipeline) this.pipeline = new Pipeline(this);
+  this.pipeline.addStep(step);
+}
+
+// this is run on every deviceFrame
+Controller.prototype.processFrame = function(frame) {
+  if (frame.gestures) {
+    this.accumulatedGestures = this.accumulatedGestures.concat(frame.gestures);
+  }
+  // lastConnectionFrame is used by the animation loop
+  this.lastConnectionFrame = frame;
+  this.startAnimationLoop(); // Only has effect if loopWhileDisconnected: false
+  this.emit('deviceFrame', frame);
+}
+
+// on a this.deviceEventName (usually 'animationFrame' in browsers), this emits a 'frame'
+Controller.prototype.processFinishedFrame = function(frame) {
+  this.lastFrame = frame;
+  if (frame.valid) {
+    this.lastValidFrame = frame;
+  }
+  frame.controller = this;
+  frame.historyIdx = this.history.push(frame);
+  if (frame.gestures) {
+    frame.gestures = this.accumulatedGestures;
+    this.accumulatedGestures = [];
+    for (var gestureIdx = 0; gestureIdx != frame.gestures.length; gestureIdx++) {
+      this.emit("gesture", frame.gestures[gestureIdx], frame);
+    }
+  }
+  if (this.pipeline) {
+    frame = this.pipeline.run(frame);
+    if (!frame) frame = Frame.Invalid;
+  }
+  this.emit('frame', frame);
+  this.emitHandEvents(frame);
+}
+
+/**
+ * The controller will emit 'hand' events for every hand on each frame.  The hand in question will be passed
+ * to the event callback.
+ *
+ * @param frame
+ */
+Controller.prototype.emitHandEvents = function(frame){
+  for (var i = 0; i < frame.hands.length; i++){
+    this.emit('hand', frame.hands[i]);
+  }
+}
+
+Controller.prototype.setupFrameEvents = function(opts){
+  if (opts.frame){
+    this.on('frame', opts.frame);
+  }
+  if (opts.hand){
+    this.on('hand', opts.hand);
+  }
+}
+
+/**
+  Controller events.  The old 'deviceConnected' and 'deviceDisconnected' have been depricated -
+  use 'deviceStreaming' and 'deviceStopped' instead, except in the case of an unexpected disconnect.
+
+  There are 4 pairs of device events recently added/changed:
+  -deviceAttached/deviceRemoved - called when a device's physical connection to the computer changes
+  -deviceStreaming/deviceStopped - called when a device is paused or resumed.
+  -streamingStarted/streamingStopped - called when there is/is no longer at least 1 streaming device.
+									  Always comes after deviceStreaming.
+  
+  The first of all of the above event pairs is triggered as appropriate upon connection.  All of
+  these events receives an argument with the most recent info about the device that triggered it.
+  These events will always be fired in the order they are listed here, with reverse ordering for the
+  matching shutdown call. (ie, deviceStreaming always comes after deviceAttached, and deviceStopped 
+  will come before deviceRemoved).
+  
+  -deviceConnected/deviceDisconnected - These are considered deprecated and will be removed in
+  the next revision.  In contrast to the other events and in keeping with it's original behavior,
+  it will only be fired when a device begins streaming AFTER a connection has been established.
+  It is not paired, and receives no device info.  Nearly identical functionality to
+  streamingStarted/Stopped if you need to port.
+*/
+Controller.prototype.setupConnectionEvents = function() {
+  var controller = this;
+  this.connection.on('frame', function(frame) {
+    controller.processFrame(frame);
+  });
+  // either deviceFrame or animationFrame:
+  this.on(this.frameEventName, function(frame) {
+    controller.processFinishedFrame(frame);
+  });
+
+
+  // here we backfill the 0.5.0 deviceEvents as best possible
+  // backfill begin streaming events
+  var backfillStreamingStartedEventsHandler = function(){
+    if (controller.connection.opts.requestProtocolVersion < 5 && controller.streamingCount == 0){
+      controller.streamingCount = 1;
+      var info = {
+        attached: true,
+        streaming: true,
+        type: 'unknown',
+        id: "Lx00000000000"
+      };
+      controller.devices[info.id] = info;
+
+      controller.emit('deviceAttached', info);
+      controller.emit('deviceStreaming', info);
+      controller.emit('streamingStarted', info);
+      controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler)
+    }
+  }
+
+  var backfillStreamingStoppedEvents = function(){
+    if (controller.streamingCount > 0) {
+      for (var deviceId in controller.devices){
+        controller.emit('deviceStopped', controller.devices[deviceId]);
+        controller.emit('deviceRemoved', controller.devices[deviceId]);
+      }
+      // only emit streamingStopped once, with the last device
+      controller.emit('streamingStopped', controller.devices[deviceId]);
+
+      controller.streamingCount = 0;
+
+      for (var deviceId in controller.devices){
+        delete controller.devices[deviceId];
+      }
+    }
+  }
+  // Delegate connection events
+  this.connection.on('focus', function() {
+
+    if ( controller.loopWhileDisconnected ){
+
+      controller.startAnimationLoop();
+
+    }
+
+    controller.emit('focus');
+
+  });
+  this.connection.on('blur', function() { controller.emit('blur') });
+  this.connection.on('protocol', function(protocol) {
+
+    protocol.on('beforeFrameCreated', function(frameData){
+      controller.emit('beforeFrameCreated', frameData)
+    });
+
+    protocol.on('afterFrameCreated', function(frame, frameData){
+      controller.emit('afterFrameCreated', frame, frameData)
+    });
+
+    controller.emit('protocol', protocol); 
+  });
+
+  this.connection.on('ready', function() {
+
+    if (controller.checkVersion && !controller.inNode){
+      // show dialog only to web users
+      controller.checkOutOfDate();
+    }
+
+    controller.emit('ready');
+  });
+
+  this.connection.on('connect', function() {
+    controller.emit('connect');
+    controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler)
+    controller.connection.on('frame', backfillStreamingStartedEventsHandler);
+  });
+
+  this.connection.on('disconnect', function() {
+    controller.emit('disconnect');
+    backfillStreamingStoppedEvents();
+  });
+
+  // this does not fire when the controller is manually disconnected
+  // or for Leap Service v1.2.0+
+  this.connection.on('deviceConnect', function(evt) {
+    if (evt.state){
+      controller.emit('deviceConnected');
+      controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler)
+      controller.connection.on('frame', backfillStreamingStartedEventsHandler);
+    }else{
+      controller.emit('deviceDisconnected');
+      backfillStreamingStoppedEvents();
+    }
+  });
+
+  // Does not fire for Leap Service pre v1.2.0
+  this.connection.on('deviceEvent', function(evt) {
+    var info = evt.state,
+        oldInfo = controller.devices[info.id];
+
+    //Grab a list of changed properties in the device info
+    var changed = {};
+    for(var property in info) {
+      //If a property i doesn't exist the cache, or has changed...
+      if( !oldInfo || !oldInfo.hasOwnProperty(property) || oldInfo[property] != info[property] ) {
+        changed[property] = true;
+      }
+    }
+
+    //Update the device list
+    controller.devices[info.id] = info;
+
+    //Fire events based on change list
+    if(changed.attached) {
+      controller.emit(info.attached ? 'deviceAttached' : 'deviceRemoved', info);
+    }
+
+    if(!changed.streaming) return;
+
+    if(info.streaming) {
+      controller.streamingCount++;
+      controller.emit('deviceStreaming', info);
+      if( controller.streamingCount == 1 ) {
+        controller.emit('streamingStarted', info);
+      }
+      //if attached & streaming both change to true at the same time, that device was streaming
+      //already when we connected.
+      if(!changed.attached) {
+        controller.emit('deviceConnected');
+      }
+    }
+    //Since when devices are attached all fields have changed, don't send events for streaming being false.
+    else if(!(changed.attached && info.attached)) {
+      controller.streamingCount--;
+      controller.emit('deviceStopped', info);
+      if(controller.streamingCount == 0){
+        controller.emit('streamingStopped', info);
+      }
+      controller.emit('deviceDisconnected');
+    }
+
+  });
+
+
+  this.on('newListener', function(event, listener) {
+    if( event == 'deviceConnected' || event == 'deviceDisconnected' ) {
+      console.warn(event + " events are depricated.  Consider using 'streamingStarted/streamingStopped' or 'deviceStreaming/deviceStopped' instead");
+    }
+  });
+
+};
+
+
+
+
+// Checks if the protocol version is the latest, if if not, shows the dialog.
+Controller.prototype.checkOutOfDate = function(){
+  console.assert(this.connection && this.connection.protocol);
+
+  var serviceVersion = this.connection.protocol.serviceVersion;
+  var protocolVersion = this.connection.protocol.version;
+  var defaultProtocolVersion = this.connectionType.defaultProtocolVersion;
+
+  if (defaultProtocolVersion > protocolVersion){
+
+    console.warn("Your Protocol Version is v" + protocolVersion +
+        ", this app was designed for v" + defaultProtocolVersion);
+
+    Dialog.warnOutOfDate({
+      sV: serviceVersion,
+      pV: protocolVersion
+    });
+    return true
+  }else{
+    return false
+  }
+
+};
+
+
+
+Controller._pluginFactories = {};
+
+/*
+ * Registers a plugin, making is accessible to controller.use later on.
+ *
+ * @member plugin
+ * @memberof Leap.Controller.prototype
+ * @param {String} name The name of the plugin (usually camelCase).
+ * @param {function} factory A factory method which will return an instance of a plugin.
+ * The factory receives an optional hash of options, passed in via controller.use.
+ *
+ * Valid keys for the object include frame, hand, finger, tool, and pointable.  The value
+ * of each key can be either a function or an object.  If given a function, that function
+ * will be called once for every instance of the object, with that instance injected as an
+ * argument.  This allows decoration of objects with additional data:
+ *
+ * ```javascript
+ * Leap.Controller.plugin('testPlugin', function(options){
+ *   return {
+ *     frame: function(frame){
+ *       frame.foo = 'bar';
+ *     }
+ *   }
+ * });
+ * ```
+ *
+ * When hand is used, the callback is called for every hand in `frame.hands`.  Note that
+ * hand objects are recreated with every new frame, so that data saved on the hand will not
+ * persist.
+ *
+ * ```javascript
+ * Leap.Controller.plugin('testPlugin', function(){
+ *   return {
+ *     hand: function(hand){
+ *       console.log('testPlugin running on hand ' + hand.id);
+ *     }
+ *   }
+ * });
+ * ```
+ *
+ * A factory can return an object to add custom functionality to Frames, Hands, or Pointables.
+ * The methods are added directly to the object's prototype.  Finger and Tool cannot be used here, Pointable
+ * must be used instead.
+ * This is encouraged for calculations which may not be necessary on every frame.
+ * Memoization is also encouraged, for cases where the method may be called many times per frame by the application.
+ *
+ * ```javascript
+ * // This plugin allows hand.usefulData() to be called later.
+ * Leap.Controller.plugin('testPlugin', function(){
+ *   return {
+ *     hand: {
+ *       usefulData: function(){
+ *         console.log('usefulData on hand', this.id);
+ *         // memoize the results on to the hand, preventing repeat work:
+ *         this.x || this.x = someExpensiveCalculation();
+ *         return this.x;
+ *       }
+ *     }
+ *   }
+ * });
+ *
+ * Note that the factory pattern allows encapsulation for every plugin instance.
+ *
+ * ```javascript
+ * Leap.Controller.plugin('testPlugin', function(options){
+ *   options || options = {}
+ *   options.center || options.center = [0,0,0]
+ *
+ *   privatePrintingMethod = function(){
+ *     console.log('privatePrintingMethod - options', options);
+ *   }
+ *
+ *   return {
+ *     pointable: {
+ *       publicPrintingMethod: function(){
+ *         privatePrintingMethod();
+ *       }
+ *     }
+ *   }
+ * });
+ *
+ */
+Controller.plugin = function(pluginName, factory) {
+  if (this._pluginFactories[pluginName]) {
+    console.warn("Plugin \"" + pluginName + "\" already registered");
+  }
+  return this._pluginFactories[pluginName] = factory;
+};
+
+/*
+ * Returns a list of registered plugins.
+ * @returns {Array} Plugin Factories.
+ */
+Controller.plugins = function() {
+  return _.keys(this._pluginFactories);
+};
+
+
+
+var setPluginCallbacks = function(pluginName, type, callback){
+  
+  if ( ['beforeFrameCreated', 'afterFrameCreated'].indexOf(type) != -1 ){
+    
+      // todo - not able to "unuse" a plugin currently
+      this.on(type, callback);
+      
+    }else {
+      
+      if (!this.pipeline) this.pipeline = new Pipeline(this);
+    
+      if (!this._pluginPipelineSteps[pluginName]) this._pluginPipelineSteps[pluginName] = [];
+
+      this._pluginPipelineSteps[pluginName].push(
+        
+        this.pipeline.addWrappedStep(type, callback)
+        
+      );
+      
+    }
+  
+};
+
+var setPluginMethods = function(pluginName, type, hash){
+  var klass;
+  
+  if (!this._pluginExtendedMethods[pluginName]) this._pluginExtendedMethods[pluginName] = [];
+
+  switch (type) {
+    case 'frame':
+      klass = Frame;
+      break;
+    case 'hand':
+      klass = Hand;
+      break;
+    case 'pointable':
+      klass = Pointable;
+      _.extend(Finger.prototype, hash);
+      _.extend(Finger.Invalid,   hash);
+      break;
+    case 'finger':
+      klass = Finger;
+      break;
+    default:
+      throw pluginName + ' specifies invalid object type "' + type + '" for prototypical extension'
+  }
+
+  _.extend(klass.prototype, hash);
+  _.extend(klass.Invalid, hash);
+  this._pluginExtendedMethods[pluginName].push([klass, hash])
+  
+}
+
+
+
+/*
+ * Begin using a registered plugin.  The plugin's functionality will be added to all frames
+ * returned by the controller (and/or added to the objects within the frame).
+ *  - The order of plugin execution inside the loop will match the order in which use is called by the application.
+ *  - The plugin be run for both deviceFrames and animationFrames.
+ *
+ *  If called a second time, the options will be merged with those of the already instantiated plugin.
+ *
+ * @method use
+ * @memberOf Leap.Controller.prototype
+ * @param pluginName
+ * @param {Hash} Options to be passed to the plugin's factory.
+ * @returns the controller
+ */
+Controller.prototype.use = function(pluginName, options) {
+  var functionOrHash, pluginFactory, key, pluginInstance;
+
+  pluginFactory = (typeof pluginName == 'function') ? pluginName : Controller._pluginFactories[pluginName];
+
+  if (!pluginFactory) {
+    throw 'Leap Plugin ' + pluginName + ' not found.';
+  }
+
+  options || (options = {});
+
+  if (this.plugins[pluginName]){
+    _.extend(this.plugins[pluginName], options);
+    return this;
+  }
+
+  this.plugins[pluginName] = options;
+
+  pluginInstance = pluginFactory.call(this, options);
+
+  for (key in pluginInstance) {
+
+    functionOrHash = pluginInstance[key];
+
+    if (typeof functionOrHash === 'function') {
+      
+      setPluginCallbacks.call(this, pluginName, key, functionOrHash);
+      
+    } else {
+      
+      setPluginMethods.call(this, pluginName, key, functionOrHash);
+      
+    }
+
+  }
+
+  return this;
+};
+
+
+
+
+/*
+ * Stop using a used plugin.  This will remove any of the plugin's pipeline methods (those called on every frame)
+ * and remove any methods which extend frame-object prototypes.
+ *
+ * @method stopUsing
+ * @memberOf Leap.Controller.prototype
+ * @param pluginName
+ * @returns the controller
+ */
+Controller.prototype.stopUsing = function (pluginName) {
+  var steps = this._pluginPipelineSteps[pluginName],
+      extMethodHashes = this._pluginExtendedMethods[pluginName],
+      i = 0, klass, extMethodHash;
+
+  if (!this.plugins[pluginName]) return;
+
+  if (steps) {
+    for (i = 0; i < steps.length; i++) {
+      this.pipeline.removeStep(steps[i]);
+    }
+  }
+
+  if (extMethodHashes){
+    for (i = 0; i < extMethodHashes.length; i++){
+      klass = extMethodHashes[i][0];
+      extMethodHash = extMethodHashes[i][1];
+      for (var methodName in extMethodHash) {
+        delete klass.prototype[methodName];
+        delete klass.Invalid[methodName];
+      }
+    }
+  }
+
+  delete this.plugins[pluginName];
+
+  return this;
+}
+
+Controller.prototype.useRegisteredPlugins = function(){
+  for (var plugin in Controller._pluginFactories){
+    this.use(plugin);
+  }
+}
+
+
+_.extend(Controller.prototype, EventEmitter.prototype);
+
+},{"./circular_buffer":2,"./connection/browser":4,"./connection/node":20,"./dialog":6,"./finger":7,"./frame":8,"./gesture":9,"./hand":10,"./pipeline":13,"./pointable":14,"__browserify_process":22,"events":21,"underscore":24}],6:[function(require,module,exports){
+var process=require("__browserify_process");var Dialog = module.exports = function(message, options){
+  this.options = (options || {});
+  this.message = message;
+
+  this.createElement();
+};
+
+Dialog.prototype.createElement = function(){
+  this.element = document.createElement('div');
+  this.element.className = "leapjs-dialog";
+  this.element.style.position = "fixed";
+  this.element.style.top = '8px';
+  this.element.style.left = 0;
+  this.element.style.right = 0;
+  this.element.style.textAlign = 'center';
+  this.element.style.zIndex = 1000;
+
+  var dialog  = document.createElement('div');
+  this.element.appendChild(dialog);
+  dialog.style.className = "leapjs-dialog";
+  dialog.style.display = "inline-block";
+  dialog.style.margin = "auto";
+  dialog.style.padding = "8px";
+  dialog.style.color = "#222";
+  dialog.style.background = "#eee";
+  dialog.style.borderRadius = "4px";
+  dialog.style.border = "1px solid #999";
+  dialog.style.textAlign = "left";
+  dialog.style.cursor = "pointer";
+  dialog.style.whiteSpace = "nowrap";
+  dialog.style.transition = "box-shadow 1s linear";
+  dialog.innerHTML = this.message;
+
+
+  if (this.options.onclick){
+    dialog.addEventListener('click', this.options.onclick);
+  }
+
+  if (this.options.onmouseover){
+    dialog.addEventListener('mouseover', this.options.onmouseover);
+  }
+
+  if (this.options.onmouseout){
+    dialog.addEventListener('mouseout', this.options.onmouseout);
+  }
+
+  if (this.options.onmousemove){
+    dialog.addEventListener('mousemove', this.options.onmousemove);
+  }
+};
+
+Dialog.prototype.show = function(){
+  document.body.appendChild(this.element);
+  return this;
+};
+
+Dialog.prototype.hide = function(){
+  document.body.removeChild(this.element);
+  return this;
+};
+
+
+
+
+// Shows a DOM dialog box with links to developer.leapmotion.com to upgrade
+// This will work whether or not the Leap is plugged in,
+// As long as it is called after a call to .connect() and the 'ready' event has fired.
+Dialog.warnOutOfDate = function(params){
+  params || (params = {});
+
+  var url = "http://developer.leapmotion.com?";
+
+  params.returnTo = window.location.href;
+
+  for (var key in params){
+    url += key + '=' + encodeURIComponent(params[key]) + '&';
+  }
+
+  var dialog,
+    onclick = function(event){
+
+       if (event.target.id != 'leapjs-decline-upgrade'){
+
+         var popup = window.open(url,
+           '_blank',
+           'height=800,width=1000,location=1,menubar=1,resizable=1,status=1,toolbar=1,scrollbars=1'
+         );
+
+         if (window.focus) {popup.focus()}
+
+       }
+
+       dialog.hide();
+
+       return true;
+    },
+
+
+    message = "This site requires Leap Motion Tracking V2." +
+      "<button id='leapjs-accept-upgrade'  style='color: #444; transition: box-shadow 100ms linear; cursor: pointer; vertical-align: baseline; margin-left: 16px;'>Upgrade</button>" +
+      "<button id='leapjs-decline-upgrade' style='color: #444; transition: box-shadow 100ms linear; cursor: pointer; vertical-align: baseline; margin-left: 8px; '>Not Now</button>";
+
+  dialog = new Dialog(message, {
+      onclick: onclick,
+      onmousemove: function(e){
+        if (e.target == document.getElementById('leapjs-decline-upgrade')){
+          document.getElementById('leapjs-decline-upgrade').style.color = '#000';
+          document.getElementById('leapjs-decline-upgrade').style.boxShadow = '0px 0px 2px #5daa00';
+
+          document.getElementById('leapjs-accept-upgrade').style.color = '#444';
+          document.getElementById('leapjs-accept-upgrade').style.boxShadow = 'none';
+        }else{
+          document.getElementById('leapjs-accept-upgrade').style.color = '#000';
+          document.getElementById('leapjs-accept-upgrade').style.boxShadow = '0px 0px 2px #5daa00';
+
+          document.getElementById('leapjs-decline-upgrade').style.color = '#444';
+          document.getElementById('leapjs-decline-upgrade').style.boxShadow = 'none';
+        }
+      },
+      onmouseout: function(){
+        document.getElementById('leapjs-decline-upgrade').style.color = '#444';
+        document.getElementById('leapjs-decline-upgrade').style.boxShadow = 'none';
+        document.getElementById('leapjs-accept-upgrade').style.color = '#444';
+        document.getElementById('leapjs-accept-upgrade').style.boxShadow = 'none';
+      }
+    }
+  );
+
+  return dialog.show();
+};
+
+
+// Tracks whether we've warned for lack of bones API.  This will be shown only for early private-beta members.
+Dialog.hasWarnedBones = false;
+
+Dialog.warnBones = function(){
+  if (this.hasWarnedBones) return;
+  this.hasWarnedBones = true;
+
+  console.warn("Your Leap Service is out of date");
+
+  if ( !(typeof(process) !== 'undefined' && process.versions && process.versions.node) ){
+    this.warnOutOfDate({reason: 'bones'});
+  }
+
+}
+},{"__browserify_process":22}],7:[function(require,module,exports){
+var Pointable = require('./pointable'),
+  Bone = require('./bone')
+  , Dialog = require('./dialog')
+  , _ = require('underscore');
+
+/**
+* Constructs a Finger object.
+*
+* An uninitialized finger is considered invalid.
+* Get valid Finger objects from a Frame or a Hand object.
+*
+* @class Finger
+* @memberof Leap
+* @classdesc
+* The Finger class reports the physical characteristics of a finger.
+*
+* Both fingers and tools are classified as Pointable objects. Use the
+* Pointable.tool property to determine whether a Pointable object represents a
+* tool or finger. The Leap classifies a detected entity as a tool when it is
+* thinner, straighter, and longer than a typical finger.
+*
+* Note that Finger objects can be invalid, which means that they do not
+* contain valid tracking data and do not correspond to a physical entity.
+* Invalid Finger objects can be the result of asking for a Finger object
+* using an ID from an earlier frame when no Finger objects with that ID
+* exist in the current frame. A Finger object created from the Finger
+* constructor is also invalid. Test for validity with the Pointable.valid
+* property.
+*/
+var Finger = module.exports = function(data) {
+  Pointable.call(this, data); // use pointable as super-constructor
+  
+  /**
+  * The position of the distal interphalangeal joint of the finger.
+  * This joint is closest to the tip.
+  * 
+  * The distal interphalangeal joint is located between the most extreme segment
+  * of the finger (the distal phalanx) and the middle segment (the medial
+  * phalanx).
+  *
+  * @member dipPosition
+  * @type {number[]}
+  * @memberof Leap.Finger.prototype
+  */  
+  this.dipPosition = data.dipPosition;
+
+  /**
+  * The position of the proximal interphalangeal joint of the finger. This joint is the middle
+  * joint of a finger.
+  *
+  * The proximal interphalangeal joint is located between the two finger segments
+  * closest to the hand (the proximal and the medial phalanges). On a thumb,
+  * which lacks an medial phalanx, this joint index identifies the knuckle joint
+  * between the proximal phalanx and the metacarpal bone.
+  *
+  * @member pipPosition
+  * @type {number[]}
+  * @memberof Leap.Finger.prototype
+  */  
+  this.pipPosition = data.pipPosition;
+
+  /**
+  * The position of the metacarpopophalangeal joint, or knuckle, of the finger.
+  *
+  * The metacarpopophalangeal joint is located at the base of a finger between
+  * the metacarpal bone and the first phalanx. The common name for this joint is
+  * the knuckle.
+  *
+  * On a thumb, which has one less phalanx than a finger, this joint index
+  * identifies the thumb joint near the base of the hand, between the carpal
+  * and metacarpal bones.
+  *
+  * @member mcpPosition
+  * @type {number[]}
+  * @memberof Leap.Finger.prototype
+  */  
+  this.mcpPosition = data.mcpPosition;
+
+  /**
+   * The position of the Carpometacarpal joint
+   *
+   * This is at the distal end of the wrist, and has no common name.
+   *
+   */
+  this.carpPosition = data.carpPosition;
+
+  /**
+  * Whether or not this finger is in an extended posture.
+  *
+  * A finger is considered extended if it is extended straight from the hand as if
+  * pointing. A finger is not extended when it is bent down and curled towards the 
+  * palm.
+  * @member extended
+  * @type {Boolean}
+  * @memberof Leap.Finger.prototype
+  */
+  this.extended = data.extended;
+
+  /**
+  * An integer code for the name of this finger.
+  * 
+  * * 0 -- thumb
+  * * 1 -- index finger
+  * * 2 -- middle finger
+  * * 3 -- ring finger
+  * * 4 -- pinky
+  *
+  * @member type
+  * @type {number}
+  * @memberof Leap.Finger.prototype
+  */
+  this.type = data.type;
+
+  this.finger = true;
+  
+  /**
+  * The joint positions of this finger as an array in the order base to tip.
+  *
+  * @member positions
+  * @type {array[]}
+  * @memberof Leap.Finger.prototype
+  */
+  this.positions = [this.carpPosition, this.mcpPosition, this.pipPosition, this.dipPosition, this.tipPosition];
+
+  if (data.bases){
+    this.addBones(data);
+  } else {
+    Dialog.warnBones();
+  }
+
+};
+
+_.extend(Finger.prototype, Pointable.prototype);
+
+
+Finger.prototype.addBones = function(data){
+  /**
+  * Four bones per finger, from wrist outwards:
+  * metacarpal, proximal, medial, and distal.
+  *
+  * See http://en.wikipedia.org/wiki/Interphalangeal_articulations_of_hand
+  */
+  this.metacarpal   = new Bone(this, {
+    type: 0,
+    width: this.width,
+    prevJoint: this.carpPosition,
+    nextJoint: this.mcpPosition,
+    basis: data.bases[0]
+  });
+
+  this.proximal     = new Bone(this, {
+    type: 1,
+    width: this.width,
+    prevJoint: this.mcpPosition,
+    nextJoint: this.pipPosition,
+    basis: data.bases[1]
+  });
+
+  this.medial = new Bone(this, {
+    type: 2,
+    width: this.width,
+    prevJoint: this.pipPosition,
+    nextJoint: this.dipPosition,
+    basis: data.bases[2]
+  });
+
+  /**
+   * Note that the `distal.nextJoint` position is slightly different from the `finger.tipPosition`.
+   * The former is at the very end of the bone, where the latter is the center of a sphere positioned at
+   * the tip of the finger.  The btipPosition "bone tip position" is a few mm closer to the wrist than
+   * the tipPosition.
+   * @type {Bone}
+   */
+  this.distal       = new Bone(this, {
+    type: 3,
+    width: this.width,
+    prevJoint: this.dipPosition,
+    nextJoint: data.btipPosition,
+    basis: data.bases[3]
+  });
+
+  this.bones = [this.metacarpal, this.proximal, this.medial, this.distal];
+};
+
+Finger.prototype.toString = function() {
+    return "Finger [ id:" + this.id + " " + this.length + "mmx | width:" + this.width + "mm | direction:" + this.direction + ' ]';
+};
+
+Finger.Invalid = { valid: false };
+
+},{"./bone":1,"./dialog":6,"./pointable":14,"underscore":24}],8:[function(require,module,exports){
+var Hand = require("./hand")
+  , Pointable = require("./pointable")
+  , createGesture = require("./gesture").createGesture
+  , glMatrix = require("gl-matrix")
+  , mat3 = glMatrix.mat3
+  , vec3 = glMatrix.vec3
+  , InteractionBox = require("./interaction_box")
+  , Finger = require('./finger')
+  , _ = require("underscore");
+
+/**
+ * Constructs a Frame object.
+ *
+ * Frame instances created with this constructor are invalid.
+ * Get valid Frame objects by calling the
+ * [Controller.frame]{@link Leap.Controller#frame}() function.
+ *<C-D-Space>
+ * @class Frame
+ * @memberof Leap
+ * @classdesc
+ * The Frame class represents a set of hand and finger tracking data detected
+ * in a single frame.
+ *
+ * The Leap detects hands, fingers and tools within the tracking area, reporting
+ * their positions, orientations and motions in frames at the Leap frame rate.
+ *
+ * Access Frame objects using the [Controller.frame]{@link Leap.Controller#frame}() function.
+ */
+var Frame = module.exports = function(data) {
+  /**
+   * Reports whether this Frame instance is valid.
+   *
+   * A valid Frame is one generated by the Controller object that contains
+   * tracking data for all detected entities. An invalid Frame contains no
+   * actual tracking data, but you can call its functions without risk of a
+   * undefined object exception. The invalid Frame mechanism makes it more
+   * convenient to track individual data across the frame history. For example,
+   * you can invoke:
+   *
+   * ```javascript
+   * var finger = controller.frame(n).finger(fingerID);
+   * ```
+   *
+   * for an arbitrary Frame history value, "n", without first checking whether
+   * frame(n) returned a null object. (You should still check that the
+   * returned Finger instance is valid.)
+   *
+   * @member valid
+   * @memberof Leap.Frame.prototype
+   * @type {Boolean}
+   */
+  this.valid = true;
+  /**
+   * A unique ID for this Frame. Consecutive frames processed by the Leap
+   * have consecutive increasing values.
+   * @member id
+   * @memberof Leap.Frame.prototype
+   * @type {String}
+   */
+  this.id = data.id;
+  /**
+   * The frame capture time in microseconds elapsed since the Leap started.
+   * @member timestamp
+   * @memberof Leap.Frame.prototype
+   * @type {number}
+   */
+  this.timestamp = data.timestamp;
+  /**
+   * The list of Hand objects detected in this frame, given in arbitrary order.
+   * The list can be empty if no hands are detected.
+   *
+   * @member hands[]
+   * @memberof Leap.Frame.prototype
+   * @type {Leap.Hand}
+   */
+  this.hands = [];
+  this.handsMap = {};
+  /**
+   * The list of Pointable objects (fingers and tools) detected in this frame,
+   * given in arbitrary order. The list can be empty if no fingers or tools are
+   * detected.
+   *
+   * @member pointables[]
+   * @memberof Leap.Frame.prototype
+   * @type {Leap.Pointable}
+   */
+  this.pointables = [];
+  /**
+   * The list of Tool objects detected in this frame, given in arbitrary order.
+   * The list can be empty if no tools are detected.
+   *
+   * @member tools[]
+   * @memberof Leap.Frame.prototype
+   * @type {Leap.Pointable}
+   */
+  this.tools = [];
+  /**
+   * The list of Finger objects detected in this frame, given in arbitrary order.
+   * The list can be empty if no fingers are detected.
+   * @member fingers[]
+   * @memberof Leap.Frame.prototype
+   * @type {Leap.Pointable}
+   */
+  this.fingers = [];
+
+  /**
+   * The InteractionBox associated with the current frame.
+   *
+   * @member interactionBox
+   * @memberof Leap.Frame.prototype
+   * @type {Leap.InteractionBox}
+   */
+  if (data.interactionBox) {
+    this.interactionBox = new InteractionBox(data.interactionBox);
+  }
+  this.gestures = [];
+  this.pointablesMap = {};
+  this._translation = data.t;
+  this._rotation = _.flatten(data.r);
+  this._scaleFactor = data.s;
+  this.data = data;
+  this.type = 'frame'; // used by event emitting
+  this.currentFrameRate = data.currentFrameRate;
+
+  if (data.gestures) {
+   /**
+    * The list of Gesture objects detected in this frame, given in arbitrary order.
+    * The list can be empty if no gestures are detected.
+    *
+    * Circle and swipe gestures are updated every frame. Tap gestures
+    * only appear in the list for a single frame.
+    * @member gestures[]
+    * @memberof Leap.Frame.prototype
+    * @type {Leap.Gesture}
+    */
+    for (var gestureIdx = 0, gestureCount = data.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
+      this.gestures.push(createGesture(data.gestures[gestureIdx]));
+    }
+  }
+  this.postprocessData(data);
+};
+
+Frame.prototype.postprocessData = function(data){
+  if (!data) {
+    data = this.data;
+  }
+
+  for (var handIdx = 0, handCount = data.hands.length; handIdx != handCount; handIdx++) {
+    var hand = new Hand(data.hands[handIdx]);
+    hand.frame = this;
+    this.hands.push(hand);
+    this.handsMap[hand.id] = hand;
+  }
+
+  data.pointables = _.sortBy(data.pointables, function(pointable) { return pointable.id });
+
+  for (var pointableIdx = 0, pointableCount = data.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
+    var pointableData = data.pointables[pointableIdx];
+    var pointable = pointableData.dipPosition ? new Finger(pointableData) : new Pointable(pointableData);
+    pointable.frame = this;
+    this.addPointable(pointable);
+  }
+};
+
+/**
+ * Adds data from a pointable element into the pointablesMap; 
+ * also adds the pointable to the frame.handsMap hand to which it belongs,
+ * and to the hand's tools or hand's fingers map.
+ * 
+ * @param pointable {Object} a Pointable
+ */
+Frame.prototype.addPointable = function (pointable) {
+  this.pointables.push(pointable);
+  this.pointablesMap[pointable.id] = pointable;
+  (pointable.tool ? this.tools : this.fingers).push(pointable);
+  if (pointable.handId !== undefined && this.handsMap.hasOwnProperty(pointable.handId)) {
+    var hand = this.handsMap[pointable.handId];
+    hand.pointables.push(pointable);
+    (pointable.tool ? hand.tools : hand.fingers).push(pointable);
+    switch (pointable.type){
+      case 0:
+        hand.thumb = pointable;
+        break;
+      case 1:
+        hand.indexFinger = pointable;
+        break;
+      case 2:
+        hand.middleFinger = pointable;
+        break;
+      case 3:
+        hand.ringFinger = pointable;
+        break;
+      case 4:
+        hand.pinky = pointable;
+        break;
+    }
+  }
+};
+
+/**
+ * The tool with the specified ID in this frame.
+ *
+ * Use the Frame tool() function to retrieve a tool from
+ * this frame using an ID value obtained from a previous frame.
+ * This function always returns a Pointable object, but if no tool
+ * with the specified ID is present, an invalid Pointable object is returned.
+ *
+ * Note that ID values persist across frames, but only until tracking of a
+ * particular object is lost. If tracking of a tool is lost and subsequently
+ * regained, the new Pointable object representing that tool may have a
+ * different ID than that representing the tool in an earlier frame.
+ *
+ * @method tool
+ * @memberof Leap.Frame.prototype
+ * @param {String} id The ID value of a Tool object from a previous frame.
+ * @returns {Leap.Pointable} The tool with the
+ * matching ID if one exists in this frame; otherwise, an invalid Pointable object
+ * is returned.
+ */
+Frame.prototype.tool = function(id) {
+  var pointable = this.pointable(id);
+  return pointable.tool ? pointable : Pointable.Invalid;
+};
+
+/**
+ * The Pointable object with the specified ID in this frame.
+ *
+ * Use the Frame pointable() function to retrieve the Pointable object from
+ * this frame using an ID value obtained from a previous frame.
+ * This function always returns a Pointable object, but if no finger or tool
+ * with the specified ID is present, an invalid Pointable object is returned.
+ *
+ * Note that ID values persist across frames, but only until tracking of a
+ * particular object is lost. If tracking of a finger or tool is lost and subsequently
+ * regained, the new Pointable object representing that finger or tool may have
+ * a different ID than that representing the finger or tool in an earlier frame.
+ *
+ * @method pointable
+ * @memberof Leap.Frame.prototype
+ * @param {String} id The ID value of a Pointable object from a previous frame.
+ * @returns {Leap.Pointable} The Pointable object with
+ * the matching ID if one exists in this frame;
+ * otherwise, an invalid Pointable object is returned.
+ */
+Frame.prototype.pointable = function(id) {
+  return this.pointablesMap[id] || Pointable.Invalid;
+};
+
+/**
+ * The finger with the specified ID in this frame.
+ *
+ * Use the Frame finger() function to retrieve the finger from
+ * this frame using an ID value obtained from a previous frame.
+ * This function always returns a Finger object, but if no finger
+ * with the specified ID is present, an invalid Pointable object is returned.
+ *
+ * Note that ID values persist across frames, but only until tracking of a
+ * particular object is lost. If tracking of a finger is lost and subsequently
+ * regained, the new Pointable object representing that physical finger may have
+ * a different ID than that representing the finger in an earlier frame.
+ *
+ * @method finger
+ * @memberof Leap.Frame.prototype
+ * @param {String} id The ID value of a finger from a previous frame.
+ * @returns {Leap.Pointable} The finger with the
+ * matching ID if one exists in this frame; otherwise, an invalid Pointable
+ * object is returned.
+ */
+Frame.prototype.finger = function(id) {
+  var pointable = this.pointable(id);
+  return !pointable.tool ? pointable : Pointable.Invalid;
+};
+
+/**
+ * The Hand object with the specified ID in this frame.
+ *
+ * Use the Frame hand() function to retrieve the Hand object from
+ * this frame using an ID value obtained from a previous frame.
+ * This function always returns a Hand object, but if no hand
+ * with the specified ID is present, an invalid Hand object is returned.
+ *
+ * Note that ID values persist across frames, but only until tracking of a
+ * particular object is lost. If tracking of a hand is lost and subsequently
+ * regained, the new Hand object representing that physical hand may have
+ * a different ID than that representing the physical hand in an earlier frame.
+ *
+ * @method hand
+ * @memberof Leap.Frame.prototype
+ * @param {String} id The ID value of a Hand object from a previous frame.
+ * @returns {Leap.Hand} The Hand object with the matching
+ * ID if one exists in this frame; otherwise, an invalid Hand object is returned.
+ */
+Frame.prototype.hand = function(id) {
+  return this.handsMap[id] || Hand.Invalid;
+};
+
+/**
+ * The angle of rotation around the rotation axis derived from the overall
+ * rotational motion between the current frame and the specified frame.
+ *
+ * The returned angle is expressed in radians measured clockwise around
+ * the rotation axis (using the right-hand rule) between the start and end frames.
+ * The value is always between 0 and pi radians (0 and 180 degrees).
+ *
+ * The Leap derives frame rotation from the relative change in position and
+ * orientation of all objects detected in the field of view.
+ *
+ * If either this frame or sinceFrame is an invalid Frame object, then the
+ * angle of rotation is zero.
+ *
+ * @method rotationAngle
+ * @memberof Leap.Frame.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
+ * @param {number[]} [axis] The axis to measure rotation around.
+ * @returns {number} A positive value containing the heuristically determined
+ * rotational change between the current frame and that specified in the sinceFrame parameter.
+ */
+Frame.prototype.rotationAngle = function(sinceFrame, axis) {
+  if (!this.valid || !sinceFrame.valid) return 0.0;
+
+  var rot = this.rotationMatrix(sinceFrame);
+  var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5;
+  var angle = Math.acos(cs);
+  angle = isNaN(angle) ? 0.0 : angle;
+
+  if (axis !== undefined) {
+    var rotAxis = this.rotationAxis(sinceFrame);
+    angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis));
+  }
+
+  return angle;
+};
+
+/**
+ * The axis of rotation derived from the overall rotational motion between
+ * the current frame and the specified frame.
+ *
+ * The returned direction vector is normalized.
+ *
+ * The Leap derives frame rotation from the relative change in position and
+ * orientation of all objects detected in the field of view.
+ *
+ * If either this frame or sinceFrame is an invalid Frame object, or if no
+ * rotation is detected between the two frames, a zero vector is returned.
+ *
+ * @method rotationAxis
+ * @memberof Leap.Frame.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
+ * @returns {number[]} A normalized direction vector representing the axis of the heuristically determined
+ * rotational change between the current frame and that specified in the sinceFrame parameter.
+ */
+Frame.prototype.rotationAxis = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return vec3.create();
+  return vec3.normalize(vec3.create(), [
+    this._rotation[7] - sinceFrame._rotation[5],
+    this._rotation[2] - sinceFrame._rotation[6],
+    this._rotation[3] - sinceFrame._rotation[1]
+  ]);
+}
+
+/**
+ * The transform matrix expressing the rotation derived from the overall
+ * rotational motion between the current frame and the specified frame.
+ *
+ * The Leap derives frame rotation from the relative change in position and
+ * orientation of all objects detected in the field of view.
+ *
+ * If either this frame or sinceFrame is an invalid Frame object, then
+ * this method returns an identity matrix.
+ *
+ * @method rotationMatrix
+ * @memberof Leap.Frame.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
+ * @returns {number[]} A transformation matrix containing the heuristically determined
+ * rotational change between the current frame and that specified in the sinceFrame parameter.
+ */
+Frame.prototype.rotationMatrix = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return mat3.create();
+  var transpose = mat3.transpose(mat3.create(), this._rotation)
+  return mat3.multiply(mat3.create(), sinceFrame._rotation, transpose);
+}
+
+/**
+ * The scale factor derived from the overall motion between the current frame and the specified frame.
+ *
+ * The scale factor is always positive. A value of 1.0 indicates no scaling took place.
+ * Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion.
+ *
+ * The Leap derives scaling from the relative inward or outward motion of all
+ * objects detected in the field of view (independent of translation and rotation).
+ *
+ * If either this frame or sinceFrame is an invalid Frame object, then this method returns 1.0.
+ *
+ * @method scaleFactor
+ * @memberof Leap.Frame.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling.
+ * @returns {number} A positive value representing the heuristically determined
+ * scaling change ratio between the current frame and that specified in the sinceFrame parameter.
+ */
+Frame.prototype.scaleFactor = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return 1.0;
+  return Math.exp(this._scaleFactor - sinceFrame._scaleFactor);
+}
+
+/**
+ * The change of position derived from the overall linear motion between the
+ * current frame and the specified frame.
+ *
+ * The returned translation vector provides the magnitude and direction of the
+ * movement in millimeters.
+ *
+ * The Leap derives frame translation from the linear motion of all objects
+ * detected in the field of view.
+ *
+ * If either this frame or sinceFrame is an invalid Frame object, then this
+ * method returns a zero vector.
+ *
+ * @method translation
+ * @memberof Leap.Frame.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation.
+ * @returns {number[]} A vector representing the heuristically determined change in
+ * position of all objects between the current frame and that specified in the sinceFrame parameter.
+ */
+Frame.prototype.translation = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return vec3.create();
+  return vec3.subtract(vec3.create(), this._translation, sinceFrame._translation);
+}
+
+/**
+ * A string containing a brief, human readable description of the Frame object.
+ *
+ * @method toString
+ * @memberof Leap.Frame.prototype
+ * @returns {String} A brief description of this frame.
+ */
+Frame.prototype.toString = function() {
+  var str = "Frame [ id:"+this.id+" | timestamp:"+this.timestamp+" | Hand count:("+this.hands.length+") | Pointable count:("+this.pointables.length+")";
+  if (this.gestures) str += " | Gesture count:("+this.gestures.length+")";
+  str += " ]";
+  return str;
+}
+
+/**
+ * Returns a JSON-formatted string containing the hands, pointables and gestures
+ * in this frame.
+ *
+ * @method dump
+ * @memberof Leap.Frame.prototype
+ * @returns {String} A JSON-formatted string.
+ */
+Frame.prototype.dump = function() {
+  var out = '';
+  out += "Frame Info:<br/>";
+  out += this.toString();
+  out += "<br/><br/>Hands:<br/>"
+  for (var handIdx = 0, handCount = this.hands.length; handIdx != handCount; handIdx++) {
+    out += "  "+ this.hands[handIdx].toString() + "<br/>";
+  }
+  out += "<br/><br/>Pointables:<br/>";
+  for (var pointableIdx = 0, pointableCount = this.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
+      out += "  "+ this.pointables[pointableIdx].toString() + "<br/>";
+  }
+  if (this.gestures) {
+    out += "<br/><br/>Gestures:<br/>";
+    for (var gestureIdx = 0, gestureCount = this.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
+        out += "  "+ this.gestures[gestureIdx].toString() + "<br/>";
+    }
+  }
+  out += "<br/><br/>Raw JSON:<br/>";
+  out += JSON.stringify(this.data);
+  return out;
+}
+
+/**
+ * An invalid Frame object.
+ *
+ * You can use this invalid Frame in comparisons testing
+ * whether a given Frame instance is valid or invalid. (You can also check the
+ * [Frame.valid]{@link Leap.Frame#valid} property.)
+ *
+ * @static
+ * @type {Leap.Frame}
+ * @name Invalid
+ * @memberof Leap.Frame
+ */
+Frame.Invalid = {
+  valid: false,
+  hands: [],
+  fingers: [],
+  tools: [],
+  gestures: [],
+  pointables: [],
+  pointable: function() { return Pointable.Invalid },
+  finger: function() { return Pointable.Invalid },
+  hand: function() { return Hand.Invalid },
+  toString: function() { return "invalid frame" },
+  dump: function() { return this.toString() },
+  rotationAngle: function() { return 0.0; },
+  rotationMatrix: function() { return mat3.create(); },
+  rotationAxis: function() { return vec3.create(); },
+  scaleFactor: function() { return 1.0; },
+  translation: function() { return vec3.create(); }
+};
+
+},{"./finger":7,"./gesture":9,"./hand":10,"./interaction_box":12,"./pointable":14,"gl-matrix":23,"underscore":24}],9:[function(require,module,exports){
+var glMatrix = require("gl-matrix")
+  , vec3 = glMatrix.vec3
+  , EventEmitter = require('events').EventEmitter
+  , _ = require('underscore');
+
+/**
+ * Constructs a new Gesture object.
+ *
+ * An uninitialized Gesture object is considered invalid. Get valid instances
+ * of the Gesture class, which will be one of the Gesture subclasses, from a
+ * Frame object.
+ *
+ * @class Gesture
+ * @abstract
+ * @memberof Leap
+ * @classdesc
+ * The Gesture class represents a recognized movement by the user.
+ *
+ * The Leap watches the activity within its field of view for certain movement
+ * patterns typical of a user gesture or command. For example, a movement from side to
+ * side with the hand can indicate a swipe gesture, while a finger poking forward
+ * can indicate a screen tap gesture.
+ *
+ * When the Leap recognizes a gesture, it assigns an ID and adds a
+ * Gesture object to the frame gesture list. For continuous gestures, which
+ * occur over many frames, the Leap updates the gesture by adding
+ * a Gesture object having the same ID and updated properties in each
+ * subsequent frame.
+ *
+ * **Important:** Recognition for each type of gesture must be enabled;
+ * otherwise **no gestures are recognized or reported**.
+ *
+ * Subclasses of Gesture define the properties for the specific movement patterns
+ * recognized by the Leap.
+ *
+ * The Gesture subclasses for include:
+ *
+ * * CircleGesture -- A circular movement by a finger.
+ * * SwipeGesture -- A straight line movement by the hand with fingers extended.
+ * * ScreenTapGesture -- A forward tapping movement by a finger.
+ * * KeyTapGesture -- A downward tapping movement by a finger.
+ *
+ * Circle and swipe gestures are continuous and these objects can have a
+ * state of start, update, and stop.
+ *
+ * The screen tap gesture is a discrete gesture. The Leap only creates a single
+ * ScreenTapGesture object appears for each tap and it always has a stop state.
+ *
+ * Get valid Gesture instances from a Frame object. You can get a list of gestures
+ * from the Frame gestures array. You can also use the Frame gesture() method
+ * to find a gesture in the current frame using an ID value obtained in a
+ * previous frame.
+ *
+ * Gesture objects can be invalid. For example, when you get a gesture by ID
+ * using Frame.gesture(), and there is no gesture with that ID in the current
+ * frame, then gesture() returns an Invalid Gesture object (rather than a null
+ * value). Always check object validity in situations where a gesture might be
+ * invalid.
+ */
+var createGesture = exports.createGesture = function(data) {
+  var gesture;
+  switch (data.type) {
+    case 'circle':
+      gesture = new CircleGesture(data);
+      break;
+    case 'swipe':
+      gesture = new SwipeGesture(data);
+      break;
+    case 'screenTap':
+      gesture = new ScreenTapGesture(data);
+      break;
+    case 'keyTap':
+      gesture = new KeyTapGesture(data);
+      break;
+    default:
+      throw "unknown gesture type";
+  }
+
+ /**
+  * The gesture ID.
+  *
+  * All Gesture objects belonging to the same recognized movement share the
+  * same ID value. Use the ID value with the Frame::gesture() method to
+  * find updates related to this Gesture object in subsequent frames.
+  *
+  * @member id
+  * @memberof Leap.Gesture.prototype
+  * @type {number}
+  */
+  gesture.id = data.id;
+ /**
+  * The list of hands associated with this Gesture, if any.
+  *
+  * If no hands are related to this gesture, the list is empty.
+  *
+  * @member handIds
+  * @memberof Leap.Gesture.prototype
+  * @type {Array}
+  */
+  gesture.handIds = data.handIds.slice();
+ /**
+  * The list of fingers and tools associated with this Gesture, if any.
+  *
+  * If no Pointable objects are related to this gesture, the list is empty.
+  *
+  * @member pointableIds
+  * @memberof Leap.Gesture.prototype
+  * @type {Array}
+  */
+  gesture.pointableIds = data.pointableIds.slice();
+ /**
+  * The elapsed duration of the recognized movement up to the
+  * frame containing this Gesture object, in microseconds.
+  *
+  * The duration reported for the first Gesture in the sequence (with the
+  * start state) will typically be a small positive number since
+  * the movement must progress far enough for the Leap to recognize it as
+  * an intentional gesture.
+  *
+  * @member duration
+  * @memberof Leap.Gesture.prototype
+  * @type {number}
+  */
+  gesture.duration = data.duration;
+ /**
+  * The gesture ID.
+  *
+  * Recognized movements occur over time and have a beginning, a middle,
+  * and an end. The 'state()' attribute reports where in that sequence this
+  * Gesture object falls.
+  *
+  * Possible values for the state field are:
+  *
+  * * start
+  * * update
+  * * stop
+  *
+  * @member state
+  * @memberof Leap.Gesture.prototype
+  * @type {String}
+  */
+  gesture.state = data.state;
+ /**
+  * The gesture type.
+  *
+  * Possible values for the type field are:
+  *
+  * * circle
+  * * swipe
+  * * screenTap
+  * * keyTap
+  *
+  * @member type
+  * @memberof Leap.Gesture.prototype
+  * @type {String}
+  */
+  gesture.type = data.type;
+  return gesture;
+}
+
+/*
+ * Returns a builder object, which uses method chaining for gesture callback binding.
+ */
+var gestureListener = exports.gestureListener = function(controller, type) {
+  var handlers = {};
+  var gestureMap = {};
+
+  controller.on('gesture', function(gesture, frame) {
+    if (gesture.type == type) {
+      if (gesture.state == "start" || gesture.state == "stop") {
+        if (gestureMap[gesture.id] === undefined) {
+          var gestureTracker = new Gesture(gesture, frame);
+          gestureMap[gesture.id] = gestureTracker;
+          _.each(handlers, function(cb, name) {
+            gestureTracker.on(name, cb);
+          });
+        }
+      }
+      gestureMap[gesture.id].update(gesture, frame);
+      if (gesture.state == "stop") {
+        delete gestureMap[gesture.id];
+      }
+    }
+  });
+  var builder = {
+    start: function(cb) {
+      handlers['start'] = cb;
+      return builder;
+    },
+    stop: function(cb) {
+      handlers['stop'] = cb;
+      return builder;
+    },
+    complete: function(cb) {
+      handlers['stop'] = cb;
+      return builder;
+    },
+    update: function(cb) {
+      handlers['update'] = cb;
+      return builder;
+    }
+  }
+  return builder;
+}
+
+var Gesture = exports.Gesture = function(gesture, frame) {
+  this.gestures = [gesture];
+  this.frames = [frame];
+}
+
+Gesture.prototype.update = function(gesture, frame) {
+  this.lastGesture = gesture;
+  this.lastFrame = frame;
+  this.gestures.push(gesture);
+  this.frames.push(frame);
+  this.emit(gesture.state, this);
+}
+
+Gesture.prototype.translation = function() {
+  return vec3.subtract(vec3.create(), this.lastGesture.startPosition, this.lastGesture.position);
+}
+
+_.extend(Gesture.prototype, EventEmitter.prototype);
+
+/**
+ * Constructs a new CircleGesture object.
+ *
+ * An uninitialized CircleGesture object is considered invalid. Get valid instances
+ * of the CircleGesture class from a Frame object.
+ *
+ * @class CircleGesture
+ * @memberof Leap
+ * @augments Leap.Gesture
+ * @classdesc
+ * The CircleGesture classes represents a circular finger movement.
+ *
+ * A circle movement is recognized when the tip of a finger draws a circle
+ * within the Leap field of view.
+ *
+ * ![CircleGesture](images/Leap_Gesture_Circle.png)
+ *
+ * Circle gestures are continuous. The CircleGesture objects for the gesture have
+ * three possible states:
+ *
+ * * start -- The circle gesture has just started. The movement has
+ *  progressed far enough for the recognizer to classify it as a circle.
+ * * update -- The circle gesture is continuing.
+ * * stop -- The circle gesture is finished.
+ */
+var CircleGesture = function(data) {
+ /**
+  * The center point of the circle within the Leap frame of reference.
+  *
+  * @member center
+  * @memberof Leap.CircleGesture.prototype
+  * @type {number[]}
+  */
+  this.center = data.center;
+ /**
+  * The normal vector for the circle being traced.
+  *
+  * If you draw the circle clockwise, the normal vector points in the same
+  * general direction as the pointable object drawing the circle. If you draw
+  * the circle counterclockwise, the normal points back toward the
+  * pointable. If the angle between the normal and the pointable object
+  * drawing the circle is less than 90 degrees, then the circle is clockwise.
+  *
+  * ```javascript
+  *    var clockwiseness;
+  *    if (circle.pointable.direction.angleTo(circle.normal) <= PI/4) {
+  *        clockwiseness = "clockwise";
+  *    }
+  *    else
+  *    {
+  *        clockwiseness = "counterclockwise";
+  *    }
+  * ```
+  *
+  * @member normal
+  * @memberof Leap.CircleGesture.prototype
+  * @type {number[]}
+  */
+  this.normal = data.normal;
+ /**
+  * The number of times the finger tip has traversed the circle.
+  *
+  * Progress is reported as a positive number of the number. For example,
+  * a progress value of .5 indicates that the finger has gone halfway
+  * around, while a value of 3 indicates that the finger has gone around
+  * the the circle three times.
+  *
+  * Progress starts where the circle gesture began. Since the circle
+  * must be partially formed before the Leap can recognize it, progress
+  * will be greater than zero when a circle gesture first appears in the
+  * frame.
+  *
+  * @member progress
+  * @memberof Leap.CircleGesture.prototype
+  * @type {number}
+  */
+  this.progress = data.progress;
+ /**
+  * The radius of the circle in mm.
+  *
+  * @member radius
+  * @memberof Leap.CircleGesture.prototype
+  * @type {number}
+  */
+  this.radius = data.radius;
+}
+
+CircleGesture.prototype.toString = function() {
+  return "CircleGesture ["+JSON.stringify(this)+"]";
+}
+
+/**
+ * Constructs a new SwipeGesture object.
+ *
+ * An uninitialized SwipeGesture object is considered invalid. Get valid instances
+ * of the SwipeGesture class from a Frame object.
+ *
+ * @class SwipeGesture
+ * @memberof Leap
+ * @augments Leap.Gesture
+ * @classdesc
+ * The SwipeGesture class represents a swiping motion of a finger or tool.
+ *
+ * ![SwipeGesture](images/Leap_Gesture_Swipe.png)
+ *
+ * Swipe gestures are continuous.
+ */
+var SwipeGesture = function(data) {
+ /**
+  * The starting position within the Leap frame of
+  * reference, in mm.
+  *
+  * @member startPosition
+  * @memberof Leap.SwipeGesture.prototype
+  * @type {number[]}
+  */
+  this.startPosition = data.startPosition;
+ /**
+  * The current swipe position within the Leap frame of
+  * reference, in mm.
+  *
+  * @member position
+  * @memberof Leap.SwipeGesture.prototype
+  * @type {number[]}
+  */
+  this.position = data.position;
+ /**
+  * The unit direction vector parallel to the swipe motion.
+  *
+  * You can compare the components of the vector to classify the swipe as
+  * appropriate for your application. For example, if you are using swipes
+  * for two dimensional scrolling, you can compare the x and y values to
+  * determine if the swipe is primarily horizontal or vertical.
+  *
+  * @member direction
+  * @memberof Leap.SwipeGesture.prototype
+  * @type {number[]}
+  */
+  this.direction = data.direction;
+ /**
+  * The speed of the finger performing the swipe gesture in
+  * millimeters per second.
+  *
+  * @member speed
+  * @memberof Leap.SwipeGesture.prototype
+  * @type {number}
+  */
+  this.speed = data.speed;
+}
+
+SwipeGesture.prototype.toString = function() {
+  return "SwipeGesture ["+JSON.stringify(this)+"]";
+}
+
+/**
+ * Constructs a new ScreenTapGesture object.
+ *
+ * An uninitialized ScreenTapGesture object is considered invalid. Get valid instances
+ * of the ScreenTapGesture class from a Frame object.
+ *
+ * @class ScreenTapGesture
+ * @memberof Leap
+ * @augments Leap.Gesture
+ * @classdesc
+ * The ScreenTapGesture class represents a tapping gesture by a finger or tool.
+ *
+ * A screen tap gesture is recognized when the tip of a finger pokes forward
+ * and then springs back to approximately the original postion, as if
+ * tapping a vertical screen. The tapping finger must pause briefly before beginning the tap.
+ *
+ * ![ScreenTap](images/Leap_Gesture_Tap2.png)
+ *
+ * ScreenTap gestures are discrete. The ScreenTapGesture object representing a tap always
+ * has the state, STATE_STOP. Only one ScreenTapGesture object is created for each
+ * screen tap gesture recognized.
+ */
+var ScreenTapGesture = function(data) {
+ /**
+  * The position where the screen tap is registered.
+  *
+  * @member position
+  * @memberof Leap.ScreenTapGesture.prototype
+  * @type {number[]}
+  */
+  this.position = data.position;
+ /**
+  * The direction of finger tip motion.
+  *
+  * @member direction
+  * @memberof Leap.ScreenTapGesture.prototype
+  * @type {number[]}
+  */
+  this.direction = data.direction;
+ /**
+  * The progess value is always 1.0 for a screen tap gesture.
+  *
+  * @member progress
+  * @memberof Leap.ScreenTapGesture.prototype
+  * @type {number}
+  */
+  this.progress = data.progress;
+}
+
+ScreenTapGesture.prototype.toString = function() {
+  return "ScreenTapGesture ["+JSON.stringify(this)+"]";
+}
+
+/**
+ * Constructs a new KeyTapGesture object.
+ *
+ * An uninitialized KeyTapGesture object is considered invalid. Get valid instances
+ * of the KeyTapGesture class from a Frame object.
+ *
+ * @class KeyTapGesture
+ * @memberof Leap
+ * @augments Leap.Gesture
+ * @classdesc
+ * The KeyTapGesture class represents a tapping gesture by a finger or tool.
+ *
+ * A key tap gesture is recognized when the tip of a finger rotates down toward the
+ * palm and then springs back to approximately the original postion, as if
+ * tapping. The tapping finger must pause briefly before beginning the tap.
+ *
+ * ![KeyTap](images/Leap_Gesture_Tap.png)
+ *
+ * Key tap gestures are discrete. The KeyTapGesture object representing a tap always
+ * has the state, STATE_STOP. Only one KeyTapGesture object is created for each
+ * key tap gesture recognized.
+ */
+var KeyTapGesture = function(data) {
+    /**
+     * The position where the key tap is registered.
+     *
+     * @member position
+     * @memberof Leap.KeyTapGesture.prototype
+     * @type {number[]}
+     */
+    this.position = data.position;
+    /**
+     * The direction of finger tip motion.
+     *
+     * @member direction
+     * @memberof Leap.KeyTapGesture.prototype
+     * @type {number[]}
+     */
+    this.direction = data.direction;
+    /**
+     * The progess value is always 1.0 for a key tap gesture.
+     *
+     * @member progress
+     * @memberof Leap.KeyTapGesture.prototype
+     * @type {number}
+     */
+    this.progress = data.progress;
+}
+
+KeyTapGesture.prototype.toString = function() {
+  return "KeyTapGesture ["+JSON.stringify(this)+"]";
+}
+
+},{"events":21,"gl-matrix":23,"underscore":24}],10:[function(require,module,exports){
+var Pointable = require("./pointable")
+  , Bone = require('./bone')
+  , glMatrix = require("gl-matrix")
+  , mat3 = glMatrix.mat3
+  , vec3 = glMatrix.vec3
+  , _ = require("underscore");
+
+/**
+ * Constructs a Hand object.
+ *
+ * An uninitialized hand is considered invalid.
+ * Get valid Hand objects from a Frame object.
+ * @class Hand
+ * @memberof Leap
+ * @classdesc
+ * The Hand class reports the physical characteristics of a detected hand.
+ *
+ * Hand tracking data includes a palm position and velocity; vectors for
+ * the palm normal and direction to the fingers; properties of a sphere fit
+ * to the hand; and lists of the attached fingers and tools.
+ *
+ * Note that Hand objects can be invalid, which means that they do not contain
+ * valid tracking data and do not correspond to a physical entity. Invalid Hand
+ * objects can be the result of asking for a Hand object using an ID from an
+ * earlier frame when no Hand objects with that ID exist in the current frame.
+ * A Hand object created from the Hand constructor is also invalid.
+ * Test for validity with the [Hand.valid]{@link Leap.Hand#valid} property.
+ */
+var Hand = module.exports = function(data) {
+  /**
+   * A unique ID assigned to this Hand object, whose value remains the same
+   * across consecutive frames while the tracked hand remains visible. If
+   * tracking is lost (for example, when a hand is occluded by another hand
+   * or when it is withdrawn from or reaches the edge of the Leap field of view),
+   * the Leap may assign a new ID when it detects the hand in a future frame.
+   *
+   * Use the ID value with the {@link Frame.hand}() function to find this
+   * Hand object in future frames.
+   *
+   * @member id
+   * @memberof Leap.Hand.prototype
+   * @type {String}
+   */
+  this.id = data.id;
+  /**
+   * The center position of the palm in millimeters from the Leap origin.
+   * @member palmPosition
+   * @memberof Leap.Hand.prototype
+   * @type {number[]}
+   */
+  this.palmPosition = data.palmPosition;
+  /**
+   * The direction from the palm position toward the fingers.
+   *
+   * The direction is expressed as a unit vector pointing in the same
+   * direction as the directed line from the palm position to the fingers.
+   *
+   * @member direction
+   * @memberof Leap.Hand.prototype
+   * @type {number[]}
+   */
+  this.direction = data.direction;
+  /**
+   * The rate of change of the palm position in millimeters/second.
+   *
+   * @member palmVeclocity
+   * @memberof Leap.Hand.prototype
+   * @type {number[]}
+   */
+  this.palmVelocity = data.palmVelocity;
+  /**
+   * The normal vector to the palm. If your hand is flat, this vector will
+   * point downward, or "out" of the front surface of your palm.
+   *
+   * ![Palm Vectors](images/Leap_Palm_Vectors.png)
+   *
+   * The direction is expressed as a unit vector pointing in the same
+   * direction as the palm normal (that is, a vector orthogonal to the palm).
+   * @member palmNormal
+   * @memberof Leap.Hand.prototype
+   * @type {number[]}
+   */
+  this.palmNormal = data.palmNormal;
+  /**
+   * The center of a sphere fit to the curvature of this hand.
+   *
+   * This sphere is placed roughly as if the hand were holding a ball.
+   *
+   * ![Hand Ball](images/Leap_Hand_Ball.png)
+   * @member sphereCenter
+   * @memberof Leap.Hand.prototype
+   * @type {number[]}
+   */
+  this.sphereCenter = data.sphereCenter;
+  /**
+   * The radius of a sphere fit to the curvature of this hand, in millimeters.
+   *
+   * This sphere is placed roughly as if the hand were holding a ball. Thus the
+   * size of the sphere decreases as the fingers are curled into a fist.
+   *
+   * @member sphereRadius
+   * @memberof Leap.Hand.prototype
+   * @type {number}
+   */
+  this.sphereRadius = data.sphereRadius;
+  /**
+   * Reports whether this is a valid Hand object.
+   *
+   * @member valid
+   * @memberof Leap.Hand.prototype
+   * @type {boolean}
+   */
+  this.valid = true;
+  /**
+   * The list of Pointable objects (fingers and tools) detected in this frame
+   * that are associated with this hand, given in arbitrary order. The list
+   * can be empty if no fingers or tools associated with this hand are detected.
+   *
+   * Use the {@link Pointable} tool property to determine
+   * whether or not an item in the list represents a tool or finger.
+   * You can also get only the tools using the Hand.tools[] list or
+   * only the fingers using the Hand.fingers[] list.
+   *
+   * @member pointables[]
+   * @memberof Leap.Hand.prototype
+   * @type {Leap.Pointable[]}
+   */
+  this.pointables = [];
+  /**
+   * The list of fingers detected in this frame that are attached to
+   * this hand, given in arbitrary order.
+   *
+   * The list can be empty if no fingers attached to this hand are detected.
+   *
+   * @member fingers[]
+   * @memberof Leap.Hand.prototype
+   * @type {Leap.Pointable[]}
+   */
+  this.fingers = [];
+  
+  if (data.armBasis){
+    this.arm = new Bone(this, {
+      type: 4,
+      width: data.armWidth,
+      prevJoint: data.elbow,
+      nextJoint: data.wrist,
+      basis: data.armBasis
+    });
+  }else{
+    this.arm = null;
+  }
+  
+  /**
+   * The list of tools detected in this frame that are held by this
+   * hand, given in arbitrary order.
+   *
+   * The list can be empty if no tools held by this hand are detected.
+   *
+   * @member tools[]
+   * @memberof Leap.Hand.prototype
+   * @type {Leap.Pointable[]}
+   */
+  this.tools = [];
+  this._translation = data.t;
+  this._rotation = _.flatten(data.r);
+  this._scaleFactor = data.s;
+
+  /**
+   * Time the hand has been visible in seconds.
+   *
+   * @member timeVisible
+   * @memberof Leap.Hand.prototype
+   * @type {number}
+   */
+   this.timeVisible = data.timeVisible;
+
+  /**
+   * The palm position with stabalization
+   * @member stabilizedPalmPosition
+   * @memberof Leap.Hand.prototype
+   * @type {number[]}
+   */
+   this.stabilizedPalmPosition = data.stabilizedPalmPosition;
+
+   /**
+   * Reports whether this is a left or a right hand.
+   *
+   * @member type
+   * @type {String}
+   * @memberof Leap.Hand.prototype
+   */
+   this.type = data.type;
+   this.grabStrength = data.grabStrength;
+   this.pinchStrength = data.pinchStrength;
+   this.confidence = data.confidence;
+}
+
+/**
+ * The finger with the specified ID attached to this hand.
+ *
+ * Use this function to retrieve a Pointable object representing a finger
+ * attached to this hand using an ID value obtained from a previous frame.
+ * This function always returns a Pointable object, but if no finger
+ * with the specified ID is present, an invalid Pointable object is returned.
+ *
+ * Note that the ID values assigned to fingers persist across frames, but only
+ * until tracking of a particular finger is lost. If tracking of a finger is
+ * lost and subsequently regained, the new Finger object representing that
+ * finger may have a different ID than that representing the finger in an
+ * earlier frame.
+ *
+ * @method finger
+ * @memberof Leap.Hand.prototype
+ * @param {String} id The ID value of a finger from a previous frame.
+ * @returns {Leap.Pointable} The Finger object with
+ * the matching ID if one exists for this hand in this frame; otherwise, an
+ * invalid Finger object is returned.
+ */
+Hand.prototype.finger = function(id) {
+  var finger = this.frame.finger(id);
+  return (finger && (finger.handId == this.id)) ? finger : Pointable.Invalid;
+}
+
+/**
+ * The angle of rotation around the rotation axis derived from the change in
+ * orientation of this hand, and any associated fingers and tools, between the
+ * current frame and the specified frame.
+ *
+ * The returned angle is expressed in radians measured clockwise around the
+ * rotation axis (using the right-hand rule) between the start and end frames.
+ * The value is always between 0 and pi radians (0 and 180 degrees).
+ *
+ * If a corresponding Hand object is not found in sinceFrame, or if either
+ * this frame or sinceFrame are invalid Frame objects, then the angle of rotation is zero.
+ *
+ * @method rotationAngle
+ * @memberof Leap.Hand.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
+ * @param {numnber[]} [axis] The axis to measure rotation around.
+ * @returns {number} A positive value representing the heuristically determined
+ * rotational change of the hand between the current frame and that specified in
+ * the sinceFrame parameter.
+ */
+Hand.prototype.rotationAngle = function(sinceFrame, axis) {
+  if (!this.valid || !sinceFrame.valid) return 0.0;
+  var sinceHand = sinceFrame.hand(this.id);
+  if(!sinceHand.valid) return 0.0;
+  var rot = this.rotationMatrix(sinceFrame);
+  var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5
+  var angle = Math.acos(cs);
+  angle = isNaN(angle) ? 0.0 : angle;
+  if (axis !== undefined) {
+    var rotAxis = this.rotationAxis(sinceFrame);
+    angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis));
+  }
+  return angle;
+}
+
+/**
+ * The axis of rotation derived from the change in orientation of this hand, and
+ * any associated fingers and tools, between the current frame and the specified frame.
+ *
+ * The returned direction vector is normalized.
+ *
+ * If a corresponding Hand object is not found in sinceFrame, or if either
+ * this frame or sinceFrame are invalid Frame objects, then this method returns a zero vector.
+ *
+ * @method rotationAxis
+ * @memberof Leap.Hand.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
+ * @returns {number[]} A normalized direction Vector representing the axis of the heuristically determined
+ * rotational change of the hand between the current frame and that specified in the sinceFrame parameter.
+ */
+Hand.prototype.rotationAxis = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return vec3.create();
+  var sinceHand = sinceFrame.hand(this.id);
+  if (!sinceHand.valid) return vec3.create();
+  return vec3.normalize(vec3.create(), [
+    this._rotation[7] - sinceHand._rotation[5],
+    this._rotation[2] - sinceHand._rotation[6],
+    this._rotation[3] - sinceHand._rotation[1]
+  ]);
+}
+
+/**
+ * The transform matrix expressing the rotation derived from the change in
+ * orientation of this hand, and any associated fingers and tools, between
+ * the current frame and the specified frame.
+ *
+ * If a corresponding Hand object is not found in sinceFrame, or if either
+ * this frame or sinceFrame are invalid Frame objects, then this method returns
+ * an identity matrix.
+ *
+ * @method rotationMatrix
+ * @memberof Leap.Hand.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
+ * @returns {number[]} A transformation Matrix containing the heuristically determined
+ * rotational change of the hand between the current frame and that specified in the sinceFrame parameter.
+ */
+Hand.prototype.rotationMatrix = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return mat3.create();
+  var sinceHand = sinceFrame.hand(this.id);
+  if(!sinceHand.valid) return mat3.create();
+  var transpose = mat3.transpose(mat3.create(), this._rotation);
+  var m = mat3.multiply(mat3.create(), sinceHand._rotation, transpose);
+  return m;
+}
+
+/**
+ * The scale factor derived from the hand's motion between the current frame and the specified frame.
+ *
+ * The scale factor is always positive. A value of 1.0 indicates no scaling took place.
+ * Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion.
+ *
+ * The Leap derives scaling from the relative inward or outward motion of a hand
+ * and its associated fingers and tools (independent of translation and rotation).
+ *
+ * If a corresponding Hand object is not found in sinceFrame, or if either this frame or sinceFrame
+ * are invalid Frame objects, then this method returns 1.0.
+ *
+ * @method scaleFactor
+ * @memberof Leap.Hand.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling.
+ * @returns {number} A positive value representing the heuristically determined
+ * scaling change ratio of the hand between the current frame and that specified in the sinceFrame parameter.
+ */
+Hand.prototype.scaleFactor = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return 1.0;
+  var sinceHand = sinceFrame.hand(this.id);
+  if(!sinceHand.valid) return 1.0;
+
+  return Math.exp(this._scaleFactor - sinceHand._scaleFactor);
+}
+
+/**
+ * The change of position of this hand between the current frame and the specified frame
+ *
+ * The returned translation vector provides the magnitude and direction of the
+ * movement in millimeters.
+ *
+ * If a corresponding Hand object is not found in sinceFrame, or if either this frame or
+ * sinceFrame are invalid Frame objects, then this method returns a zero vector.
+ *
+ * @method translation
+ * @memberof Leap.Hand.prototype
+ * @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation.
+ * @returns {number[]} A Vector representing the heuristically determined change in hand
+ * position between the current frame and that specified in the sinceFrame parameter.
+ */
+Hand.prototype.translation = function(sinceFrame) {
+  if (!this.valid || !sinceFrame.valid) return vec3.create();
+  var sinceHand = sinceFrame.hand(this.id);
+  if(!sinceHand.valid) return vec3.create();
+  return [
+    this._translation[0] - sinceHand._translation[0],
+    this._translation[1] - sinceHand._translation[1],
+    this._translation[2] - sinceHand._translation[2]
+  ];
+}
+
+/**
+ * A string containing a brief, human readable description of the Hand object.
+ * @method toString
+ * @memberof Leap.Hand.prototype
+ * @returns {String} A description of the Hand as a string.
+ */
+Hand.prototype.toString = function() {
+  return "Hand (" + this.type + ") [ id: "+ this.id + " | palm velocity:"+this.palmVelocity+" | sphere center:"+this.sphereCenter+" ] ";
+}
+
+/**
+ * The pitch angle in radians.
+ *
+ * Pitch is the angle between the negative z-axis and the projection of
+ * the vector onto the y-z plane. In other words, pitch represents rotation
+ * around the x-axis.
+ * If the vector points upward, the returned angle is between 0 and pi radians
+ * (180 degrees); if it points downward, the angle is between 0 and -pi radians.
+ *
+ * @method pitch
+ * @memberof Leap.Hand.prototype
+ * @returns {number} The angle of this vector above or below the horizon (x-z plane).
+ *
+ */
+Hand.prototype.pitch = function() {
+  return Math.atan2(this.direction[1], -this.direction[2]);
+}
+
+/**
+ *  The yaw angle in radians.
+ *
+ * Yaw is the angle between the negative z-axis and the projection of
+ * the vector onto the x-z plane. In other words, yaw represents rotation
+ * around the y-axis. If the vector points to the right of the negative z-axis,
+ * then the returned angle is between 0 and pi radians (180 degrees);
+ * if it points to the left, the angle is between 0 and -pi radians.
+ *
+ * @method yaw
+ * @memberof Leap.Hand.prototype
+ * @returns {number} The angle of this vector to the right or left of the y-axis.
+ *
+ */
+Hand.prototype.yaw = function() {
+  return Math.atan2(this.direction[0], -this.direction[2]);
+}
+
+/**
+ *  The roll angle in radians.
+ *
+ * Roll is the angle between the y-axis and the projection of
+ * the vector onto the x-y plane. In other words, roll represents rotation
+ * around the z-axis. If the vector points to the left of the y-axis,
+ * then the returned angle is between 0 and pi radians (180 degrees);
+ * if it points to the right, the angle is between 0 and -pi radians.
+ *
+ * @method roll
+ * @memberof Leap.Hand.prototype
+ * @returns {number} The angle of this vector to the right or left of the y-axis.
+ *
+ */
+Hand.prototype.roll = function() {
+  return Math.atan2(this.palmNormal[0], -this.palmNormal[1]);
+}
+
+/**
+ * An invalid Hand object.
+ *
+ * You can use an invalid Hand object in comparisons testing
+ * whether a given Hand instance is valid or invalid. (You can also use the
+ * Hand valid property.)
+ *
+ * @static
+ * @type {Leap.Hand}
+ * @name Invalid
+ * @memberof Leap.Hand
+ */
+Hand.Invalid = {
+  valid: false,
+  fingers: [],
+  tools: [],
+  pointables: [],
+  left: false,
+  pointable: function() { return Pointable.Invalid },
+  finger: function() { return Pointable.Invalid },
+  toString: function() { return "invalid frame" },
+  dump: function() { return this.toString(); },
+  rotationAngle: function() { return 0.0; },
+  rotationMatrix: function() { return mat3.create(); },
+  rotationAxis: function() { return vec3.create(); },
+  scaleFactor: function() { return 1.0; },
+  translation: function() { return vec3.create(); }
+};
+
+},{"./bone":1,"./pointable":14,"gl-matrix":23,"underscore":24}],11:[function(require,module,exports){
+/**
+ * Leap is the global namespace of the Leap API.
+ * @namespace Leap
+ */
+module.exports = {
+  Controller: require("./controller"),
+  Frame: require("./frame"),
+  Gesture: require("./gesture"),
+  Hand: require("./hand"),
+  Pointable: require("./pointable"),
+  Finger: require("./finger"),
+  InteractionBox: require("./interaction_box"),
+  CircularBuffer: require("./circular_buffer"),
+  UI: require("./ui"),
+  JSONProtocol: require("./protocol").JSONProtocol,
+  glMatrix: require("gl-matrix"),
+  mat3: require("gl-matrix").mat3,
+  vec3: require("gl-matrix").vec3,
+  loopController: undefined,
+  version: require('./version.js'),
+
+  /**
+   * Expose utility libraries for convenience
+   * Use carefully - they may be subject to upgrade or removal in different versions of LeapJS.
+   *
+   */
+  _: require('underscore'),
+  EventEmitter: require('events').EventEmitter,
+
+  /**
+   * The Leap.loop() function passes a frame of Leap data to your
+   * callback function and then calls window.requestAnimationFrame() after
+   * executing your callback function.
+   *
+   * Leap.loop() sets up the Leap controller and WebSocket connection for you.
+   * You do not need to create your own controller when using this method.
+   *
+   * Your callback function is called on an interval determined by the client
+   * browser. Typically, this is on an interval of 60 frames/second. The most
+   * recent frame of Leap data is passed to your callback function. If the Leap
+   * is producing frames at a slower rate than the browser frame rate, the same
+   * frame of Leap data can be passed to your function in successive animation
+   * updates.
+   *
+   * As an alternative, you can create your own Controller object and use a
+   * {@link Controller#onFrame onFrame} callback to process the data at
+   * the frame rate of the Leap device. See {@link Controller} for an
+   * example.
+   *
+   * @method Leap.loop
+   * @param {function} callback A function called when the browser is ready to
+   * draw to the screen. The most recent {@link Frame} object is passed to
+   * your callback function.
+   *
+   * ```javascript
+   *    Leap.loop( function( frame ) {
+   *        // ... your code here
+   *    })
+   * ```
+   */
+  loop: function(opts, callback) {
+    if (opts && callback === undefined &&  ( ({}).toString.call(opts) === '[object Function]' ) ) {
+      callback = opts;
+      opts = {};
+    }
+
+    if (this.loopController) {
+      if (opts){
+        this.loopController.setupFrameEvents(opts);
+      }
+    }else{
+      this.loopController = new this.Controller(opts);
+    }
+
+    this.loopController.loop(callback);
+    return this.loopController;
+  },
+
+  /*
+   * Convenience method for Leap.Controller.plugin
+   */
+  plugin: function(name, options){
+    this.Controller.plugin(name, options)
+  }
+}
+
+},{"./circular_buffer":2,"./controller":5,"./finger":7,"./frame":8,"./gesture":9,"./hand":10,"./interaction_box":12,"./pointable":14,"./protocol":15,"./ui":16,"./version.js":19,"events":21,"gl-matrix":23,"underscore":24}],12:[function(require,module,exports){
+var glMatrix = require("gl-matrix")
+  , vec3 = glMatrix.vec3;
+
+/**
+ * Constructs a InteractionBox object.
+ *
+ * @class InteractionBox
+ * @memberof Leap
+ * @classdesc
+ * The InteractionBox class represents a box-shaped region completely within
+ * the field of view of the Leap Motion controller.
+ *
+ * The interaction box is an axis-aligned rectangular prism and provides
+ * normalized coordinates for hands, fingers, and tools within this box.
+ * The InteractionBox class can make it easier to map positions in the
+ * Leap Motion coordinate system to 2D or 3D coordinate systems used
+ * for application drawing.
+ *
+ * ![Interaction Box](images/Leap_InteractionBox.png)
+ *
+ * The InteractionBox region is defined by a center and dimensions along the x, y, and z axes.
+ */
+var InteractionBox = module.exports = function(data) {
+  /**
+   * Indicates whether this is a valid InteractionBox object.
+   *
+   * @member valid
+   * @type {Boolean}
+   * @memberof Leap.InteractionBox.prototype
+   */
+  this.valid = true;
+  /**
+   * The center of the InteractionBox in device coordinates (millimeters).
+   * This point is equidistant from all sides of the box.
+   *
+   * @member center
+   * @type {number[]}
+   * @memberof Leap.InteractionBox.prototype
+   */
+  this.center = data.center;
+
+  this.size = data.size;
+  /**
+   * The width of the InteractionBox in millimeters, measured along the x-axis.
+   *
+   * @member width
+   * @type {number}
+   * @memberof Leap.InteractionBox.prototype
+   */
+  this.width = data.size[0];
+  /**
+   * The height of the InteractionBox in millimeters, measured along the y-axis.
+   *
+   * @member height
+   * @type {number}
+   * @memberof Leap.InteractionBox.prototype
+   */
+  this.height = data.size[1];
+  /**
+   * The depth of the InteractionBox in millimeters, measured along the z-axis.
+   *
+   * @member depth
+   * @type {number}
+   * @memberof Leap.InteractionBox.prototype
+   */
+  this.depth = data.size[2];
+}
+
+/**
+ * Converts a position defined by normalized InteractionBox coordinates
+ * into device coordinates in millimeters.
+ *
+ * This function performs the inverse of normalizePoint().
+ *
+ * @method denormalizePoint
+ * @memberof Leap.InteractionBox.prototype
+ * @param {number[]} normalizedPosition The input position in InteractionBox coordinates.
+ * @returns {number[]} The corresponding denormalized position in device coordinates.
+ */
+InteractionBox.prototype.denormalizePoint = function(normalizedPosition) {
+  return vec3.fromValues(
+    (normalizedPosition[0] - 0.5) * this.size[0] + this.center[0],
+    (normalizedPosition[1] - 0.5) * this.size[1] + this.center[1],
+    (normalizedPosition[2] - 0.5) * this.size[2] + this.center[2]
+  );
+}
+
+/**
+ * Normalizes the coordinates of a point using the interaction box.
+ *
+ * Coordinates from the Leap Motion frame of reference (millimeters) are
+ * converted to a range of [0..1] such that the minimum value of the
+ * InteractionBox maps to 0 and the maximum value of the InteractionBox maps to 1.
+ *
+ * @method normalizePoint
+ * @memberof Leap.InteractionBox.prototype
+ * @param {number[]} position The input position in device coordinates.
+ * @param {Boolean} clamp Whether or not to limit the output value to the range [0,1]
+ * when the input position is outside the InteractionBox. Defaults to true.
+ * @returns {number[]} The normalized position.
+ */
+InteractionBox.prototype.normalizePoint = function(position, clamp) {
+  var vec = vec3.fromValues(
+    ((position[0] - this.center[0]) / this.size[0]) + 0.5,
+    ((position[1] - this.center[1]) / this.size[1]) + 0.5,
+    ((position[2] - this.center[2]) / this.size[2]) + 0.5
+  );
+
+  if (clamp) {
+    vec[0] = Math.min(Math.max(vec[0], 0), 1);
+    vec[1] = Math.min(Math.max(vec[1], 0), 1);
+    vec[2] = Math.min(Math.max(vec[2], 0), 1);
+  }
+  return vec;
+}
+
+/**
+ * Writes a brief, human readable description of the InteractionBox object.
+ *
+ * @method toString
+ * @memberof Leap.InteractionBox.prototype
+ * @returns {String} A description of the InteractionBox object as a string.
+ */
+InteractionBox.prototype.toString = function() {
+  return "InteractionBox [ width:" + this.width + " | height:" + this.height + " | depth:" + this.depth + " ]";
+}
+
+/**
+ * An invalid InteractionBox object.
+ *
+ * You can use this InteractionBox instance in comparisons testing
+ * whether a given InteractionBox instance is valid or invalid. (You can also use the
+ * InteractionBox.valid property.)
+ *
+ * @static
+ * @type {Leap.InteractionBox}
+ * @name Invalid
+ * @memberof Leap.InteractionBox
+ */
+InteractionBox.Invalid = { valid: false };
+
+},{"gl-matrix":23}],13:[function(require,module,exports){
+var Pipeline = module.exports = function (controller) {
+  this.steps = [];
+  this.controller = controller;
+}
+
+Pipeline.prototype.addStep = function (step) {
+  this.steps.push(step);
+}
+
+Pipeline.prototype.run = function (frame) {
+  var stepsLength = this.steps.length;
+  for (var i = 0; i != stepsLength; i++) {
+    if (!frame) break;
+    frame = this.steps[i](frame);
+  }
+  return frame;
+}
+
+Pipeline.prototype.removeStep = function(step){
+  var index = this.steps.indexOf(step);
+  if (index === -1) throw "Step not found in pipeline";
+  this.steps.splice(index, 1);
+}
+
+/*
+ * Wraps a plugin callback method in method which can be run inside the pipeline.
+ * This wrapper method loops the callback over objects within the frame as is appropriate,
+ * calling the callback for each in turn.
+ *
+ * @method createStepFunction
+ * @memberOf Leap.Controller.prototype
+ * @param {Controller} The controller on which the callback is called.
+ * @param {String} type What frame object the callback is run for and receives.
+ *       Can be one of 'frame', 'finger', 'hand', 'pointable', 'tool'
+ * @param {function} callback The method which will be run inside the pipeline loop.  Receives one argument, such as a hand.
+ * @private
+ */
+Pipeline.prototype.addWrappedStep = function (type, callback) {
+  var controller = this.controller,
+    step = function (frame) {
+      var dependencies, i, len;
+      dependencies = (type == 'frame') ? [frame] : (frame[type + 's'] || []);
+
+      for (i = 0, len = dependencies.length; i < len; i++) {
+        callback.call(controller, dependencies[i]);
+      }
+
+      return frame;
+    };
+
+  this.addStep(step);
+  return step;
+};
+},{}],14:[function(require,module,exports){
+var glMatrix = require("gl-matrix")
+  , vec3 = glMatrix.vec3;
+
+/**
+ * Constructs a Pointable object.
+ *
+ * An uninitialized pointable is considered invalid.
+ * Get valid Pointable objects from a Frame or a Hand object.
+ *
+ * @class Pointable
+ * @memberof Leap
+ * @classdesc
+ * The Pointable class reports the physical characteristics of a detected
+ * finger or tool.
+ *
+ * Both fingers and tools are classified as Pointable objects. Use the
+ * Pointable.tool property to determine whether a Pointable object represents a
+ * tool or finger. The Leap classifies a detected entity as a tool when it is
+ * thinner, straighter, and longer than a typical finger.
+ *
+ * Note that Pointable objects can be invalid, which means that they do not
+ * contain valid tracking data and do not correspond to a physical entity.
+ * Invalid Pointable objects can be the result of asking for a Pointable object
+ * using an ID from an earlier frame when no Pointable objects with that ID
+ * exist in the current frame. A Pointable object created from the Pointable
+ * constructor is also invalid. Test for validity with the Pointable.valid
+ * property.
+ */
+var Pointable = module.exports = function(data) {
+  /**
+   * Indicates whether this is a valid Pointable object.
+   *
+   * @member valid
+   * @type {Boolean}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.valid = true;
+  /**
+   * A unique ID assigned to this Pointable object, whose value remains the
+   * same across consecutive frames while the tracked finger or tool remains
+   * visible. If tracking is lost (for example, when a finger is occluded by
+   * another finger or when it is withdrawn from the Leap field of view), the
+   * Leap may assign a new ID when it detects the entity in a future frame.
+   *
+   * Use the ID value with the pointable() functions defined for the
+   * {@link Frame} and {@link Frame.Hand} classes to find this
+   * Pointable object in future frames.
+   *
+   * @member id
+   * @type {String}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.id = data.id;
+  this.handId = data.handId;
+  /**
+   * The estimated length of the finger or tool in millimeters.
+   *
+   * The reported length is the visible length of the finger or tool from the
+   * hand to tip. If the length isn't known, then a value of 0 is returned.
+   *
+   * @member length
+   * @type {number}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.length = data.length;
+  /**
+   * Whether or not the Pointable is believed to be a tool.
+   * Tools are generally longer, thinner, and straighter than fingers.
+   *
+   * If tool is false, then this Pointable must be a finger.
+   *
+   * @member tool
+   * @type {Boolean}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.tool = data.tool;
+  /**
+   * The estimated width of the tool in millimeters.
+   *
+   * The reported width is the average width of the visible portion of the
+   * tool from the hand to the tip. If the width isn't known,
+   * then a value of 0 is returned.
+   *
+   * Pointable objects representing fingers do not have a width property.
+   *
+   * @member width
+   * @type {number}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.width = data.width;
+  /**
+   * The direction in which this finger or tool is pointing.
+   *
+   * The direction is expressed as a unit vector pointing in the same
+   * direction as the tip.
+   *
+   * ![Finger](images/Leap_Finger_Model.png)
+   * @member direction
+   * @type {number[]}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.direction = data.direction;
+  /**
+   * The tip position in millimeters from the Leap origin.
+   * Stabilized
+   *
+   * @member stabilizedTipPosition
+   * @type {number[]}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.stabilizedTipPosition = data.stabilizedTipPosition;
+  /**
+   * The tip position in millimeters from the Leap origin.
+   *
+   * @member tipPosition
+   * @type {number[]}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.tipPosition = data.tipPosition;
+  /**
+   * The rate of change of the tip position in millimeters/second.
+   *
+   * @member tipVelocity
+   * @type {number[]}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.tipVelocity = data.tipVelocity;
+  /**
+   * The current touch zone of this Pointable object.
+   *
+   * The Leap Motion software computes the touch zone based on a floating touch
+   * plane that adapts to the user's finger movement and hand posture. The Leap
+   * Motion software interprets purposeful movements toward this plane as potential touch
+   * points. When a Pointable moves close to the adaptive touch plane, it enters the
+   * "hovering" zone. When a Pointable reaches or passes through the plane, it enters
+   * the "touching" zone.
+   *
+   * The possible states include:
+   *
+   * * "none" -- The Pointable is outside the hovering zone.
+   * * "hovering" -- The Pointable is close to, but not touching the touch plane.
+   * * "touching" -- The Pointable has penetrated the touch plane.
+   *
+   * The touchDistance value provides a normalized indication of the distance to
+   * the touch plane when the Pointable is in the hovering or touching zones.
+   *
+   * @member touchZone
+   * @type {String}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.touchZone = data.touchZone;
+  /**
+   * A value proportional to the distance between this Pointable object and the
+   * adaptive touch plane.
+   *
+   * ![Touch Distance](images/Leap_Touch_Plane.png)
+   *
+   * The touch distance is a value in the range [-1, 1]. The value 1.0 indicates the
+   * Pointable is at the far edge of the hovering zone. The value 0 indicates the
+   * Pointable is just entering the touching zone. A value of -1.0 indicates the
+   * Pointable is firmly within the touching zone. Values in between are
+   * proportional to the distance from the plane. Thus, the touchDistance of 0.5
+   * indicates that the Pointable is halfway into the hovering zone.
+   *
+   * You can use the touchDistance value to modulate visual feedback given to the
+   * user as their fingers close in on a touch target, such as a button.
+   *
+   * @member touchDistance
+   * @type {number}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.touchDistance = data.touchDistance;
+
+  /**
+   * How long the pointable has been visible in seconds.
+   *
+   * @member timeVisible
+   * @type {number}
+   * @memberof Leap.Pointable.prototype
+   */
+  this.timeVisible = data.timeVisible;
+}
+
+/**
+ * A string containing a brief, human readable description of the Pointable
+ * object.
+ *
+ * @method toString
+ * @memberof Leap.Pointable.prototype
+ * @returns {String} A description of the Pointable object as a string.
+ */
+Pointable.prototype.toString = function() {
+  return "Pointable [ id:" + this.id + " " + this.length + "mmx | width:" + this.width + "mm | direction:" + this.direction + ' ]';
+}
+
+/**
+ * Returns the hand which the pointable is attached to.
+ */
+Pointable.prototype.hand = function(){
+  return this.frame.hand(this.handId);
+}
+
+/**
+ * An invalid Pointable object.
+ *
+ * You can use this Pointable instance in comparisons testing
+ * whether a given Pointable instance is valid or invalid. (You can also use the
+ * Pointable.valid property.)
+
+ * @static
+ * @type {Leap.Pointable}
+ * @name Invalid
+ * @memberof Leap.Pointable
+ */
+Pointable.Invalid = { valid: false };
+
+},{"gl-matrix":23}],15:[function(require,module,exports){
+var Frame = require('./frame')
+  , Hand = require('./hand')
+  , Pointable = require('./pointable')
+  , Finger = require('./finger')
+  , _ = require('underscore')
+  , EventEmitter = require('events').EventEmitter;
+
+var Event = function(data) {
+  this.type = data.type;
+  this.state = data.state;
+};
+
+exports.chooseProtocol = function(header) {
+  var protocol;
+  switch(header.version) {
+    case 1:
+    case 2:
+    case 3:
+    case 4:
+    case 5:
+    case 6:
+      protocol = JSONProtocol(header);
+      protocol.sendBackground = function(connection, state) {
+        connection.send(protocol.encode({background: state}));
+      }
+      protocol.sendFocused = function(connection, state) {
+        connection.send(protocol.encode({focused: state}));
+      }
+      protocol.sendOptimizeHMD = function(connection, state) {
+        connection.send(protocol.encode({optimizeHMD: state}));
+      }
+      break;
+    default:
+      throw "unrecognized version";
+  }
+  return protocol;
+}
+
+var JSONProtocol = exports.JSONProtocol = function(header) {
+
+  var protocol = function(frameData) {
+
+    if (frameData.event) {
+
+      return new Event(frameData.event);
+
+    } else {
+
+      protocol.emit('beforeFrameCreated', frameData);
+
+      var frame = new Frame(frameData);
+
+      protocol.emit('afterFrameCreated', frame, frameData);
+
+      return frame;
+
+    }
+
+  };
+
+  protocol.encode = function(message) {
+    return JSON.stringify(message);
+  };
+  protocol.version = header.version;
+  protocol.serviceVersion = header.serviceVersion;
+  protocol.versionLong = 'Version ' + header.version;
+  protocol.type = 'protocol';
+
+  _.extend(protocol, EventEmitter.prototype);
+
+  return protocol;
+};
+
+
+
+},{"./finger":7,"./frame":8,"./hand":10,"./pointable":14,"events":21,"underscore":24}],16:[function(require,module,exports){
+exports.UI = {
+  Region: require("./ui/region"),
+  Cursor: require("./ui/cursor")
+};
+},{"./ui/cursor":17,"./ui/region":18}],17:[function(require,module,exports){
+var Cursor = module.exports = function() {
+  return function(frame) {
+    var pointable = frame.pointables.sort(function(a, b) { return a.z - b.z })[0]
+    if (pointable && pointable.valid) {
+      frame.cursorPosition = pointable.tipPosition
+    }
+    return frame
+  }
+}
+
+},{}],18:[function(require,module,exports){
+var EventEmitter = require('events').EventEmitter
+  , _ = require('underscore')
+
+var Region = module.exports = function(start, end) {
+  this.start = new Vector(start)
+  this.end = new Vector(end)
+  this.enteredFrame = null
+}
+
+Region.prototype.hasPointables = function(frame) {
+  for (var i = 0; i != frame.pointables.length; i++) {
+    var position = frame.pointables[i].tipPosition
+    if (position.x >= this.start.x && position.x <= this.end.x && position.y >= this.start.y && position.y <= this.end.y && position.z >= this.start.z && position.z <= this.end.z) {
+      return true
+    }
+  }
+  return false
+}
+
+Region.prototype.listener = function(opts) {
+  var region = this
+  if (opts && opts.nearThreshold) this.setupNearRegion(opts.nearThreshold)
+  return function(frame) {
+    return region.updatePosition(frame)
+  }
+}
+
+Region.prototype.clipper = function() {
+  var region = this
+  return function(frame) {
+    region.updatePosition(frame)
+    return region.enteredFrame ? frame : null
+  }
+}
+
+Region.prototype.setupNearRegion = function(distance) {
+  var nearRegion = this.nearRegion = new Region(
+    [this.start.x - distance, this.start.y - distance, this.start.z - distance],
+    [this.end.x + distance, this.end.y + distance, this.end.z + distance]
+  )
+  var region = this
+  nearRegion.on("enter", function(frame) {
+    region.emit("near", frame)
+  })
+  nearRegion.on("exit", function(frame) {
+    region.emit("far", frame)
+  })
+  region.on('exit', function(frame) {
+    region.emit("near", frame)
+  })
+}
+
+Region.prototype.updatePosition = function(frame) {
+  if (this.nearRegion) this.nearRegion.updatePosition(frame)
+  if (this.hasPointables(frame) && this.enteredFrame == null) {
+    this.enteredFrame = frame
+    this.emit("enter", this.enteredFrame)
+  } else if (!this.hasPointables(frame) && this.enteredFrame != null) {
+    this.enteredFrame = null
+    this.emit("exit", this.enteredFrame)
+  }
+  return frame
+}
+
+Region.prototype.normalize = function(position) {
+  return new Vector([
+    (position.x - this.start.x) / (this.end.x - this.start.x),
+    (position.y - this.start.y) / (this.end.y - this.start.y),
+    (position.z - this.start.z) / (this.end.z - this.start.z)
+  ])
+}
+
+Region.prototype.mapToXY = function(position, width, height) {
+  var normalized = this.normalize(position)
+  var x = normalized.x, y = normalized.y
+  if (x > 1) x = 1
+  else if (x < -1) x = -1
+  if (y > 1) y = 1
+  else if (y < -1) y = -1
+  return [
+    (x + 1) / 2 * width,
+    (1 - y) / 2 * height,
+    normalized.z
+  ]
+}
+
+_.extend(Region.prototype, EventEmitter.prototype)
+},{"events":21,"underscore":24}],19:[function(require,module,exports){
+// This file is automatically updated from package.json by grunt.
+module.exports = {
+  full: '0.6.4',
+  major: 0,
+  minor: 6,
+  dot: 4
+}
+},{}],20:[function(require,module,exports){
+
+},{}],21:[function(require,module,exports){
+var process=require("__browserify_process");if (!process.EventEmitter) process.EventEmitter = function () {};
+
+var EventEmitter = exports.EventEmitter = process.EventEmitter;
+var isArray = typeof Array.isArray === 'function'
+    ? Array.isArray
+    : function (xs) {
+        return Object.prototype.toString.call(xs) === '[object Array]'
+    }
+;
+function indexOf (xs, x) {
+    if (xs.indexOf) return xs.indexOf(x);
+    for (var i = 0; i < xs.length; i++) {
+        if (x === xs[i]) return i;
+    }
+    return -1;
+}
+
+// By default EventEmitters will print a warning if more than
+// 10 listeners are added to it. This is a useful default which
+// helps finding memory leaks.
+//
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+var defaultMaxListeners = 10;
+EventEmitter.prototype.setMaxListeners = function(n) {
+  if (!this._events) this._events = {};
+  this._events.maxListeners = n;
+};
+
+
+EventEmitter.prototype.emit = function(type) {
+  // If there is no 'error' event listener then throw.
+  if (type === 'error') {
+    if (!this._events || !this._events.error ||
+        (isArray(this._events.error) && !this._events.error.length))
+    {
+      if (arguments[1] instanceof Error) {
+        throw arguments[1]; // Unhandled 'error' event
+      } else {
+        throw new Error("Uncaught, unspecified 'error' event.");
+      }
+      return false;
+    }
+  }
+
+  if (!this._events) return false;
+  var handler = this._events[type];
+  if (!handler) return false;
+
+  if (typeof handler == 'function') {
+    switch (arguments.length) {
+      // fast cases
+      case 1:
+        handler.call(this);
+        break;
+      case 2:
+        handler.call(this, arguments[1]);
+        break;
+      case 3:
+        handler.call(this, arguments[1], arguments[2]);
+        break;
+      // slower
+      default:
+        var args = Array.prototype.slice.call(arguments, 1);
+        handler.apply(this, args);
+    }
+    return true;
+
+  } else if (isArray(handler)) {
+    var args = Array.prototype.slice.call(arguments, 1);
+
+    var listeners = handler.slice();
+    for (var i = 0, l = listeners.length; i < l; i++) {
+      listeners[i].apply(this, args);
+    }
+    return true;
+
+  } else {
+    return false;
+  }
+};
+
+// EventEmitter is defined in src/node_events.cc
+// EventEmitter.prototype.emit() is also defined there.
+EventEmitter.prototype.addListener = function(type, listener) {
+  if ('function' !== typeof listener) {
+    throw new Error('addListener only takes instances of Function');
+  }
+
+  if (!this._events) this._events = {};
+
+  // To avoid recursion in the case that type == "newListeners"! Before
+  // adding it to the listeners, first emit "newListeners".
+  this.emit('newListener', type, listener);
+
+  if (!this._events[type]) {
+    // Optimize the case of one listener. Don't need the extra array object.
+    this._events[type] = listener;
+  } else if (isArray(this._events[type])) {
+
+    // Check for listener leak
+    if (!this._events[type].warned) {
+      var m;
+      if (this._events.maxListeners !== undefined) {
+        m = this._events.maxListeners;
+      } else {
+        m = defaultMaxListeners;
+      }
+
+      if (m && m > 0 && this._events[type].length > m) {
+        this._events[type].warned = true;
+        console.error('(node) warning: possible EventEmitter memory ' +
+                      'leak detected. %d listeners added. ' +
+                      'Use emitter.setMaxListeners() to increase limit.',
+                      this._events[type].length);
+        console.trace();
+      }
+    }
+
+    // If we've already got an array, just append.
+    this._events[type].push(listener);
+  } else {
+    // Adding the second element, need to change to array.
+    this._events[type] = [this._events[type], listener];
+  }
+
+  return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function(type, listener) {
+  var self = this;
+  self.on(type, function g() {
+    self.removeListener(type, g);
+    listener.apply(this, arguments);
+  });
+
+  return this;
+};
+
+EventEmitter.prototype.removeListener = function(type, listener) {
+  if ('function' !== typeof listener) {
+    throw new Error('removeListener only takes instances of Function');
+  }
+
+  // does not use listeners(), so no side effect of creating _events[type]
+  if (!this._events || !this._events[type]) return this;
+
+  var list = this._events[type];
+
+  if (isArray(list)) {
+    var i = indexOf(list, listener);
+    if (i < 0) return this;
+    list.splice(i, 1);
+    if (list.length == 0)
+      delete this._events[type];
+  } else if (this._events[type] === listener) {
+    delete this._events[type];
+  }
+
+  return this;
+};
+
+EventEmitter.prototype.removeAllListeners = function(type) {
+  if (arguments.length === 0) {
+    this._events = {};
+    return this;
+  }
+
+  // does not use listeners(), so no side effect of creating _events[type]
+  if (type && this._events && this._events[type]) this._events[type] = null;
+  return this;
+};
+
+EventEmitter.prototype.listeners = function(type) {
+  if (!this._events) this._events = {};
+  if (!this._events[type]) this._events[type] = [];
+  if (!isArray(this._events[type])) {
+    this._events[type] = [this._events[type]];
+  }
+  return this._events[type];
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+  var ret;
+  if (!emitter._events || !emitter._events[type])
+    ret = 0;
+  else if (typeof emitter._events[type] === 'function')
+    ret = 1;
+  else
+    ret = emitter._events[type].length;
+  return ret;
+};
+
+},{"__browserify_process":22}],22:[function(require,module,exports){
+// shim for using process in browser
+
+var process = module.exports = {};
+
+process.nextTick = (function () {
+    var canSetImmediate = typeof window !== 'undefined'
+    && window.setImmediate;
+    var canPost = typeof window !== 'undefined'
+    && window.postMessage && window.addEventListener
+    ;
+
+    if (canSetImmediate) {
+        return function (f) { return window.setImmediate(f) };
+    }
+
+    if (canPost) {
+        var queue = [];
+        window.addEventListener('message', function (ev) {
+            var source = ev.source;
+            if ((source === window || source === null) && ev.data === 'process-tick') {
+                ev.stopPropagation();
+                if (queue.length > 0) {
+                    var fn = queue.shift();
+                    fn();
+                }
+            }
+        }, true);
+
+        return function nextTick(fn) {
+            queue.push(fn);
+            window.postMessage('process-tick', '*');
+        };
+    }
+
+    return function nextTick(fn) {
+        setTimeout(fn, 0);
+    };
+})();
+
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+
+process.binding = function (name) {
+    throw new Error('process.binding is not supported');
+}
+
+// TODO(shtylman)
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+    throw new Error('process.chdir is not supported');
+};
+
+},{}],23:[function(require,module,exports){
+/**
+ * @fileoverview gl-matrix - High performance matrix and vector operations
+ * @author Brandon Jones
+ * @author Colin MacKenzie IV
+ * @version 2.2.1
+ */
+
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+
+(function(_global) {
+  "use strict";
+
+  var shim = {};
+  if (typeof(exports) === 'undefined') {
+    if(typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
+      shim.exports = {};
+      define(function() {
+        return shim.exports;
+      });
+    } else {
+      // gl-matrix lives in a browser, define its namespaces in global
+      shim.exports = typeof(window) !== 'undefined' ? window : _global;
+    }
+  }
+  else {
+    // gl-matrix lives in commonjs, define its namespaces in exports
+    shim.exports = exports;
+  }
+
+  (function(exports) {
+    /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+
+if(!GLMAT_EPSILON) {
+    var GLMAT_EPSILON = 0.000001;
+}
+
+if(!GLMAT_ARRAY_TYPE) {
+    var GLMAT_ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array;
+}
+
+if(!GLMAT_RANDOM) {
+    var GLMAT_RANDOM = Math.random;
+}
+
+/**
+ * @class Common utilities
+ * @name glMatrix
+ */
+var glMatrix = {};
+
+/**
+ * Sets the type of array used when creating new vectors and matricies
+ *
+ * @param {Type} type Array type, such as Float32Array or Array
+ */
+glMatrix.setMatrixArrayType = function(type) {
+    GLMAT_ARRAY_TYPE = type;
+}
+
+if(typeof(exports) !== 'undefined') {
+    exports.glMatrix = glMatrix;
+}
+
+var degree = Math.PI / 180;
+
+/**
+* Convert Degree To Radian
+*
+* @param {Number} Angle in Degrees
+*/
+glMatrix.toRadian = function(a){
+     return a * degree;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 2 Dimensional Vector
+ * @name vec2
+ */
+
+var vec2 = {};
+
+/**
+ * Creates a new, empty vec2
+ *
+ * @returns {vec2} a new 2D vector
+ */
+vec2.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(2);
+    out[0] = 0;
+    out[1] = 0;
+    return out;
+};
+
+/**
+ * Creates a new vec2 initialized with values from an existing vector
+ *
+ * @param {vec2} a vector to clone
+ * @returns {vec2} a new 2D vector
+ */
+vec2.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(2);
+    out[0] = a[0];
+    out[1] = a[1];
+    return out;
+};
+
+/**
+ * Creates a new vec2 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} a new 2D vector
+ */
+vec2.fromValues = function(x, y) {
+    var out = new GLMAT_ARRAY_TYPE(2);
+    out[0] = x;
+    out[1] = y;
+    return out;
+};
+
+/**
+ * Copy the values from one vec2 to another
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the source vector
+ * @returns {vec2} out
+ */
+vec2.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    return out;
+};
+
+/**
+ * Set the components of a vec2 to the given values
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} out
+ */
+vec2.set = function(out, x, y) {
+    out[0] = x;
+    out[1] = y;
+    return out;
+};
+
+/**
+ * Adds two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.add = function(out, a, b) {
+    out[0] = a[0] + b[0];
+    out[1] = a[1] + b[1];
+    return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.subtract = function(out, a, b) {
+    out[0] = a[0] - b[0];
+    out[1] = a[1] - b[1];
+    return out;
+};
+
+/**
+ * Alias for {@link vec2.subtract}
+ * @function
+ */
+vec2.sub = vec2.subtract;
+
+/**
+ * Multiplies two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.multiply = function(out, a, b) {
+    out[0] = a[0] * b[0];
+    out[1] = a[1] * b[1];
+    return out;
+};
+
+/**
+ * Alias for {@link vec2.multiply}
+ * @function
+ */
+vec2.mul = vec2.multiply;
+
+/**
+ * Divides two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.divide = function(out, a, b) {
+    out[0] = a[0] / b[0];
+    out[1] = a[1] / b[1];
+    return out;
+};
+
+/**
+ * Alias for {@link vec2.divide}
+ * @function
+ */
+vec2.div = vec2.divide;
+
+/**
+ * Returns the minimum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.min = function(out, a, b) {
+    out[0] = Math.min(a[0], b[0]);
+    out[1] = Math.min(a[1], b[1]);
+    return out;
+};
+
+/**
+ * Returns the maximum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.max = function(out, a, b) {
+    out[0] = Math.max(a[0], b[0]);
+    out[1] = Math.max(a[1], b[1]);
+    return out;
+};
+
+/**
+ * Scales a vec2 by a scalar number
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec2} out
+ */
+vec2.scale = function(out, a, b) {
+    out[0] = a[0] * b;
+    out[1] = a[1] * b;
+    return out;
+};
+
+/**
+ * Adds two vec2's after scaling the second operand by a scalar value
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec2} out
+ */
+vec2.scaleAndAdd = function(out, a, b, scale) {
+    out[0] = a[0] + (b[0] * scale);
+    out[1] = a[1] + (b[1] * scale);
+    return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec2.distance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1];
+    return Math.sqrt(x*x + y*y);
+};
+
+/**
+ * Alias for {@link vec2.distance}
+ * @function
+ */
+vec2.dist = vec2.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec2.squaredDistance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1];
+    return x*x + y*y;
+};
+
+/**
+ * Alias for {@link vec2.squaredDistance}
+ * @function
+ */
+vec2.sqrDist = vec2.squaredDistance;
+
+/**
+ * Calculates the length of a vec2
+ *
+ * @param {vec2} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec2.length = function (a) {
+    var x = a[0],
+        y = a[1];
+    return Math.sqrt(x*x + y*y);
+};
+
+/**
+ * Alias for {@link vec2.length}
+ * @function
+ */
+vec2.len = vec2.length;
+
+/**
+ * Calculates the squared length of a vec2
+ *
+ * @param {vec2} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec2.squaredLength = function (a) {
+    var x = a[0],
+        y = a[1];
+    return x*x + y*y;
+};
+
+/**
+ * Alias for {@link vec2.squaredLength}
+ * @function
+ */
+vec2.sqrLen = vec2.squaredLength;
+
+/**
+ * Negates the components of a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to negate
+ * @returns {vec2} out
+ */
+vec2.negate = function(out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    return out;
+};
+
+/**
+ * Normalize a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to normalize
+ * @returns {vec2} out
+ */
+vec2.normalize = function(out, a) {
+    var x = a[0],
+        y = a[1];
+    var len = x*x + y*y;
+    if (len > 0) {
+        //TODO: evaluate use of glm_invsqrt here?
+        len = 1 / Math.sqrt(len);
+        out[0] = a[0] * len;
+        out[1] = a[1] * len;
+    }
+    return out;
+};
+
+/**
+ * Calculates the dot product of two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec2.dot = function (a, b) {
+    return a[0] * b[0] + a[1] * b[1];
+};
+
+/**
+ * Computes the cross product of two vec2's
+ * Note that the cross product must by definition produce a 3D vector
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec3} out
+ */
+vec2.cross = function(out, a, b) {
+    var z = a[0] * b[1] - a[1] * b[0];
+    out[0] = out[1] = 0;
+    out[2] = z;
+    return out;
+};
+
+/**
+ * Performs a linear interpolation between two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec2} out
+ */
+vec2.lerp = function (out, a, b, t) {
+    var ax = a[0],
+        ay = a[1];
+    out[0] = ax + t * (b[0] - ax);
+    out[1] = ay + t * (b[1] - ay);
+    return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec2} out
+ */
+vec2.random = function (out, scale) {
+    scale = scale || 1.0;
+    var r = GLMAT_RANDOM() * 2.0 * Math.PI;
+    out[0] = Math.cos(r) * scale;
+    out[1] = Math.sin(r) * scale;
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat2 = function(out, a, m) {
+    var x = a[0],
+        y = a[1];
+    out[0] = m[0] * x + m[2] * y;
+    out[1] = m[1] * x + m[3] * y;
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat2d
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2d} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat2d = function(out, a, m) {
+    var x = a[0],
+        y = a[1];
+    out[0] = m[0] * x + m[2] * y + m[4];
+    out[1] = m[1] * x + m[3] * y + m[5];
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat3
+ * 3rd vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat3} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat3 = function(out, a, m) {
+    var x = a[0],
+        y = a[1];
+    out[0] = m[0] * x + m[3] * y + m[6];
+    out[1] = m[1] * x + m[4] * y + m[7];
+    return out;
+};
+
+/**
+ * Transforms the vec2 with a mat4
+ * 3rd vector component is implicitly '0'
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat4 = function(out, a, m) {
+    var x = a[0], 
+        y = a[1];
+    out[0] = m[0] * x + m[4] * y + m[12];
+    out[1] = m[1] * x + m[5] * y + m[13];
+    return out;
+};
+
+/**
+ * Perform some operation over an array of vec2s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec2.forEach = (function() {
+    var vec = vec2.create();
+
+    return function(a, stride, offset, count, fn, arg) {
+        var i, l;
+        if(!stride) {
+            stride = 2;
+        }
+
+        if(!offset) {
+            offset = 0;
+        }
+        
+        if(count) {
+            l = Math.min((count * stride) + offset, a.length);
+        } else {
+            l = a.length;
+        }
+
+        for(i = offset; i < l; i += stride) {
+            vec[0] = a[i]; vec[1] = a[i+1];
+            fn(vec, vec, arg);
+            a[i] = vec[0]; a[i+1] = vec[1];
+        }
+        
+        return a;
+    };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec2} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec2.str = function (a) {
+    return 'vec2(' + a[0] + ', ' + a[1] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.vec2 = vec2;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 3 Dimensional Vector
+ * @name vec3
+ */
+
+var vec3 = {};
+
+/**
+ * Creates a new, empty vec3
+ *
+ * @returns {vec3} a new 3D vector
+ */
+vec3.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(3);
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    return out;
+};
+
+/**
+ * Creates a new vec3 initialized with values from an existing vector
+ *
+ * @param {vec3} a vector to clone
+ * @returns {vec3} a new 3D vector
+ */
+vec3.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(3);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    return out;
+};
+
+/**
+ * Creates a new vec3 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} a new 3D vector
+ */
+vec3.fromValues = function(x, y, z) {
+    var out = new GLMAT_ARRAY_TYPE(3);
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    return out;
+};
+
+/**
+ * Copy the values from one vec3 to another
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the source vector
+ * @returns {vec3} out
+ */
+vec3.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    return out;
+};
+
+/**
+ * Set the components of a vec3 to the given values
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} out
+ */
+vec3.set = function(out, x, y, z) {
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    return out;
+};
+
+/**
+ * Adds two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.add = function(out, a, b) {
+    out[0] = a[0] + b[0];
+    out[1] = a[1] + b[1];
+    out[2] = a[2] + b[2];
+    return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.subtract = function(out, a, b) {
+    out[0] = a[0] - b[0];
+    out[1] = a[1] - b[1];
+    out[2] = a[2] - b[2];
+    return out;
+};
+
+/**
+ * Alias for {@link vec3.subtract}
+ * @function
+ */
+vec3.sub = vec3.subtract;
+
+/**
+ * Multiplies two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.multiply = function(out, a, b) {
+    out[0] = a[0] * b[0];
+    out[1] = a[1] * b[1];
+    out[2] = a[2] * b[2];
+    return out;
+};
+
+/**
+ * Alias for {@link vec3.multiply}
+ * @function
+ */
+vec3.mul = vec3.multiply;
+
+/**
+ * Divides two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.divide = function(out, a, b) {
+    out[0] = a[0] / b[0];
+    out[1] = a[1] / b[1];
+    out[2] = a[2] / b[2];
+    return out;
+};
+
+/**
+ * Alias for {@link vec3.divide}
+ * @function
+ */
+vec3.div = vec3.divide;
+
+/**
+ * Returns the minimum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.min = function(out, a, b) {
+    out[0] = Math.min(a[0], b[0]);
+    out[1] = Math.min(a[1], b[1]);
+    out[2] = Math.min(a[2], b[2]);
+    return out;
+};
+
+/**
+ * Returns the maximum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.max = function(out, a, b) {
+    out[0] = Math.max(a[0], b[0]);
+    out[1] = Math.max(a[1], b[1]);
+    out[2] = Math.max(a[2], b[2]);
+    return out;
+};
+
+/**
+ * Scales a vec3 by a scalar number
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec3} out
+ */
+vec3.scale = function(out, a, b) {
+    out[0] = a[0] * b;
+    out[1] = a[1] * b;
+    out[2] = a[2] * b;
+    return out;
+};
+
+/**
+ * Adds two vec3's after scaling the second operand by a scalar value
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec3} out
+ */
+vec3.scaleAndAdd = function(out, a, b, scale) {
+    out[0] = a[0] + (b[0] * scale);
+    out[1] = a[1] + (b[1] * scale);
+    out[2] = a[2] + (b[2] * scale);
+    return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec3.distance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2];
+    return Math.sqrt(x*x + y*y + z*z);
+};
+
+/**
+ * Alias for {@link vec3.distance}
+ * @function
+ */
+vec3.dist = vec3.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec3.squaredDistance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2];
+    return x*x + y*y + z*z;
+};
+
+/**
+ * Alias for {@link vec3.squaredDistance}
+ * @function
+ */
+vec3.sqrDist = vec3.squaredDistance;
+
+/**
+ * Calculates the length of a vec3
+ *
+ * @param {vec3} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec3.length = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2];
+    return Math.sqrt(x*x + y*y + z*z);
+};
+
+/**
+ * Alias for {@link vec3.length}
+ * @function
+ */
+vec3.len = vec3.length;
+
+/**
+ * Calculates the squared length of a vec3
+ *
+ * @param {vec3} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec3.squaredLength = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2];
+    return x*x + y*y + z*z;
+};
+
+/**
+ * Alias for {@link vec3.squaredLength}
+ * @function
+ */
+vec3.sqrLen = vec3.squaredLength;
+
+/**
+ * Negates the components of a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to negate
+ * @returns {vec3} out
+ */
+vec3.negate = function(out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    return out;
+};
+
+/**
+ * Normalize a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to normalize
+ * @returns {vec3} out
+ */
+vec3.normalize = function(out, a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2];
+    var len = x*x + y*y + z*z;
+    if (len > 0) {
+        //TODO: evaluate use of glm_invsqrt here?
+        len = 1 / Math.sqrt(len);
+        out[0] = a[0] * len;
+        out[1] = a[1] * len;
+        out[2] = a[2] * len;
+    }
+    return out;
+};
+
+/**
+ * Calculates the dot product of two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec3.dot = function (a, b) {
+    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+};
+
+/**
+ * Computes the cross product of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.cross = function(out, a, b) {
+    var ax = a[0], ay = a[1], az = a[2],
+        bx = b[0], by = b[1], bz = b[2];
+
+    out[0] = ay * bz - az * by;
+    out[1] = az * bx - ax * bz;
+    out[2] = ax * by - ay * bx;
+    return out;
+};
+
+/**
+ * Performs a linear interpolation between two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+vec3.lerp = function (out, a, b, t) {
+    var ax = a[0],
+        ay = a[1],
+        az = a[2];
+    out[0] = ax + t * (b[0] - ax);
+    out[1] = ay + t * (b[1] - ay);
+    out[2] = az + t * (b[2] - az);
+    return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec3} out
+ */
+vec3.random = function (out, scale) {
+    scale = scale || 1.0;
+
+    var r = GLMAT_RANDOM() * 2.0 * Math.PI;
+    var z = (GLMAT_RANDOM() * 2.0) - 1.0;
+    var zScale = Math.sqrt(1.0-z*z) * scale;
+
+    out[0] = Math.cos(r) * zScale;
+    out[1] = Math.sin(r) * zScale;
+    out[2] = z * scale;
+    return out;
+};
+
+/**
+ * Transforms the vec3 with a mat4.
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec3} out
+ */
+vec3.transformMat4 = function(out, a, m) {
+    var x = a[0], y = a[1], z = a[2];
+    out[0] = m[0] * x + m[4] * y + m[8] * z + m[12];
+    out[1] = m[1] * x + m[5] * y + m[9] * z + m[13];
+    out[2] = m[2] * x + m[6] * y + m[10] * z + m[14];
+    return out;
+};
+
+/**
+ * Transforms the vec3 with a mat3.
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m the 3x3 matrix to transform with
+ * @returns {vec3} out
+ */
+vec3.transformMat3 = function(out, a, m) {
+    var x = a[0], y = a[1], z = a[2];
+    out[0] = x * m[0] + y * m[3] + z * m[6];
+    out[1] = x * m[1] + y * m[4] + z * m[7];
+    out[2] = x * m[2] + y * m[5] + z * m[8];
+    return out;
+};
+
+/**
+ * Transforms the vec3 with a quat
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec3} out
+ */
+vec3.transformQuat = function(out, a, q) {
+    // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations
+
+    var x = a[0], y = a[1], z = a[2],
+        qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+        // calculate quat * vec
+        ix = qw * x + qy * z - qz * y,
+        iy = qw * y + qz * x - qx * z,
+        iz = qw * z + qx * y - qy * x,
+        iw = -qx * x - qy * y - qz * z;
+
+    // calculate result * inverse quat
+    out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+    out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+    out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+    return out;
+};
+
+/*
+* Rotate a 3D vector around the x-axis
+* @param {vec3} out The receiving vec3
+* @param {vec3} a The vec3 point to rotate
+* @param {vec3} b The origin of the rotation
+* @param {Number} c The angle of rotation
+* @returns {vec3} out
+*/
+vec3.rotateX = function(out, a, b, c){
+   var p = [], r=[];
+	  //Translate point to the origin
+	  p[0] = a[0] - b[0];
+	  p[1] = a[1] - b[1];
+  	p[2] = a[2] - b[2];
+
+	  //perform rotation
+	  r[0] = p[0];
+	  r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c);
+	  r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c);
+
+	  //translate to correct position
+	  out[0] = r[0] + b[0];
+	  out[1] = r[1] + b[1];
+	  out[2] = r[2] + b[2];
+
+  	return out;
+};
+
+/*
+* Rotate a 3D vector around the y-axis
+* @param {vec3} out The receiving vec3
+* @param {vec3} a The vec3 point to rotate
+* @param {vec3} b The origin of the rotation
+* @param {Number} c The angle of rotation
+* @returns {vec3} out
+*/
+vec3.rotateY = function(out, a, b, c){
+  	var p = [], r=[];
+  	//Translate point to the origin
+  	p[0] = a[0] - b[0];
+  	p[1] = a[1] - b[1];
+  	p[2] = a[2] - b[2];
+  
+  	//perform rotation
+  	r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c);
+  	r[1] = p[1];
+  	r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c);
+  
+  	//translate to correct position
+  	out[0] = r[0] + b[0];
+  	out[1] = r[1] + b[1];
+  	out[2] = r[2] + b[2];
+  
+  	return out;
+};
+
+/*
+* Rotate a 3D vector around the z-axis
+* @param {vec3} out The receiving vec3
+* @param {vec3} a The vec3 point to rotate
+* @param {vec3} b The origin of the rotation
+* @param {Number} c The angle of rotation
+* @returns {vec3} out
+*/
+vec3.rotateZ = function(out, a, b, c){
+  	var p = [], r=[];
+  	//Translate point to the origin
+  	p[0] = a[0] - b[0];
+  	p[1] = a[1] - b[1];
+  	p[2] = a[2] - b[2];
+  
+  	//perform rotation
+  	r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c);
+  	r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c);
+  	r[2] = p[2];
+  
+  	//translate to correct position
+  	out[0] = r[0] + b[0];
+  	out[1] = r[1] + b[1];
+  	out[2] = r[2] + b[2];
+  
+  	return out;
+};
+
+/**
+ * Perform some operation over an array of vec3s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec3.forEach = (function() {
+    var vec = vec3.create();
+
+    return function(a, stride, offset, count, fn, arg) {
+        var i, l;
+        if(!stride) {
+            stride = 3;
+        }
+
+        if(!offset) {
+            offset = 0;
+        }
+        
+        if(count) {
+            l = Math.min((count * stride) + offset, a.length);
+        } else {
+            l = a.length;
+        }
+
+        for(i = offset; i < l; i += stride) {
+            vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2];
+            fn(vec, vec, arg);
+            a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2];
+        }
+        
+        return a;
+    };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec3} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec3.str = function (a) {
+    return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.vec3 = vec3;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 4 Dimensional Vector
+ * @name vec4
+ */
+
+var vec4 = {};
+
+/**
+ * Creates a new, empty vec4
+ *
+ * @returns {vec4} a new 4D vector
+ */
+vec4.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    return out;
+};
+
+/**
+ * Creates a new vec4 initialized with values from an existing vector
+ *
+ * @param {vec4} a vector to clone
+ * @returns {vec4} a new 4D vector
+ */
+vec4.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Creates a new vec4 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} a new 4D vector
+ */
+vec4.fromValues = function(x, y, z, w) {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    out[3] = w;
+    return out;
+};
+
+/**
+ * Copy the values from one vec4 to another
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the source vector
+ * @returns {vec4} out
+ */
+vec4.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Set the components of a vec4 to the given values
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} out
+ */
+vec4.set = function(out, x, y, z, w) {
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    out[3] = w;
+    return out;
+};
+
+/**
+ * Adds two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.add = function(out, a, b) {
+    out[0] = a[0] + b[0];
+    out[1] = a[1] + b[1];
+    out[2] = a[2] + b[2];
+    out[3] = a[3] + b[3];
+    return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.subtract = function(out, a, b) {
+    out[0] = a[0] - b[0];
+    out[1] = a[1] - b[1];
+    out[2] = a[2] - b[2];
+    out[3] = a[3] - b[3];
+    return out;
+};
+
+/**
+ * Alias for {@link vec4.subtract}
+ * @function
+ */
+vec4.sub = vec4.subtract;
+
+/**
+ * Multiplies two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.multiply = function(out, a, b) {
+    out[0] = a[0] * b[0];
+    out[1] = a[1] * b[1];
+    out[2] = a[2] * b[2];
+    out[3] = a[3] * b[3];
+    return out;
+};
+
+/**
+ * Alias for {@link vec4.multiply}
+ * @function
+ */
+vec4.mul = vec4.multiply;
+
+/**
+ * Divides two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.divide = function(out, a, b) {
+    out[0] = a[0] / b[0];
+    out[1] = a[1] / b[1];
+    out[2] = a[2] / b[2];
+    out[3] = a[3] / b[3];
+    return out;
+};
+
+/**
+ * Alias for {@link vec4.divide}
+ * @function
+ */
+vec4.div = vec4.divide;
+
+/**
+ * Returns the minimum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.min = function(out, a, b) {
+    out[0] = Math.min(a[0], b[0]);
+    out[1] = Math.min(a[1], b[1]);
+    out[2] = Math.min(a[2], b[2]);
+    out[3] = Math.min(a[3], b[3]);
+    return out;
+};
+
+/**
+ * Returns the maximum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.max = function(out, a, b) {
+    out[0] = Math.max(a[0], b[0]);
+    out[1] = Math.max(a[1], b[1]);
+    out[2] = Math.max(a[2], b[2]);
+    out[3] = Math.max(a[3], b[3]);
+    return out;
+};
+
+/**
+ * Scales a vec4 by a scalar number
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec4} out
+ */
+vec4.scale = function(out, a, b) {
+    out[0] = a[0] * b;
+    out[1] = a[1] * b;
+    out[2] = a[2] * b;
+    out[3] = a[3] * b;
+    return out;
+};
+
+/**
+ * Adds two vec4's after scaling the second operand by a scalar value
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec4} out
+ */
+vec4.scaleAndAdd = function(out, a, b, scale) {
+    out[0] = a[0] + (b[0] * scale);
+    out[1] = a[1] + (b[1] * scale);
+    out[2] = a[2] + (b[2] * scale);
+    out[3] = a[3] + (b[3] * scale);
+    return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec4.distance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2],
+        w = b[3] - a[3];
+    return Math.sqrt(x*x + y*y + z*z + w*w);
+};
+
+/**
+ * Alias for {@link vec4.distance}
+ * @function
+ */
+vec4.dist = vec4.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec4.squaredDistance = function(a, b) {
+    var x = b[0] - a[0],
+        y = b[1] - a[1],
+        z = b[2] - a[2],
+        w = b[3] - a[3];
+    return x*x + y*y + z*z + w*w;
+};
+
+/**
+ * Alias for {@link vec4.squaredDistance}
+ * @function
+ */
+vec4.sqrDist = vec4.squaredDistance;
+
+/**
+ * Calculates the length of a vec4
+ *
+ * @param {vec4} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec4.length = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2],
+        w = a[3];
+    return Math.sqrt(x*x + y*y + z*z + w*w);
+};
+
+/**
+ * Alias for {@link vec4.length}
+ * @function
+ */
+vec4.len = vec4.length;
+
+/**
+ * Calculates the squared length of a vec4
+ *
+ * @param {vec4} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec4.squaredLength = function (a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2],
+        w = a[3];
+    return x*x + y*y + z*z + w*w;
+};
+
+/**
+ * Alias for {@link vec4.squaredLength}
+ * @function
+ */
+vec4.sqrLen = vec4.squaredLength;
+
+/**
+ * Negates the components of a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to negate
+ * @returns {vec4} out
+ */
+vec4.negate = function(out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    out[3] = -a[3];
+    return out;
+};
+
+/**
+ * Normalize a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to normalize
+ * @returns {vec4} out
+ */
+vec4.normalize = function(out, a) {
+    var x = a[0],
+        y = a[1],
+        z = a[2],
+        w = a[3];
+    var len = x*x + y*y + z*z + w*w;
+    if (len > 0) {
+        len = 1 / Math.sqrt(len);
+        out[0] = a[0] * len;
+        out[1] = a[1] * len;
+        out[2] = a[2] * len;
+        out[3] = a[3] * len;
+    }
+    return out;
+};
+
+/**
+ * Calculates the dot product of two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec4.dot = function (a, b) {
+    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
+};
+
+/**
+ * Performs a linear interpolation between two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec4} out
+ */
+vec4.lerp = function (out, a, b, t) {
+    var ax = a[0],
+        ay = a[1],
+        az = a[2],
+        aw = a[3];
+    out[0] = ax + t * (b[0] - ax);
+    out[1] = ay + t * (b[1] - ay);
+    out[2] = az + t * (b[2] - az);
+    out[3] = aw + t * (b[3] - aw);
+    return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec4} out
+ */
+vec4.random = function (out, scale) {
+    scale = scale || 1.0;
+
+    //TODO: This is a pretty awful way of doing this. Find something better.
+    out[0] = GLMAT_RANDOM();
+    out[1] = GLMAT_RANDOM();
+    out[2] = GLMAT_RANDOM();
+    out[3] = GLMAT_RANDOM();
+    vec4.normalize(out, out);
+    vec4.scale(out, out, scale);
+    return out;
+};
+
+/**
+ * Transforms the vec4 with a mat4.
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec4} out
+ */
+vec4.transformMat4 = function(out, a, m) {
+    var x = a[0], y = a[1], z = a[2], w = a[3];
+    out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
+    out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
+    out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
+    out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
+    return out;
+};
+
+/**
+ * Transforms the vec4 with a quat
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec4} out
+ */
+vec4.transformQuat = function(out, a, q) {
+    var x = a[0], y = a[1], z = a[2],
+        qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+        // calculate quat * vec
+        ix = qw * x + qy * z - qz * y,
+        iy = qw * y + qz * x - qx * z,
+        iz = qw * z + qx * y - qy * x,
+        iw = -qx * x - qy * y - qz * z;
+
+    // calculate result * inverse quat
+    out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+    out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+    out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+    return out;
+};
+
+/**
+ * Perform some operation over an array of vec4s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec4.forEach = (function() {
+    var vec = vec4.create();
+
+    return function(a, stride, offset, count, fn, arg) {
+        var i, l;
+        if(!stride) {
+            stride = 4;
+        }
+
+        if(!offset) {
+            offset = 0;
+        }
+        
+        if(count) {
+            l = Math.min((count * stride) + offset, a.length);
+        } else {
+            l = a.length;
+        }
+
+        for(i = offset; i < l; i += stride) {
+            vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3];
+            fn(vec, vec, arg);
+            a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3];
+        }
+        
+        return a;
+    };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec4} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec4.str = function (a) {
+    return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.vec4 = vec4;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 2x2 Matrix
+ * @name mat2
+ */
+
+var mat2 = {};
+
+/**
+ * Creates a new identity mat2
+ *
+ * @returns {mat2} a new 2x2 matrix
+ */
+mat2.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Creates a new mat2 initialized with values from an existing matrix
+ *
+ * @param {mat2} a matrix to clone
+ * @returns {mat2} a new 2x2 matrix
+ */
+mat2.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Copy the values from one mat2 to another
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Set a mat2 to the identity matrix
+ *
+ * @param {mat2} out the receiving matrix
+ * @returns {mat2} out
+ */
+mat2.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Transpose the values of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.transpose = function(out, a) {
+    // If we are transposing ourselves we can skip a few steps but have to cache some values
+    if (out === a) {
+        var a1 = a[1];
+        out[1] = a[2];
+        out[2] = a1;
+    } else {
+        out[0] = a[0];
+        out[1] = a[2];
+        out[2] = a[1];
+        out[3] = a[3];
+    }
+    
+    return out;
+};
+
+/**
+ * Inverts a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.invert = function(out, a) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+
+        // Calculate the determinant
+        det = a0 * a3 - a2 * a1;
+
+    if (!det) {
+        return null;
+    }
+    det = 1.0 / det;
+    
+    out[0] =  a3 * det;
+    out[1] = -a1 * det;
+    out[2] = -a2 * det;
+    out[3] =  a0 * det;
+
+    return out;
+};
+
+/**
+ * Calculates the adjugate of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.adjoint = function(out, a) {
+    // Caching this value is nessecary if out == a
+    var a0 = a[0];
+    out[0] =  a[3];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    out[3] =  a0;
+
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat2
+ *
+ * @param {mat2} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat2.determinant = function (a) {
+    return a[0] * a[3] - a[2] * a[1];
+};
+
+/**
+ * Multiplies two mat2's
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the first operand
+ * @param {mat2} b the second operand
+ * @returns {mat2} out
+ */
+mat2.multiply = function (out, a, b) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3];
+    var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+    out[0] = a0 * b0 + a2 * b1;
+    out[1] = a1 * b0 + a3 * b1;
+    out[2] = a0 * b2 + a2 * b3;
+    out[3] = a1 * b2 + a3 * b3;
+    return out;
+};
+
+/**
+ * Alias for {@link mat2.multiply}
+ * @function
+ */
+mat2.mul = mat2.multiply;
+
+/**
+ * Rotates a mat2 by the given angle
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2} out
+ */
+mat2.rotate = function (out, a, rad) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+        s = Math.sin(rad),
+        c = Math.cos(rad);
+    out[0] = a0 *  c + a2 * s;
+    out[1] = a1 *  c + a3 * s;
+    out[2] = a0 * -s + a2 * c;
+    out[3] = a1 * -s + a3 * c;
+    return out;
+};
+
+/**
+ * Scales the mat2 by the dimensions in the given vec2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2} out
+ **/
+mat2.scale = function(out, a, v) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+        v0 = v[0], v1 = v[1];
+    out[0] = a0 * v0;
+    out[1] = a1 * v0;
+    out[2] = a2 * v1;
+    out[3] = a3 * v1;
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat2
+ *
+ * @param {mat2} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat2.str = function (a) {
+    return 'mat2(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat2
+ *
+ * @param {mat2} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat2.frob = function (a) {
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2)))
+};
+
+/**
+ * Returns L, D and U matrices (Lower triangular, Diagonal and Upper triangular) by factorizing the input matrix
+ * @param {mat2} L the lower triangular matrix 
+ * @param {mat2} D the diagonal matrix 
+ * @param {mat2} U the upper triangular matrix 
+ * @param {mat2} a the input matrix to factorize
+ */
+
+mat2.LDU = function (L, D, U, a) { 
+    L[2] = a[2]/a[0]; 
+    U[0] = a[0]; 
+    U[1] = a[1]; 
+    U[3] = a[3] - L[2] * U[1]; 
+    return [L, D, U];       
+}; 
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat2 = mat2;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 2x3 Matrix
+ * @name mat2d
+ * 
+ * @description 
+ * A mat2d contains six elements defined as:
+ * <pre>
+ * [a, c, tx,
+ *  b, d, ty]
+ * </pre>
+ * This is a short form for the 3x3 matrix:
+ * <pre>
+ * [a, c, tx,
+ *  b, d, ty,
+ *  0, 0, 1]
+ * </pre>
+ * The last row is ignored so the array is shorter and operations are faster.
+ */
+
+var mat2d = {};
+
+/**
+ * Creates a new identity mat2d
+ *
+ * @returns {mat2d} a new 2x3 matrix
+ */
+mat2d.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(6);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    out[4] = 0;
+    out[5] = 0;
+    return out;
+};
+
+/**
+ * Creates a new mat2d initialized with values from an existing matrix
+ *
+ * @param {mat2d} a matrix to clone
+ * @returns {mat2d} a new 2x3 matrix
+ */
+mat2d.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(6);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    return out;
+};
+
+/**
+ * Copy the values from one mat2d to another
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+mat2d.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    return out;
+};
+
+/**
+ * Set a mat2d to the identity matrix
+ *
+ * @param {mat2d} out the receiving matrix
+ * @returns {mat2d} out
+ */
+mat2d.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    out[4] = 0;
+    out[5] = 0;
+    return out;
+};
+
+/**
+ * Inverts a mat2d
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+mat2d.invert = function(out, a) {
+    var aa = a[0], ab = a[1], ac = a[2], ad = a[3],
+        atx = a[4], aty = a[5];
+
+    var det = aa * ad - ab * ac;
+    if(!det){
+        return null;
+    }
+    det = 1.0 / det;
+
+    out[0] = ad * det;
+    out[1] = -ab * det;
+    out[2] = -ac * det;
+    out[3] = aa * det;
+    out[4] = (ac * aty - ad * atx) * det;
+    out[5] = (ab * atx - aa * aty) * det;
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat2d
+ *
+ * @param {mat2d} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat2d.determinant = function (a) {
+    return a[0] * a[3] - a[1] * a[2];
+};
+
+/**
+ * Multiplies two mat2d's
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the first operand
+ * @param {mat2d} b the second operand
+ * @returns {mat2d} out
+ */
+mat2d.multiply = function (out, a, b) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5];
+    out[0] = a0 * b0 + a2 * b1;
+    out[1] = a1 * b0 + a3 * b1;
+    out[2] = a0 * b2 + a2 * b3;
+    out[3] = a1 * b2 + a3 * b3;
+    out[4] = a0 * b4 + a2 * b5 + a4;
+    out[5] = a1 * b4 + a3 * b5 + a5;
+    return out;
+};
+
+/**
+ * Alias for {@link mat2d.multiply}
+ * @function
+ */
+mat2d.mul = mat2d.multiply;
+
+
+/**
+ * Rotates a mat2d by the given angle
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2d} out
+ */
+mat2d.rotate = function (out, a, rad) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        s = Math.sin(rad),
+        c = Math.cos(rad);
+    out[0] = a0 *  c + a2 * s;
+    out[1] = a1 *  c + a3 * s;
+    out[2] = a0 * -s + a2 * c;
+    out[3] = a1 * -s + a3 * c;
+    out[4] = a4;
+    out[5] = a5;
+    return out;
+};
+
+/**
+ * Scales the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2d} out
+ **/
+mat2d.scale = function(out, a, v) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        v0 = v[0], v1 = v[1];
+    out[0] = a0 * v0;
+    out[1] = a1 * v0;
+    out[2] = a2 * v1;
+    out[3] = a3 * v1;
+    out[4] = a4;
+    out[5] = a5;
+    return out;
+};
+
+/**
+ * Translates the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to translate the matrix by
+ * @returns {mat2d} out
+ **/
+mat2d.translate = function(out, a, v) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+        v0 = v[0], v1 = v[1];
+    out[0] = a0;
+    out[1] = a1;
+    out[2] = a2;
+    out[3] = a3;
+    out[4] = a0 * v0 + a2 * v1 + a4;
+    out[5] = a1 * v0 + a3 * v1 + a5;
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat2d
+ *
+ * @param {mat2d} a matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat2d.str = function (a) {
+    return 'mat2d(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + 
+                    a[3] + ', ' + a[4] + ', ' + a[5] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat2d
+ *
+ * @param {mat2d} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat2d.frob = function (a) { 
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + 1))
+}; 
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat2d = mat2d;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 3x3 Matrix
+ * @name mat3
+ */
+
+var mat3 = {};
+
+/**
+ * Creates a new identity mat3
+ *
+ * @returns {mat3} a new 3x3 matrix
+ */
+mat3.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(9);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 1;
+    out[5] = 0;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 1;
+    return out;
+};
+
+/**
+ * Copies the upper-left 3x3 values into the given mat3.
+ *
+ * @param {mat3} out the receiving 3x3 matrix
+ * @param {mat4} a   the source 4x4 matrix
+ * @returns {mat3} out
+ */
+mat3.fromMat4 = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[4];
+    out[4] = a[5];
+    out[5] = a[6];
+    out[6] = a[8];
+    out[7] = a[9];
+    out[8] = a[10];
+    return out;
+};
+
+/**
+ * Creates a new mat3 initialized with values from an existing matrix
+ *
+ * @param {mat3} a matrix to clone
+ * @returns {mat3} a new 3x3 matrix
+ */
+mat3.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(9);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    return out;
+};
+
+/**
+ * Copy the values from one mat3 to another
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    return out;
+};
+
+/**
+ * Set a mat3 to the identity matrix
+ *
+ * @param {mat3} out the receiving matrix
+ * @returns {mat3} out
+ */
+mat3.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 1;
+    out[5] = 0;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 1;
+    return out;
+};
+
+/**
+ * Transpose the values of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.transpose = function(out, a) {
+    // If we are transposing ourselves we can skip a few steps but have to cache some values
+    if (out === a) {
+        var a01 = a[1], a02 = a[2], a12 = a[5];
+        out[1] = a[3];
+        out[2] = a[6];
+        out[3] = a01;
+        out[5] = a[7];
+        out[6] = a02;
+        out[7] = a12;
+    } else {
+        out[0] = a[0];
+        out[1] = a[3];
+        out[2] = a[6];
+        out[3] = a[1];
+        out[4] = a[4];
+        out[5] = a[7];
+        out[6] = a[2];
+        out[7] = a[5];
+        out[8] = a[8];
+    }
+    
+    return out;
+};
+
+/**
+ * Inverts a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.invert = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+
+        b01 = a22 * a11 - a12 * a21,
+        b11 = -a22 * a10 + a12 * a20,
+        b21 = a21 * a10 - a11 * a20,
+
+        // Calculate the determinant
+        det = a00 * b01 + a01 * b11 + a02 * b21;
+
+    if (!det) { 
+        return null; 
+    }
+    det = 1.0 / det;
+
+    out[0] = b01 * det;
+    out[1] = (-a22 * a01 + a02 * a21) * det;
+    out[2] = (a12 * a01 - a02 * a11) * det;
+    out[3] = b11 * det;
+    out[4] = (a22 * a00 - a02 * a20) * det;
+    out[5] = (-a12 * a00 + a02 * a10) * det;
+    out[6] = b21 * det;
+    out[7] = (-a21 * a00 + a01 * a20) * det;
+    out[8] = (a11 * a00 - a01 * a10) * det;
+    return out;
+};
+
+/**
+ * Calculates the adjugate of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.adjoint = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8];
+
+    out[0] = (a11 * a22 - a12 * a21);
+    out[1] = (a02 * a21 - a01 * a22);
+    out[2] = (a01 * a12 - a02 * a11);
+    out[3] = (a12 * a20 - a10 * a22);
+    out[4] = (a00 * a22 - a02 * a20);
+    out[5] = (a02 * a10 - a00 * a12);
+    out[6] = (a10 * a21 - a11 * a20);
+    out[7] = (a01 * a20 - a00 * a21);
+    out[8] = (a00 * a11 - a01 * a10);
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat3
+ *
+ * @param {mat3} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat3.determinant = function (a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8];
+
+    return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20);
+};
+
+/**
+ * Multiplies two mat3's
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the first operand
+ * @param {mat3} b the second operand
+ * @returns {mat3} out
+ */
+mat3.multiply = function (out, a, b) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+
+        b00 = b[0], b01 = b[1], b02 = b[2],
+        b10 = b[3], b11 = b[4], b12 = b[5],
+        b20 = b[6], b21 = b[7], b22 = b[8];
+
+    out[0] = b00 * a00 + b01 * a10 + b02 * a20;
+    out[1] = b00 * a01 + b01 * a11 + b02 * a21;
+    out[2] = b00 * a02 + b01 * a12 + b02 * a22;
+
+    out[3] = b10 * a00 + b11 * a10 + b12 * a20;
+    out[4] = b10 * a01 + b11 * a11 + b12 * a21;
+    out[5] = b10 * a02 + b11 * a12 + b12 * a22;
+
+    out[6] = b20 * a00 + b21 * a10 + b22 * a20;
+    out[7] = b20 * a01 + b21 * a11 + b22 * a21;
+    out[8] = b20 * a02 + b21 * a12 + b22 * a22;
+    return out;
+};
+
+/**
+ * Alias for {@link mat3.multiply}
+ * @function
+ */
+mat3.mul = mat3.multiply;
+
+/**
+ * Translate a mat3 by the given vector
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to translate
+ * @param {vec2} v vector to translate by
+ * @returns {mat3} out
+ */
+mat3.translate = function(out, a, v) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+        x = v[0], y = v[1];
+
+    out[0] = a00;
+    out[1] = a01;
+    out[2] = a02;
+
+    out[3] = a10;
+    out[4] = a11;
+    out[5] = a12;
+
+    out[6] = x * a00 + y * a10 + a20;
+    out[7] = x * a01 + y * a11 + a21;
+    out[8] = x * a02 + y * a12 + a22;
+    return out;
+};
+
+/**
+ * Rotates a mat3 by the given angle
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat3} out
+ */
+mat3.rotate = function (out, a, rad) {
+    var a00 = a[0], a01 = a[1], a02 = a[2],
+        a10 = a[3], a11 = a[4], a12 = a[5],
+        a20 = a[6], a21 = a[7], a22 = a[8],
+
+        s = Math.sin(rad),
+        c = Math.cos(rad);
+
+    out[0] = c * a00 + s * a10;
+    out[1] = c * a01 + s * a11;
+    out[2] = c * a02 + s * a12;
+
+    out[3] = c * a10 - s * a00;
+    out[4] = c * a11 - s * a01;
+    out[5] = c * a12 - s * a02;
+
+    out[6] = a20;
+    out[7] = a21;
+    out[8] = a22;
+    return out;
+};
+
+/**
+ * Scales the mat3 by the dimensions in the given vec2
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat3} out
+ **/
+mat3.scale = function(out, a, v) {
+    var x = v[0], y = v[1];
+
+    out[0] = x * a[0];
+    out[1] = x * a[1];
+    out[2] = x * a[2];
+
+    out[3] = y * a[3];
+    out[4] = y * a[4];
+    out[5] = y * a[5];
+
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    return out;
+};
+
+/**
+ * Copies the values from a mat2d into a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat2d} a the matrix to copy
+ * @returns {mat3} out
+ **/
+mat3.fromMat2d = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = 0;
+
+    out[3] = a[2];
+    out[4] = a[3];
+    out[5] = 0;
+
+    out[6] = a[4];
+    out[7] = a[5];
+    out[8] = 1;
+    return out;
+};
+
+/**
+* Calculates a 3x3 matrix from the given quaternion
+*
+* @param {mat3} out mat3 receiving operation result
+* @param {quat} q Quaternion to create matrix from
+*
+* @returns {mat3} out
+*/
+mat3.fromQuat = function (out, q) {
+    var x = q[0], y = q[1], z = q[2], w = q[3],
+        x2 = x + x,
+        y2 = y + y,
+        z2 = z + z,
+
+        xx = x * x2,
+        yx = y * x2,
+        yy = y * y2,
+        zx = z * x2,
+        zy = z * y2,
+        zz = z * z2,
+        wx = w * x2,
+        wy = w * y2,
+        wz = w * z2;
+
+    out[0] = 1 - yy - zz;
+    out[3] = yx - wz;
+    out[6] = zx + wy;
+
+    out[1] = yx + wz;
+    out[4] = 1 - xx - zz;
+    out[7] = zy - wx;
+
+    out[2] = zx - wy;
+    out[5] = zy + wx;
+    out[8] = 1 - xx - yy;
+
+    return out;
+};
+
+/**
+* Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix
+*
+* @param {mat3} out mat3 receiving operation result
+* @param {mat4} a Mat4 to derive the normal matrix from
+*
+* @returns {mat3} out
+*/
+mat3.normalFromMat4 = function (out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+        b00 = a00 * a11 - a01 * a10,
+        b01 = a00 * a12 - a02 * a10,
+        b02 = a00 * a13 - a03 * a10,
+        b03 = a01 * a12 - a02 * a11,
+        b04 = a01 * a13 - a03 * a11,
+        b05 = a02 * a13 - a03 * a12,
+        b06 = a20 * a31 - a21 * a30,
+        b07 = a20 * a32 - a22 * a30,
+        b08 = a20 * a33 - a23 * a30,
+        b09 = a21 * a32 - a22 * a31,
+        b10 = a21 * a33 - a23 * a31,
+        b11 = a22 * a33 - a23 * a32,
+
+        // Calculate the determinant
+        det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+    if (!det) { 
+        return null; 
+    }
+    det = 1.0 / det;
+
+    out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+    out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+    out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+
+    out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+    out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+    out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+
+    out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+    out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+    out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat3
+ *
+ * @param {mat3} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat3.str = function (a) {
+    return 'mat3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + 
+                    a[3] + ', ' + a[4] + ', ' + a[5] + ', ' + 
+                    a[6] + ', ' + a[7] + ', ' + a[8] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat3
+ *
+ * @param {mat3} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat3.frob = function (a) {
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2)))
+};
+
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat3 = mat3;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class 4x4 Matrix
+ * @name mat4
+ */
+
+var mat4 = {};
+
+/**
+ * Creates a new identity mat4
+ *
+ * @returns {mat4} a new 4x4 matrix
+ */
+mat4.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(16);
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = 1;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = 1;
+    out[11] = 0;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = 0;
+    out[15] = 1;
+    return out;
+};
+
+/**
+ * Creates a new mat4 initialized with values from an existing matrix
+ *
+ * @param {mat4} a matrix to clone
+ * @returns {mat4} a new 4x4 matrix
+ */
+mat4.clone = function(a) {
+    var out = new GLMAT_ARRAY_TYPE(16);
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    out[9] = a[9];
+    out[10] = a[10];
+    out[11] = a[11];
+    out[12] = a[12];
+    out[13] = a[13];
+    out[14] = a[14];
+    out[15] = a[15];
+    return out;
+};
+
+/**
+ * Copy the values from one mat4 to another
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.copy = function(out, a) {
+    out[0] = a[0];
+    out[1] = a[1];
+    out[2] = a[2];
+    out[3] = a[3];
+    out[4] = a[4];
+    out[5] = a[5];
+    out[6] = a[6];
+    out[7] = a[7];
+    out[8] = a[8];
+    out[9] = a[9];
+    out[10] = a[10];
+    out[11] = a[11];
+    out[12] = a[12];
+    out[13] = a[13];
+    out[14] = a[14];
+    out[15] = a[15];
+    return out;
+};
+
+/**
+ * Set a mat4 to the identity matrix
+ *
+ * @param {mat4} out the receiving matrix
+ * @returns {mat4} out
+ */
+mat4.identity = function(out) {
+    out[0] = 1;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = 1;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = 1;
+    out[11] = 0;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = 0;
+    out[15] = 1;
+    return out;
+};
+
+/**
+ * Transpose the values of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.transpose = function(out, a) {
+    // If we are transposing ourselves we can skip a few steps but have to cache some values
+    if (out === a) {
+        var a01 = a[1], a02 = a[2], a03 = a[3],
+            a12 = a[6], a13 = a[7],
+            a23 = a[11];
+
+        out[1] = a[4];
+        out[2] = a[8];
+        out[3] = a[12];
+        out[4] = a01;
+        out[6] = a[9];
+        out[7] = a[13];
+        out[8] = a02;
+        out[9] = a12;
+        out[11] = a[14];
+        out[12] = a03;
+        out[13] = a13;
+        out[14] = a23;
+    } else {
+        out[0] = a[0];
+        out[1] = a[4];
+        out[2] = a[8];
+        out[3] = a[12];
+        out[4] = a[1];
+        out[5] = a[5];
+        out[6] = a[9];
+        out[7] = a[13];
+        out[8] = a[2];
+        out[9] = a[6];
+        out[10] = a[10];
+        out[11] = a[14];
+        out[12] = a[3];
+        out[13] = a[7];
+        out[14] = a[11];
+        out[15] = a[15];
+    }
+    
+    return out;
+};
+
+/**
+ * Inverts a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.invert = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+        b00 = a00 * a11 - a01 * a10,
+        b01 = a00 * a12 - a02 * a10,
+        b02 = a00 * a13 - a03 * a10,
+        b03 = a01 * a12 - a02 * a11,
+        b04 = a01 * a13 - a03 * a11,
+        b05 = a02 * a13 - a03 * a12,
+        b06 = a20 * a31 - a21 * a30,
+        b07 = a20 * a32 - a22 * a30,
+        b08 = a20 * a33 - a23 * a30,
+        b09 = a21 * a32 - a22 * a31,
+        b10 = a21 * a33 - a23 * a31,
+        b11 = a22 * a33 - a23 * a32,
+
+        // Calculate the determinant
+        det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+    if (!det) { 
+        return null; 
+    }
+    det = 1.0 / det;
+
+    out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+    out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+    out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+    out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
+    out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+    out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+    out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+    out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
+    out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+    out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+    out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+    out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
+    out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
+    out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
+    out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
+    out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
+
+    return out;
+};
+
+/**
+ * Calculates the adjugate of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.adjoint = function(out, a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+    out[0]  =  (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22));
+    out[1]  = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22));
+    out[2]  =  (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12));
+    out[3]  = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12));
+    out[4]  = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22));
+    out[5]  =  (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22));
+    out[6]  = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12));
+    out[7]  =  (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12));
+    out[8]  =  (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21));
+    out[9]  = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21));
+    out[10] =  (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11));
+    out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11));
+    out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21));
+    out[13] =  (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21));
+    out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11));
+    out[15] =  (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11));
+    return out;
+};
+
+/**
+ * Calculates the determinant of a mat4
+ *
+ * @param {mat4} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat4.determinant = function (a) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+        b00 = a00 * a11 - a01 * a10,
+        b01 = a00 * a12 - a02 * a10,
+        b02 = a00 * a13 - a03 * a10,
+        b03 = a01 * a12 - a02 * a11,
+        b04 = a01 * a13 - a03 * a11,
+        b05 = a02 * a13 - a03 * a12,
+        b06 = a20 * a31 - a21 * a30,
+        b07 = a20 * a32 - a22 * a30,
+        b08 = a20 * a33 - a23 * a30,
+        b09 = a21 * a32 - a22 * a31,
+        b10 = a21 * a33 - a23 * a31,
+        b11 = a22 * a33 - a23 * a32;
+
+    // Calculate the determinant
+    return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+};
+
+/**
+ * Multiplies two mat4's
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the first operand
+ * @param {mat4} b the second operand
+ * @returns {mat4} out
+ */
+mat4.multiply = function (out, a, b) {
+    var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+        a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+        a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+        a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+    // Cache only the current line of the second matrix
+    var b0  = b[0], b1 = b[1], b2 = b[2], b3 = b[3];  
+    out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+    b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
+    out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+    b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
+    out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+    b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
+    out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+    out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+    out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+    out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+    return out;
+};
+
+/**
+ * Alias for {@link mat4.multiply}
+ * @function
+ */
+mat4.mul = mat4.multiply;
+
+/**
+ * Translate a mat4 by the given vector
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to translate
+ * @param {vec3} v vector to translate by
+ * @returns {mat4} out
+ */
+mat4.translate = function (out, a, v) {
+    var x = v[0], y = v[1], z = v[2],
+        a00, a01, a02, a03,
+        a10, a11, a12, a13,
+        a20, a21, a22, a23;
+
+    if (a === out) {
+        out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
+        out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
+        out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
+        out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
+    } else {
+        a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+        a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+        a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+        out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03;
+        out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13;
+        out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23;
+
+        out[12] = a00 * x + a10 * y + a20 * z + a[12];
+        out[13] = a01 * x + a11 * y + a21 * z + a[13];
+        out[14] = a02 * x + a12 * y + a22 * z + a[14];
+        out[15] = a03 * x + a13 * y + a23 * z + a[15];
+    }
+
+    return out;
+};
+
+/**
+ * Scales the mat4 by the dimensions in the given vec3
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to scale
+ * @param {vec3} v the vec3 to scale the matrix by
+ * @returns {mat4} out
+ **/
+mat4.scale = function(out, a, v) {
+    var x = v[0], y = v[1], z = v[2];
+
+    out[0] = a[0] * x;
+    out[1] = a[1] * x;
+    out[2] = a[2] * x;
+    out[3] = a[3] * x;
+    out[4] = a[4] * y;
+    out[5] = a[5] * y;
+    out[6] = a[6] * y;
+    out[7] = a[7] * y;
+    out[8] = a[8] * z;
+    out[9] = a[9] * z;
+    out[10] = a[10] * z;
+    out[11] = a[11] * z;
+    out[12] = a[12];
+    out[13] = a[13];
+    out[14] = a[14];
+    out[15] = a[15];
+    return out;
+};
+
+/**
+ * Rotates a mat4 by the given angle
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @param {vec3} axis the axis to rotate around
+ * @returns {mat4} out
+ */
+mat4.rotate = function (out, a, rad, axis) {
+    var x = axis[0], y = axis[1], z = axis[2],
+        len = Math.sqrt(x * x + y * y + z * z),
+        s, c, t,
+        a00, a01, a02, a03,
+        a10, a11, a12, a13,
+        a20, a21, a22, a23,
+        b00, b01, b02,
+        b10, b11, b12,
+        b20, b21, b22;
+
+    if (Math.abs(len) < GLMAT_EPSILON) { return null; }
+    
+    len = 1 / len;
+    x *= len;
+    y *= len;
+    z *= len;
+
+    s = Math.sin(rad);
+    c = Math.cos(rad);
+    t = 1 - c;
+
+    a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+    a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+    a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+    // Construct the elements of the rotation matrix
+    b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s;
+    b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s;
+    b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c;
+
+    // Perform rotation-specific matrix multiplication
+    out[0] = a00 * b00 + a10 * b01 + a20 * b02;
+    out[1] = a01 * b00 + a11 * b01 + a21 * b02;
+    out[2] = a02 * b00 + a12 * b01 + a22 * b02;
+    out[3] = a03 * b00 + a13 * b01 + a23 * b02;
+    out[4] = a00 * b10 + a10 * b11 + a20 * b12;
+    out[5] = a01 * b10 + a11 * b11 + a21 * b12;
+    out[6] = a02 * b10 + a12 * b11 + a22 * b12;
+    out[7] = a03 * b10 + a13 * b11 + a23 * b12;
+    out[8] = a00 * b20 + a10 * b21 + a20 * b22;
+    out[9] = a01 * b20 + a11 * b21 + a21 * b22;
+    out[10] = a02 * b20 + a12 * b21 + a22 * b22;
+    out[11] = a03 * b20 + a13 * b21 + a23 * b22;
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged last row
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+    return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the X axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateX = function (out, a, rad) {
+    var s = Math.sin(rad),
+        c = Math.cos(rad),
+        a10 = a[4],
+        a11 = a[5],
+        a12 = a[6],
+        a13 = a[7],
+        a20 = a[8],
+        a21 = a[9],
+        a22 = a[10],
+        a23 = a[11];
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged rows
+        out[0]  = a[0];
+        out[1]  = a[1];
+        out[2]  = a[2];
+        out[3]  = a[3];
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+
+    // Perform axis-specific matrix multiplication
+    out[4] = a10 * c + a20 * s;
+    out[5] = a11 * c + a21 * s;
+    out[6] = a12 * c + a22 * s;
+    out[7] = a13 * c + a23 * s;
+    out[8] = a20 * c - a10 * s;
+    out[9] = a21 * c - a11 * s;
+    out[10] = a22 * c - a12 * s;
+    out[11] = a23 * c - a13 * s;
+    return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the Y axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateY = function (out, a, rad) {
+    var s = Math.sin(rad),
+        c = Math.cos(rad),
+        a00 = a[0],
+        a01 = a[1],
+        a02 = a[2],
+        a03 = a[3],
+        a20 = a[8],
+        a21 = a[9],
+        a22 = a[10],
+        a23 = a[11];
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged rows
+        out[4]  = a[4];
+        out[5]  = a[5];
+        out[6]  = a[6];
+        out[7]  = a[7];
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+
+    // Perform axis-specific matrix multiplication
+    out[0] = a00 * c - a20 * s;
+    out[1] = a01 * c - a21 * s;
+    out[2] = a02 * c - a22 * s;
+    out[3] = a03 * c - a23 * s;
+    out[8] = a00 * s + a20 * c;
+    out[9] = a01 * s + a21 * c;
+    out[10] = a02 * s + a22 * c;
+    out[11] = a03 * s + a23 * c;
+    return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the Z axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateZ = function (out, a, rad) {
+    var s = Math.sin(rad),
+        c = Math.cos(rad),
+        a00 = a[0],
+        a01 = a[1],
+        a02 = a[2],
+        a03 = a[3],
+        a10 = a[4],
+        a11 = a[5],
+        a12 = a[6],
+        a13 = a[7];
+
+    if (a !== out) { // If the source and destination differ, copy the unchanged last row
+        out[8]  = a[8];
+        out[9]  = a[9];
+        out[10] = a[10];
+        out[11] = a[11];
+        out[12] = a[12];
+        out[13] = a[13];
+        out[14] = a[14];
+        out[15] = a[15];
+    }
+
+    // Perform axis-specific matrix multiplication
+    out[0] = a00 * c + a10 * s;
+    out[1] = a01 * c + a11 * s;
+    out[2] = a02 * c + a12 * s;
+    out[3] = a03 * c + a13 * s;
+    out[4] = a10 * c - a00 * s;
+    out[5] = a11 * c - a01 * s;
+    out[6] = a12 * c - a02 * s;
+    out[7] = a13 * c - a03 * s;
+    return out;
+};
+
+/**
+ * Creates a matrix from a quaternion rotation and vector translation
+ * This is equivalent to (but much faster than):
+ *
+ *     mat4.identity(dest);
+ *     mat4.translate(dest, vec);
+ *     var quatMat = mat4.create();
+ *     quat4.toMat4(quat, quatMat);
+ *     mat4.multiply(dest, quatMat);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @returns {mat4} out
+ */
+mat4.fromRotationTranslation = function (out, q, v) {
+    // Quaternion math
+    var x = q[0], y = q[1], z = q[2], w = q[3],
+        x2 = x + x,
+        y2 = y + y,
+        z2 = z + z,
+
+        xx = x * x2,
+        xy = x * y2,
+        xz = x * z2,
+        yy = y * y2,
+        yz = y * z2,
+        zz = z * z2,
+        wx = w * x2,
+        wy = w * y2,
+        wz = w * z2;
+
+    out[0] = 1 - (yy + zz);
+    out[1] = xy + wz;
+    out[2] = xz - wy;
+    out[3] = 0;
+    out[4] = xy - wz;
+    out[5] = 1 - (xx + zz);
+    out[6] = yz + wx;
+    out[7] = 0;
+    out[8] = xz + wy;
+    out[9] = yz - wx;
+    out[10] = 1 - (xx + yy);
+    out[11] = 0;
+    out[12] = v[0];
+    out[13] = v[1];
+    out[14] = v[2];
+    out[15] = 1;
+    
+    return out;
+};
+
+mat4.fromQuat = function (out, q) {
+    var x = q[0], y = q[1], z = q[2], w = q[3],
+        x2 = x + x,
+        y2 = y + y,
+        z2 = z + z,
+
+        xx = x * x2,
+        yx = y * x2,
+        yy = y * y2,
+        zx = z * x2,
+        zy = z * y2,
+        zz = z * z2,
+        wx = w * x2,
+        wy = w * y2,
+        wz = w * z2;
+
+    out[0] = 1 - yy - zz;
+    out[1] = yx + wz;
+    out[2] = zx - wy;
+    out[3] = 0;
+
+    out[4] = yx - wz;
+    out[5] = 1 - xx - zz;
+    out[6] = zy + wx;
+    out[7] = 0;
+
+    out[8] = zx + wy;
+    out[9] = zy - wx;
+    out[10] = 1 - xx - yy;
+    out[11] = 0;
+
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = 0;
+    out[15] = 1;
+
+    return out;
+};
+
+/**
+ * Generates a frustum matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {Number} left Left bound of the frustum
+ * @param {Number} right Right bound of the frustum
+ * @param {Number} bottom Bottom bound of the frustum
+ * @param {Number} top Top bound of the frustum
+ * @param {Number} near Near bound of the frustum
+ * @param {Number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.frustum = function (out, left, right, bottom, top, near, far) {
+    var rl = 1 / (right - left),
+        tb = 1 / (top - bottom),
+        nf = 1 / (near - far);
+    out[0] = (near * 2) * rl;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = (near * 2) * tb;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = (right + left) * rl;
+    out[9] = (top + bottom) * tb;
+    out[10] = (far + near) * nf;
+    out[11] = -1;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = (far * near * 2) * nf;
+    out[15] = 0;
+    return out;
+};
+
+/**
+ * Generates a perspective projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} fovy Vertical field of view in radians
+ * @param {number} aspect Aspect ratio. typically viewport width/height
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.perspective = function (out, fovy, aspect, near, far) {
+    var f = 1.0 / Math.tan(fovy / 2),
+        nf = 1 / (near - far);
+    out[0] = f / aspect;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = f;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = (far + near) * nf;
+    out[11] = -1;
+    out[12] = 0;
+    out[13] = 0;
+    out[14] = (2 * far * near) * nf;
+    out[15] = 0;
+    return out;
+};
+
+/**
+ * Generates a orthogonal projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} left Left bound of the frustum
+ * @param {number} right Right bound of the frustum
+ * @param {number} bottom Bottom bound of the frustum
+ * @param {number} top Top bound of the frustum
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.ortho = function (out, left, right, bottom, top, near, far) {
+    var lr = 1 / (left - right),
+        bt = 1 / (bottom - top),
+        nf = 1 / (near - far);
+    out[0] = -2 * lr;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 0;
+    out[4] = 0;
+    out[5] = -2 * bt;
+    out[6] = 0;
+    out[7] = 0;
+    out[8] = 0;
+    out[9] = 0;
+    out[10] = 2 * nf;
+    out[11] = 0;
+    out[12] = (left + right) * lr;
+    out[13] = (top + bottom) * bt;
+    out[14] = (far + near) * nf;
+    out[15] = 1;
+    return out;
+};
+
+/**
+ * Generates a look-at matrix with the given eye position, focal point, and up axis
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {vec3} eye Position of the viewer
+ * @param {vec3} center Point the viewer is looking at
+ * @param {vec3} up vec3 pointing up
+ * @returns {mat4} out
+ */
+mat4.lookAt = function (out, eye, center, up) {
+    var x0, x1, x2, y0, y1, y2, z0, z1, z2, len,
+        eyex = eye[0],
+        eyey = eye[1],
+        eyez = eye[2],
+        upx = up[0],
+        upy = up[1],
+        upz = up[2],
+        centerx = center[0],
+        centery = center[1],
+        centerz = center[2];
+
+    if (Math.abs(eyex - centerx) < GLMAT_EPSILON &&
+        Math.abs(eyey - centery) < GLMAT_EPSILON &&
+        Math.abs(eyez - centerz) < GLMAT_EPSILON) {
+        return mat4.identity(out);
+    }
+
+    z0 = eyex - centerx;
+    z1 = eyey - centery;
+    z2 = eyez - centerz;
+
+    len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
+    z0 *= len;
+    z1 *= len;
+    z2 *= len;
+
+    x0 = upy * z2 - upz * z1;
+    x1 = upz * z0 - upx * z2;
+    x2 = upx * z1 - upy * z0;
+    len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
+    if (!len) {
+        x0 = 0;
+        x1 = 0;
+        x2 = 0;
+    } else {
+        len = 1 / len;
+        x0 *= len;
+        x1 *= len;
+        x2 *= len;
+    }
+
+    y0 = z1 * x2 - z2 * x1;
+    y1 = z2 * x0 - z0 * x2;
+    y2 = z0 * x1 - z1 * x0;
+
+    len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
+    if (!len) {
+        y0 = 0;
+        y1 = 0;
+        y2 = 0;
+    } else {
+        len = 1 / len;
+        y0 *= len;
+        y1 *= len;
+        y2 *= len;
+    }
+
+    out[0] = x0;
+    out[1] = y0;
+    out[2] = z0;
+    out[3] = 0;
+    out[4] = x1;
+    out[5] = y1;
+    out[6] = z1;
+    out[7] = 0;
+    out[8] = x2;
+    out[9] = y2;
+    out[10] = z2;
+    out[11] = 0;
+    out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
+    out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
+    out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
+    out[15] = 1;
+
+    return out;
+};
+
+/**
+ * Returns a string representation of a mat4
+ *
+ * @param {mat4} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat4.str = function (a) {
+    return 'mat4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' +
+                    a[4] + ', ' + a[5] + ', ' + a[6] + ', ' + a[7] + ', ' +
+                    a[8] + ', ' + a[9] + ', ' + a[10] + ', ' + a[11] + ', ' + 
+                    a[12] + ', ' + a[13] + ', ' + a[14] + ', ' + a[15] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat4
+ *
+ * @param {mat4} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat4.frob = function (a) {
+    return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2) + Math.pow(a[9], 2) + Math.pow(a[10], 2) + Math.pow(a[11], 2) + Math.pow(a[12], 2) + Math.pow(a[13], 2) + Math.pow(a[14], 2) + Math.pow(a[15], 2) ))
+};
+
+
+if(typeof(exports) !== 'undefined') {
+    exports.mat4 = mat4;
+}
+;
+/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation 
+    and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+/**
+ * @class Quaternion
+ * @name quat
+ */
+
+var quat = {};
+
+/**
+ * Creates a new identity quat
+ *
+ * @returns {quat} a new quaternion
+ */
+quat.create = function() {
+    var out = new GLMAT_ARRAY_TYPE(4);
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Sets a quaternion to represent the shortest rotation from one
+ * vector to another.
+ *
+ * Both vectors are assumed to be unit length.
+ *
+ * @param {quat} out the receiving quaternion.
+ * @param {vec3} a the initial vector
+ * @param {vec3} b the destination vector
+ * @returns {quat} out
+ */
+quat.rotationTo = (function() {
+    var tmpvec3 = vec3.create();
+    var xUnitVec3 = vec3.fromValues(1,0,0);
+    var yUnitVec3 = vec3.fromValues(0,1,0);
+
+    return function(out, a, b) {
+        var dot = vec3.dot(a, b);
+        if (dot < -0.999999) {
+            vec3.cross(tmpvec3, xUnitVec3, a);
+            if (vec3.length(tmpvec3) < 0.000001)
+                vec3.cross(tmpvec3, yUnitVec3, a);
+            vec3.normalize(tmpvec3, tmpvec3);
+            quat.setAxisAngle(out, tmpvec3, Math.PI);
+            return out;
+        } else if (dot > 0.999999) {
+            out[0] = 0;
+            out[1] = 0;
+            out[2] = 0;
+            out[3] = 1;
+            return out;
+        } else {
+            vec3.cross(tmpvec3, a, b);
+            out[0] = tmpvec3[0];
+            out[1] = tmpvec3[1];
+            out[2] = tmpvec3[2];
+            out[3] = 1 + dot;
+            return quat.normalize(out, out);
+        }
+    };
+})();
+
+/**
+ * Sets the specified quaternion with values corresponding to the given
+ * axes. Each axis is a vec3 and is expected to be unit length and
+ * perpendicular to all other specified axes.
+ *
+ * @param {vec3} view  the vector representing the viewing direction
+ * @param {vec3} right the vector representing the local "right" direction
+ * @param {vec3} up    the vector representing the local "up" direction
+ * @returns {quat} out
+ */
+quat.setAxes = (function() {
+    var matr = mat3.create();
+
+    return function(out, view, right, up) {
+        matr[0] = right[0];
+        matr[3] = right[1];
+        matr[6] = right[2];
+
+        matr[1] = up[0];
+        matr[4] = up[1];
+        matr[7] = up[2];
+
+        matr[2] = -view[0];
+        matr[5] = -view[1];
+        matr[8] = -view[2];
+
+        return quat.normalize(out, quat.fromMat3(out, matr));
+    };
+})();
+
+/**
+ * Creates a new quat initialized with values from an existing quaternion
+ *
+ * @param {quat} a quaternion to clone
+ * @returns {quat} a new quaternion
+ * @function
+ */
+quat.clone = vec4.clone;
+
+/**
+ * Creates a new quat initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} a new quaternion
+ * @function
+ */
+quat.fromValues = vec4.fromValues;
+
+/**
+ * Copy the values from one quat to another
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the source quaternion
+ * @returns {quat} out
+ * @function
+ */
+quat.copy = vec4.copy;
+
+/**
+ * Set the components of a quat to the given values
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} out
+ * @function
+ */
+quat.set = vec4.set;
+
+/**
+ * Set a quat to the identity quaternion
+ *
+ * @param {quat} out the receiving quaternion
+ * @returns {quat} out
+ */
+quat.identity = function(out) {
+    out[0] = 0;
+    out[1] = 0;
+    out[2] = 0;
+    out[3] = 1;
+    return out;
+};
+
+/**
+ * Sets a quat from the given angle and rotation axis,
+ * then returns it.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {vec3} axis the axis around which to rotate
+ * @param {Number} rad the angle in radians
+ * @returns {quat} out
+ **/
+quat.setAxisAngle = function(out, axis, rad) {
+    rad = rad * 0.5;
+    var s = Math.sin(rad);
+    out[0] = s * axis[0];
+    out[1] = s * axis[1];
+    out[2] = s * axis[2];
+    out[3] = Math.cos(rad);
+    return out;
+};
+
+/**
+ * Adds two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ * @function
+ */
+quat.add = vec4.add;
+
+/**
+ * Multiplies two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ */
+quat.multiply = function(out, a, b) {
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+    out[0] = ax * bw + aw * bx + ay * bz - az * by;
+    out[1] = ay * bw + aw * by + az * bx - ax * bz;
+    out[2] = az * bw + aw * bz + ax * by - ay * bx;
+    out[3] = aw * bw - ax * bx - ay * by - az * bz;
+    return out;
+};
+
+/**
+ * Alias for {@link quat.multiply}
+ * @function
+ */
+quat.mul = quat.multiply;
+
+/**
+ * Scales a quat by a scalar number
+ *
+ * @param {quat} out the receiving vector
+ * @param {quat} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {quat} out
+ * @function
+ */
+quat.scale = vec4.scale;
+
+/**
+ * Rotates a quaternion by the given angle about the X axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateX = function (out, a, rad) {
+    rad *= 0.5; 
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bx = Math.sin(rad), bw = Math.cos(rad);
+
+    out[0] = ax * bw + aw * bx;
+    out[1] = ay * bw + az * bx;
+    out[2] = az * bw - ay * bx;
+    out[3] = aw * bw - ax * bx;
+    return out;
+};
+
+/**
+ * Rotates a quaternion by the given angle about the Y axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateY = function (out, a, rad) {
+    rad *= 0.5; 
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        by = Math.sin(rad), bw = Math.cos(rad);
+
+    out[0] = ax * bw - az * by;
+    out[1] = ay * bw + aw * by;
+    out[2] = az * bw + ax * by;
+    out[3] = aw * bw - ay * by;
+    return out;
+};
+
+/**
+ * Rotates a quaternion by the given angle about the Z axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateZ = function (out, a, rad) {
+    rad *= 0.5; 
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bz = Math.sin(rad), bw = Math.cos(rad);
+
+    out[0] = ax * bw + ay * bz;
+    out[1] = ay * bw - ax * bz;
+    out[2] = az * bw + aw * bz;
+    out[3] = aw * bw - az * bz;
+    return out;
+};
+
+/**
+ * Calculates the W component of a quat from the X, Y, and Z components.
+ * Assumes that quaternion is 1 unit in length.
+ * Any existing W component will be ignored.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate W component of
+ * @returns {quat} out
+ */
+quat.calculateW = function (out, a) {
+    var x = a[0], y = a[1], z = a[2];
+
+    out[0] = x;
+    out[1] = y;
+    out[2] = z;
+    out[3] = -Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z));
+    return out;
+};
+
+/**
+ * Calculates the dot product of two quat's
+ *
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {Number} dot product of a and b
+ * @function
+ */
+quat.dot = vec4.dot;
+
+/**
+ * Performs a linear interpolation between two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ * @function
+ */
+quat.lerp = vec4.lerp;
+
+/**
+ * Performs a spherical linear interpolation between two quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ */
+quat.slerp = function (out, a, b, t) {
+    // benchmarks:
+    //    http://jsperf.com/quaternion-slerp-implementations
+
+    var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+        bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+    var        omega, cosom, sinom, scale0, scale1;
+
+    // calc cosine
+    cosom = ax * bx + ay * by + az * bz + aw * bw;
+    // adjust signs (if necessary)
+    if ( cosom < 0.0 ) {
+        cosom = -cosom;
+        bx = - bx;
+        by = - by;
+        bz = - bz;
+        bw = - bw;
+    }
+    // calculate coefficients
+    if ( (1.0 - cosom) > 0.000001 ) {
+        // standard case (slerp)
+        omega  = Math.acos(cosom);
+        sinom  = Math.sin(omega);
+        scale0 = Math.sin((1.0 - t) * omega) / sinom;
+        scale1 = Math.sin(t * omega) / sinom;
+    } else {        
+        // "from" and "to" quaternions are very close 
+        //  ... so we can do a linear interpolation
+        scale0 = 1.0 - t;
+        scale1 = t;
+    }
+    // calculate final values
+    out[0] = scale0 * ax + scale1 * bx;
+    out[1] = scale0 * ay + scale1 * by;
+    out[2] = scale0 * az + scale1 * bz;
+    out[3] = scale0 * aw + scale1 * bw;
+    
+    return out;
+};
+
+/**
+ * Calculates the inverse of a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate inverse of
+ * @returns {quat} out
+ */
+quat.invert = function(out, a) {
+    var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+        dot = a0*a0 + a1*a1 + a2*a2 + a3*a3,
+        invDot = dot ? 1.0/dot : 0;
+    
+    // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0
+
+    out[0] = -a0*invDot;
+    out[1] = -a1*invDot;
+    out[2] = -a2*invDot;
+    out[3] = a3*invDot;
+    return out;
+};
+
+/**
+ * Calculates the conjugate of a quat
+ * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate conjugate of
+ * @returns {quat} out
+ */
+quat.conjugate = function (out, a) {
+    out[0] = -a[0];
+    out[1] = -a[1];
+    out[2] = -a[2];
+    out[3] = a[3];
+    return out;
+};
+
+/**
+ * Calculates the length of a quat
+ *
+ * @param {quat} a vector to calculate length of
+ * @returns {Number} length of a
+ * @function
+ */
+quat.length = vec4.length;
+
+/**
+ * Alias for {@link quat.length}
+ * @function
+ */
+quat.len = quat.length;
+
+/**
+ * Calculates the squared length of a quat
+ *
+ * @param {quat} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ * @function
+ */
+quat.squaredLength = vec4.squaredLength;
+
+/**
+ * Alias for {@link quat.squaredLength}
+ * @function
+ */
+quat.sqrLen = quat.squaredLength;
+
+/**
+ * Normalize a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quaternion to normalize
+ * @returns {quat} out
+ * @function
+ */
+quat.normalize = vec4.normalize;
+
+/**
+ * Creates a quaternion from the given 3x3 rotation matrix.
+ *
+ * NOTE: The resultant quaternion is not normalized, so you should be sure
+ * to renormalize the quaternion yourself where necessary.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {mat3} m rotation matrix
+ * @returns {quat} out
+ * @function
+ */
+quat.fromMat3 = function(out, m) {
+    // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes
+    // article "Quaternion Calculus and Fast Animation".
+    var fTrace = m[0] + m[4] + m[8];
+    var fRoot;
+
+    if ( fTrace > 0.0 ) {
+        // |w| > 1/2, may as well choose w > 1/2
+        fRoot = Math.sqrt(fTrace + 1.0);  // 2w
+        out[3] = 0.5 * fRoot;
+        fRoot = 0.5/fRoot;  // 1/(4w)
+        out[0] = (m[7]-m[5])*fRoot;
+        out[1] = (m[2]-m[6])*fRoot;
+        out[2] = (m[3]-m[1])*fRoot;
+    } else {
+        // |w| <= 1/2
+        var i = 0;
+        if ( m[4] > m[0] )
+          i = 1;
+        if ( m[8] > m[i*3+i] )
+          i = 2;
+        var j = (i+1)%3;
+        var k = (i+2)%3;
+        
+        fRoot = Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k] + 1.0);
+        out[i] = 0.5 * fRoot;
+        fRoot = 0.5 / fRoot;
+        out[3] = (m[k*3+j] - m[j*3+k]) * fRoot;
+        out[j] = (m[j*3+i] + m[i*3+j]) * fRoot;
+        out[k] = (m[k*3+i] + m[i*3+k]) * fRoot;
+    }
+    
+    return out;
+};
+
+/**
+ * Returns a string representation of a quatenion
+ *
+ * @param {quat} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+quat.str = function (a) {
+    return 'quat(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+if(typeof(exports) !== 'undefined') {
+    exports.quat = quat;
+}
+;
+
+
+
+
+
+
+
+
+
+
+
+
+
+  })(shim.exports);
+})(this);
+
+},{}],24:[function(require,module,exports){
+//     Underscore.js 1.4.4
+//     http://underscorejs.org
+//     (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
+//     Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` in the browser, or `global` on the server.
+  var root = this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Establish the object that gets returned to break out of a loop iteration.
+  var breaker = {};
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var push             = ArrayProto.push,
+      slice            = ArrayProto.slice,
+      concat           = ArrayProto.concat,
+      toString         = ObjProto.toString,
+      hasOwnProperty   = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeForEach      = ArrayProto.forEach,
+    nativeMap          = ArrayProto.map,
+    nativeReduce       = ArrayProto.reduce,
+    nativeReduceRight  = ArrayProto.reduceRight,
+    nativeFilter       = ArrayProto.filter,
+    nativeEvery        = ArrayProto.every,
+    nativeSome         = ArrayProto.some,
+    nativeIndexOf      = ArrayProto.indexOf,
+    nativeLastIndexOf  = ArrayProto.lastIndexOf,
+    nativeIsArray      = Array.isArray,
+    nativeKeys         = Object.keys,
+    nativeBind         = FuncProto.bind;
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) {
+    if (obj instanceof _) return obj;
+    if (!(this instanceof _)) return new _(obj);
+    this._wrapped = obj;
+  };
+
+  // Export the Underscore object for **Node.js**, with
+  // backwards-compatibility for the old `require()` API. If we're in
+  // the browser, add `_` as a global object via a string identifier,
+  // for Closure Compiler "advanced" mode.
+  if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports) {
+      exports = module.exports = _;
+    }
+    exports._ = _;
+  } else {
+    root._ = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.4.4';
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles objects with the built-in `forEach`, arrays, and raw objects.
+  // Delegates to **ECMAScript 5**'s native `forEach` if available.
+  var each = _.each = _.forEach = function(obj, iterator, context) {
+    if (obj == null) return;
+    if (nativeForEach && obj.forEach === nativeForEach) {
+      obj.forEach(iterator, context);
+    } else if (obj.length === +obj.length) {
+      for (var i = 0, l = obj.length; i < l; i++) {
+        if (iterator.call(context, obj[i], i, obj) === breaker) return;
+      }
+    } else {
+      for (var key in obj) {
+        if (_.has(obj, key)) {
+          if (iterator.call(context, obj[key], key, obj) === breaker) return;
+        }
+      }
+    }
+  };
+
+  // Return the results of applying the iterator to each element.
+  // Delegates to **ECMAScript 5**'s native `map` if available.
+  _.map = _.collect = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
+    each(obj, function(value, index, list) {
+      results[results.length] = iterator.call(context, value, index, list);
+    });
+    return results;
+  };
+
+  var reduceError = 'Reduce of empty array with no initial value';
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
+  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
+    var initial = arguments.length > 2;
+    if (obj == null) obj = [];
+    if (nativeReduce && obj.reduce === nativeReduce) {
+      if (context) iterator = _.bind(iterator, context);
+      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
+    }
+    each(obj, function(value, index, list) {
+      if (!initial) {
+        memo = value;
+        initial = true;
+      } else {
+        memo = iterator.call(context, memo, value, index, list);
+      }
+    });
+    if (!initial) throw new TypeError(reduceError);
+    return memo;
+  };
+
+  // The right-associative version of reduce, also known as `foldr`.
+  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
+  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
+    var initial = arguments.length > 2;
+    if (obj == null) obj = [];
+    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
+      if (context) iterator = _.bind(iterator, context);
+      return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
+    }
+    var length = obj.length;
+    if (length !== +length) {
+      var keys = _.keys(obj);
+      length = keys.length;
+    }
+    each(obj, function(value, index, list) {
+      index = keys ? keys[--length] : --length;
+      if (!initial) {
+        memo = obj[index];
+        initial = true;
+      } else {
+        memo = iterator.call(context, memo, obj[index], index, list);
+      }
+    });
+    if (!initial) throw new TypeError(reduceError);
+    return memo;
+  };
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, iterator, context) {
+    var result;
+    any(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) {
+        result = value;
+        return true;
+      }
+    });
+    return result;
+  };
+
+  // Return all the elements that pass a truth test.
+  // Delegates to **ECMAScript 5**'s native `filter` if available.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
+    each(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) results[results.length] = value;
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, iterator, context) {
+    return _.filter(obj, function(value, index, list) {
+      return !iterator.call(context, value, index, list);
+    }, context);
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Delegates to **ECMAScript 5**'s native `every` if available.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, iterator, context) {
+    iterator || (iterator = _.identity);
+    var result = true;
+    if (obj == null) return result;
+    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
+    each(obj, function(value, index, list) {
+      if (!(result = result && iterator.call(context, value, index, list))) return breaker;
+    });
+    return !!result;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Delegates to **ECMAScript 5**'s native `some` if available.
+  // Aliased as `any`.
+  var any = _.some = _.any = function(obj, iterator, context) {
+    iterator || (iterator = _.identity);
+    var result = false;
+    if (obj == null) return result;
+    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
+    each(obj, function(value, index, list) {
+      if (result || (result = iterator.call(context, value, index, list))) return breaker;
+    });
+    return !!result;
+  };
+
+  // Determine if the array or object contains a given value (using `===`).
+  // Aliased as `include`.
+  _.contains = _.include = function(obj, target) {
+    if (obj == null) return false;
+    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
+    return any(obj, function(value) {
+      return value === target;
+    });
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = function(obj, method) {
+    var args = slice.call(arguments, 2);
+    var isFunc = _.isFunction(method);
+    return _.map(obj, function(value) {
+      return (isFunc ? method : value[method]).apply(value, args);
+    });
+  };
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, function(value){ return value[key]; });
+  };
+
+  // Convenience version of a common use case of `filter`: selecting only objects
+  // containing specific `key:value` pairs.
+  _.where = function(obj, attrs, first) {
+    if (_.isEmpty(attrs)) return first ? null : [];
+    return _[first ? 'find' : 'filter'](obj, function(value) {
+      for (var key in attrs) {
+        if (attrs[key] !== value[key]) return false;
+      }
+      return true;
+    });
+  };
+
+  // Convenience version of a common use case of `find`: getting the first object
+  // containing specific `key:value` pairs.
+  _.findWhere = function(obj, attrs) {
+    return _.where(obj, attrs, true);
+  };
+
+  // Return the maximum element or (element-based computation).
+  // Can't optimize arrays of integers longer than 65,535 elements.
+  // See: https://bugs.webkit.org/show_bug.cgi?id=80797
+  _.max = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
+      return Math.max.apply(Math, obj);
+    }
+    if (!iterator && _.isEmpty(obj)) return -Infinity;
+    var result = {computed : -Infinity, value: -Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed >= result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
+      return Math.min.apply(Math, obj);
+    }
+    if (!iterator && _.isEmpty(obj)) return Infinity;
+    var result = {computed : Infinity, value: Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed < result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Shuffle an array.
+  _.shuffle = function(obj) {
+    var rand;
+    var index = 0;
+    var shuffled = [];
+    each(obj, function(value) {
+      rand = _.random(index++);
+      shuffled[index - 1] = shuffled[rand];
+      shuffled[rand] = value;
+    });
+    return shuffled;
+  };
+
+  // An internal function to generate lookup iterators.
+  var lookupIterator = function(value) {
+    return _.isFunction(value) ? value : function(obj){ return obj[value]; };
+  };
+
+  // Sort the object's values by a criterion produced by an iterator.
+  _.sortBy = function(obj, value, context) {
+    var iterator = lookupIterator(value);
+    return _.pluck(_.map(obj, function(value, index, list) {
+      return {
+        value : value,
+        index : index,
+        criteria : iterator.call(context, value, index, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria;
+      var b = right.criteria;
+      if (a !== b) {
+        if (a > b || a === void 0) return 1;
+        if (a < b || b === void 0) return -1;
+      }
+      return left.index < right.index ? -1 : 1;
+    }), 'value');
+  };
+
+  // An internal function used for aggregate "group by" operations.
+  var group = function(obj, value, context, behavior) {
+    var result = {};
+    var iterator = lookupIterator(value || _.identity);
+    each(obj, function(value, index) {
+      var key = iterator.call(context, value, index, obj);
+      behavior(result, key, value);
+    });
+    return result;
+  };
+
+  // Groups the object's values by a criterion. Pass either a string attribute
+  // to group by, or a function that returns the criterion.
+  _.groupBy = function(obj, value, context) {
+    return group(obj, value, context, function(result, key, value) {
+      (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
+    });
+  };
+
+  // Counts instances of an object that group by a certain criterion. Pass
+  // either a string attribute to count by, or a function that returns the
+  // criterion.
+  _.countBy = function(obj, value, context) {
+    return group(obj, value, context, function(result, key) {
+      if (!_.has(result, key)) result[key] = 0;
+      result[key]++;
+    });
+  };
+
+  // Use a comparator function to figure out the smallest index at which
+  // an object should be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iterator, context) {
+    iterator = iterator == null ? _.identity : lookupIterator(iterator);
+    var value = iterator.call(context, obj);
+    var low = 0, high = array.length;
+    while (low < high) {
+      var mid = (low + high) >>> 1;
+      iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
+    }
+    return low;
+  };
+
+  // Safely convert anything iterable into a real, live array.
+  _.toArray = function(obj) {
+    if (!obj) return [];
+    if (_.isArray(obj)) return slice.call(obj);
+    if (obj.length === +obj.length) return _.map(obj, _.identity);
+    return _.values(obj);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    if (obj == null) return 0;
+    return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
+  };
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head` and `take`. The **guard** check
+  // allows it to work with `_.map`.
+  _.first = _.head = _.take = function(array, n, guard) {
+    if (array == null) return void 0;
+    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
+  };
+
+  // Returns everything but the last entry of the array. Especially useful on
+  // the arguments object. Passing **n** will return all the values in
+  // the array, excluding the last N. The **guard** check allows it to work with
+  // `_.map`.
+  _.initial = function(array, n, guard) {
+    return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
+  };
+
+  // Get the last element of an array. Passing **n** will return the last N
+  // values in the array. The **guard** check allows it to work with `_.map`.
+  _.last = function(array, n, guard) {
+    if (array == null) return void 0;
+    if ((n != null) && !guard) {
+      return slice.call(array, Math.max(array.length - n, 0));
+    } else {
+      return array[array.length - 1];
+    }
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+  // Especially useful on the arguments object. Passing an **n** will return
+  // the rest N values in the array. The **guard**
+  // check allows it to work with `_.map`.
+  _.rest = _.tail = _.drop = function(array, n, guard) {
+    return slice.call(array, (n == null) || guard ? 1 : n);
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, _.identity);
+  };
+
+  // Internal implementation of a recursive `flatten` function.
+  var flatten = function(input, shallow, output) {
+    each(input, function(value) {
+      if (_.isArray(value)) {
+        shallow ? push.apply(output, value) : flatten(value, shallow, output);
+      } else {
+        output.push(value);
+      }
+    });
+    return output;
+  };
+
+  // Return a completely flattened version of an array.
+  _.flatten = function(array, shallow) {
+    return flatten(array, shallow, []);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = function(array) {
+    return _.difference(array, slice.call(arguments, 1));
+  };
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted, iterator, context) {
+    if (_.isFunction(isSorted)) {
+      context = iterator;
+      iterator = isSorted;
+      isSorted = false;
+    }
+    var initial = iterator ? _.map(array, iterator, context) : array;
+    var results = [];
+    var seen = [];
+    each(initial, function(value, index) {
+      if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
+        seen.push(value);
+        results.push(array[index]);
+      }
+    });
+    return results;
+  };
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  _.union = function() {
+    return _.uniq(concat.apply(ArrayProto, arguments));
+  };
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  _.intersection = function(array) {
+    var rest = slice.call(arguments, 1);
+    return _.filter(_.uniq(array), function(item) {
+      return _.every(rest, function(other) {
+        return _.indexOf(other, item) >= 0;
+      });
+    });
+  };
+
+  // Take the difference between one array and a number of other arrays.
+  // Only the elements present in just the first array will remain.
+  _.difference = function(array) {
+    var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
+    return _.filter(array, function(value){ return !_.contains(rest, value); });
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = function() {
+    var args = slice.call(arguments);
+    var length = _.max(_.pluck(args, 'length'));
+    var results = new Array(length);
+    for (var i = 0; i < length; i++) {
+      results[i] = _.pluck(args, "" + i);
+    }
+    return results;
+  };
+
+  // Converts lists into objects. Pass either a single array of `[key, value]`
+  // pairs, or two parallel arrays of the same length -- one of keys, and one of
+  // the corresponding values.
+  _.object = function(list, values) {
+    if (list == null) return {};
+    var result = {};
+    for (var i = 0, l = list.length; i < l; i++) {
+      if (values) {
+        result[list[i]] = values[i];
+      } else {
+        result[list[i][0]] = list[i][1];
+      }
+    }
+    return result;
+  };
+
+  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
+  // we need this function. Return the position of the first occurrence of an
+  // item in an array, or -1 if the item is not included in the array.
+  // Delegates to **ECMAScript 5**'s native `indexOf` if available.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = function(array, item, isSorted) {
+    if (array == null) return -1;
+    var i = 0, l = array.length;
+    if (isSorted) {
+      if (typeof isSorted == 'number') {
+        i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
+      } else {
+        i = _.sortedIndex(array, item);
+        return array[i] === item ? i : -1;
+      }
+    }
+    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
+    for (; i < l; i++) if (array[i] === item) return i;
+    return -1;
+  };
+
+  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
+  _.lastIndexOf = function(array, item, from) {
+    if (array == null) return -1;
+    var hasIndex = from != null;
+    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
+      return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
+    }
+    var i = (hasIndex ? from : array.length);
+    while (i--) if (array[i] === item) return i;
+    return -1;
+  };
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (arguments.length <= 1) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = arguments[2] || 1;
+
+    var len = Math.max(Math.ceil((stop - start) / step), 0);
+    var idx = 0;
+    var range = new Array(len);
+
+    while(idx < len) {
+      range[idx++] = start;
+      start += step;
+    }
+
+    return range;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+  // available.
+  _.bind = function(func, context) {
+    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+    var args = slice.call(arguments, 2);
+    return function() {
+      return func.apply(context, args.concat(slice.call(arguments)));
+    };
+  };
+
+  // Partially apply a function by creating a version that has had some of its
+  // arguments pre-filled, without changing its dynamic `this` context.
+  _.partial = function(func) {
+    var args = slice.call(arguments, 1);
+    return function() {
+      return func.apply(this, args.concat(slice.call(arguments)));
+    };
+  };
+
+  // Bind all of an object's methods to that object. Useful for ensuring that
+  // all callbacks defined on an object belong to it.
+  _.bindAll = function(obj) {
+    var funcs = slice.call(arguments, 1);
+    if (funcs.length === 0) funcs = _.functions(obj);
+    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
+    return obj;
+  };
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memo = {};
+    hasher || (hasher = _.identity);
+    return function() {
+      var key = hasher.apply(this, arguments);
+      return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
+    };
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = function(func, wait) {
+    var args = slice.call(arguments, 2);
+    return setTimeout(function(){ return func.apply(null, args); }, wait);
+  };
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = function(func) {
+    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+  };
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time.
+  _.throttle = function(func, wait) {
+    var context, args, timeout, result;
+    var previous = 0;
+    var later = function() {
+      previous = new Date;
+      timeout = null;
+      result = func.apply(context, args);
+    };
+    return function() {
+      var now = new Date;
+      var remaining = wait - (now - previous);
+      context = this;
+      args = arguments;
+      if (remaining <= 0) {
+        clearTimeout(timeout);
+        timeout = null;
+        previous = now;
+        result = func.apply(context, args);
+      } else if (!timeout) {
+        timeout = setTimeout(later, remaining);
+      }
+      return result;
+    };
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds. If `immediate` is passed, trigger the function on the
+  // leading edge, instead of the trailing.
+  _.debounce = function(func, wait, immediate) {
+    var timeout, result;
+    return function() {
+      var context = this, args = arguments;
+      var later = function() {
+        timeout = null;
+        if (!immediate) result = func.apply(context, args);
+      };
+      var callNow = immediate && !timeout;
+      clearTimeout(timeout);
+      timeout = setTimeout(later, wait);
+      if (callNow) result = func.apply(context, args);
+      return result;
+    };
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = function(func) {
+    var ran = false, memo;
+    return function() {
+      if (ran) return memo;
+      ran = true;
+      memo = func.apply(this, arguments);
+      func = null;
+      return memo;
+    };
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return function() {
+      var args = [func];
+      push.apply(args, arguments);
+      return wrapper.apply(this, args);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var funcs = arguments;
+    return function() {
+      var args = arguments;
+      for (var i = funcs.length - 1; i >= 0; i--) {
+        args = [funcs[i].apply(this, args)];
+      }
+      return args[0];
+    };
+  };
+
+  // Returns a function that will only be executed after being called N times.
+  _.after = function(times, func) {
+    if (times <= 0) return func();
+    return function() {
+      if (--times < 1) {
+        return func.apply(this, arguments);
+      }
+    };
+  };
+
+  // Object Functions
+  // ----------------
+
+  // Retrieve the names of an object's properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = nativeKeys || function(obj) {
+    if (obj !== Object(obj)) throw new TypeError('Invalid object');
+    var keys = [];
+    for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    var values = [];
+    for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
+    return values;
+  };
+
+  // Convert an object into a list of `[key, value]` pairs.
+  _.pairs = function(obj) {
+    var pairs = [];
+    for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
+    return pairs;
+  };
+
+  // Invert the keys and values of an object. The values must be serializable.
+  _.invert = function(obj) {
+    var result = {};
+    for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
+    return result;
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (_.isFunction(obj[key])) names.push(key);
+    }
+    return names.sort();
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      if (source) {
+        for (var prop in source) {
+          obj[prop] = source[prop];
+        }
+      }
+    });
+    return obj;
+  };
+
+  // Return a copy of the object only containing the whitelisted properties.
+  _.pick = function(obj) {
+    var copy = {};
+    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
+    each(keys, function(key) {
+      if (key in obj) copy[key] = obj[key];
+    });
+    return copy;
+  };
+
+   // Return a copy of the object without the blacklisted properties.
+  _.omit = function(obj) {
+    var copy = {};
+    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
+    for (var key in obj) {
+      if (!_.contains(keys, key)) copy[key] = obj[key];
+    }
+    return copy;
+  };
+
+  // Fill in a given object with default properties.
+  _.defaults = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      if (source) {
+        for (var prop in source) {
+          if (obj[prop] == null) obj[prop] = source[prop];
+        }
+      }
+    });
+    return obj;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Internal recursive comparison function for `isEqual`.
+  var eq = function(a, b, aStack, bStack) {
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
+    if (a === b) return a !== 0 || 1 / a == 1 / b;
+    // A strict comparison is necessary because `null == undefined`.
+    if (a == null || b == null) return a === b;
+    // Unwrap any wrapped objects.
+    if (a instanceof _) a = a._wrapped;
+    if (b instanceof _) b = b._wrapped;
+    // Compare `[[Class]]` names.
+    var className = toString.call(a);
+    if (className != toString.call(b)) return false;
+    switch (className) {
+      // Strings, numbers, dates, and booleans are compared by value.
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return a == String(b);
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+        // other numeric values.
+        return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a == +b;
+      // RegExps are compared by their source patterns and flags.
+      case '[object RegExp]':
+        return a.source == b.source &&
+               a.global == b.global &&
+               a.multiline == b.multiline &&
+               a.ignoreCase == b.ignoreCase;
+    }
+    if (typeof a != 'object' || typeof b != 'object') return false;
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] == a) return bStack[length] == b;
+    }
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+    var size = 0, result = true;
+    // Recursively compare objects and arrays.
+    if (className == '[object Array]') {
+      // Compare array lengths to determine if a deep comparison is necessary.
+      size = a.length;
+      result = size == b.length;
+      if (result) {
+        // Deep compare the contents, ignoring non-numeric properties.
+        while (size--) {
+          if (!(result = eq(a[size], b[size], aStack, bStack))) break;
+        }
+      }
+    } else {
+      // Objects with different constructors are not equivalent, but `Object`s
+      // from different frames are.
+      var aCtor = a.constructor, bCtor = b.constructor;
+      if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
+                               _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
+        return false;
+      }
+      // Deep compare objects.
+      for (var key in a) {
+        if (_.has(a, key)) {
+          // Count the expected number of properties.
+          size++;
+          // Deep compare each member.
+          if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
+        }
+      }
+      // Ensure that both objects contain the same number of properties.
+      if (result) {
+        for (key in b) {
+          if (_.has(b, key) && !(size--)) break;
+        }
+        result = !size;
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+    return result;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    return eq(a, b, [], []);
+  };
+
+  // Is a given array, string, or object empty?
+  // An "empty" object has no enumerable own-properties.
+  _.isEmpty = function(obj) {
+    if (obj == null) return true;
+    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
+    for (var key in obj) if (_.has(obj, key)) return false;
+    return true;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType === 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) == '[object Array]';
+  };
+
+  // Is a given variable an object?
+  _.isObject = function(obj) {
+    return obj === Object(obj);
+  };
+
+  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
+  each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
+    _['is' + name] = function(obj) {
+      return toString.call(obj) == '[object ' + name + ']';
+    };
+  });
+
+  // Define a fallback version of the method in browsers (ahem, IE), where
+  // there isn't any inspectable "Arguments" type.
+  if (!_.isArguments(arguments)) {
+    _.isArguments = function(obj) {
+      return !!(obj && _.has(obj, 'callee'));
+    };
+  }
+
+  // Optimize `isFunction` if appropriate.
+  if (typeof (/./) !== 'function') {
+    _.isFunction = function(obj) {
+      return typeof obj === 'function';
+    };
+  }
+
+  // Is a given object a finite number?
+  _.isFinite = function(obj) {
+    return isFinite(obj) && !isNaN(parseFloat(obj));
+  };
+
+  // Is the given value `NaN`? (NaN is the only number which does not equal itself).
+  _.isNaN = function(obj) {
+    return _.isNumber(obj) && obj != +obj;
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Shortcut function for checking if an object has a given property directly
+  // on itself (in other words, not on a prototype).
+  _.has = function(obj, key) {
+    return hasOwnProperty.call(obj, key);
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iterators.
+  _.identity = function(value) {
+    return value;
+  };
+
+  // Run a function **n** times.
+  _.times = function(n, iterator, context) {
+    var accum = Array(n);
+    for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
+    return accum;
+  };
+
+  // Return a random integer between min and max (inclusive).
+  _.random = function(min, max) {
+    if (max == null) {
+      max = min;
+      min = 0;
+    }
+    return min + Math.floor(Math.random() * (max - min + 1));
+  };
+
+  // List of HTML entities for escaping.
+  var entityMap = {
+    escape: {
+      '&': '&amp;',
+      '<': '&lt;',
+      '>': '&gt;',
+      '"': '&quot;',
+      "'": '&#x27;',
+      '/': '&#x2F;'
+    }
+  };
+  entityMap.unescape = _.invert(entityMap.escape);
+
+  // Regexes containing the keys and values listed immediately above.
+  var entityRegexes = {
+    escape:   new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
+    unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
+  };
+
+  // Functions for escaping and unescaping strings to/from HTML interpolation.
+  _.each(['escape', 'unescape'], function(method) {
+    _[method] = function(string) {
+      if (string == null) return '';
+      return ('' + string).replace(entityRegexes[method], function(match) {
+        return entityMap[method][match];
+      });
+    };
+  });
+
+  // If the value of the named property is a function then invoke it;
+  // otherwise, return it.
+  _.result = function(object, property) {
+    if (object == null) return null;
+    var value = object[property];
+    return _.isFunction(value) ? value.call(object) : value;
+  };
+
+  // Add your own custom functions to the Underscore object.
+  _.mixin = function(obj) {
+    each(_.functions(obj), function(name){
+      var func = _[name] = obj[name];
+      _.prototype[name] = function() {
+        var args = [this._wrapped];
+        push.apply(args, arguments);
+        return result.call(this, func.apply(_, args));
+      };
+    });
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = ++idCounter + '';
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g,
+    escape      : /<%-([\s\S]+?)%>/g
+  };
+
+  // When customizing `templateSettings`, if you don't want to define an
+  // interpolation, evaluation or escaping regex, we need one that is
+  // guaranteed not to match.
+  var noMatch = /(.)^/;
+
+  // Certain characters need to be escaped so that they can be put into a
+  // string literal.
+  var escapes = {
+    "'":      "'",
+    '\\':     '\\',
+    '\r':     'r',
+    '\n':     'n',
+    '\t':     't',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  _.template = function(text, data, settings) {
+    var render;
+    settings = _.defaults({}, settings, _.templateSettings);
+
+    // Combine delimiters into one regular expression via alternation.
+    var matcher = new RegExp([
+      (settings.escape || noMatch).source,
+      (settings.interpolate || noMatch).source,
+      (settings.evaluate || noMatch).source
+    ].join('|') + '|$', 'g');
+
+    // Compile the template source, escaping string literals appropriately.
+    var index = 0;
+    var source = "__p+='";
+    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+      source += text.slice(index, offset)
+        .replace(escaper, function(match) { return '\\' + escapes[match]; });
+
+      if (escape) {
+        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+      }
+      if (interpolate) {
+        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+      }
+      if (evaluate) {
+        source += "';\n" + evaluate + "\n__p+='";
+      }
+      index = offset + match.length;
+      return match;
+    });
+    source += "';\n";
+
+    // If a variable is not specified, place data values in local scope.
+    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+    source = "var __t,__p='',__j=Array.prototype.join," +
+      "print=function(){__p+=__j.call(arguments,'');};\n" +
+      source + "return __p;\n";
+
+    try {
+      render = new Function(settings.variable || 'obj', '_', source);
+    } catch (e) {
+      e.source = source;
+      throw e;
+    }
+
+    if (data) return render(data, _);
+    var template = function(data) {
+      return render.call(this, data, _);
+    };
+
+    // Provide the compiled function source as a convenience for precompilation.
+    template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
+
+    return template;
+  };
+
+  // Add a "chain" function, which will delegate to the wrapper.
+  _.chain = function(obj) {
+    return _(obj).chain();
+  };
+
+  // OOP
+  // ---------------
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+
+  // Helper function to continue chaining intermediate results.
+  var result = function(obj) {
+    return this._chain ? _(obj).chain() : obj;
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      method.apply(obj, arguments);
+      if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
+      return result.call(this, obj);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      return result.call(this, method.apply(this._wrapped, arguments));
+    };
+  });
+
+  _.extend(_.prototype, {
+
+    // Start chaining a wrapped Underscore object.
+    chain: function() {
+      this._chain = true;
+      return this;
+    },
+
+    // Extracts the result from a wrapped and chained object.
+    value: function() {
+      return this._wrapped;
+    }
+
+  });
+
+}).call(this);
+
+},{}],25:[function(require,module,exports){
+if (typeof(window) !== 'undefined' && typeof(window.requestAnimationFrame) !== 'function') {
+  window.requestAnimationFrame = (
+    window.webkitRequestAnimationFrame   ||
+    window.mozRequestAnimationFrame      ||
+    window.oRequestAnimationFrame        ||
+    window.msRequestAnimationFrame       ||
+    function(callback) { setTimeout(callback, 1000 / 60); }
+  );
+}
+
+Leap = require("../lib/index");
+
+},{"../lib/index":11}]},{},[25])
+;
\ No newline at end of file