Browse Source

Merge gl-geo

David Sehnal 7 năm trước cách đây
mục cha
commit
9f936d9c12
44 tập tin đã thay đổi với 2632 bổ sung27 xóa
  1. 3 1
      .gitignore
  2. 1 1
      .vscode/settings.json
  3. 5 0
      .vscode/tasks.json
  4. 29 12
      package.json
  5. 3 1
      src/apps/render-test/index.tsx
  6. 171 0
      src/apps/render-test/state.ts
  7. 10 3
      src/apps/render-test/ui.tsx
  8. 15 0
      src/helpers.d.ts
  9. 95 0
      src/mol-geo/primitive/box.ts
  10. 28 0
      src/mol-geo/primitive/icosahedron.ts
  11. 94 0
      src/mol-geo/primitive/polyhedron.ts
  12. 0 0
      src/mol-geo/representation/structure/spacefill.ts
  13. 17 0
      src/mol-geo/shape/point.ts
  14. 5 0
      src/mol-geo/shape/sphere.ts
  15. 113 0
      src/mol-geo/util.ts
  16. 102 0
      src/mol-gl/attribute.ts
  17. 191 0
      src/mol-gl/camera.ts
  18. 57 0
      src/mol-gl/model.ts
  19. 21 0
      src/mol-gl/renderable.ts
  20. 5 0
      src/mol-gl/renderable/line.ts
  21. 74 0
      src/mol-gl/renderable/mesh.ts
  22. 85 0
      src/mol-gl/renderable/point.ts
  23. 30 0
      src/mol-gl/renderable/util.ts
  24. 14 0
      src/mol-gl/shader/attenuation.glsl
  25. 70 0
      src/mol-gl/shader/inverse.glsl
  26. 75 0
      src/mol-gl/shader/mesh.frag
  27. 58 0
      src/mol-gl/shader/mesh.vert
  28. 9 0
      src/mol-gl/shader/point.frag
  29. 26 0
      src/mol-gl/shader/point.vert
  30. 13 0
      src/mol-gl/shader/read-vec3.glsl
  31. 26 0
      src/mol-gl/shader/transpose.glsl
  32. 12 0
      src/mol-gl/shaders.ts
  33. 13 0
      src/mol-gl/util.ts
  34. 61 0
      src/mol-math/interpolate.ts
  35. 673 4
      src/mol-math/linear-algebra/3d.ts
  36. 4 0
      src/mol-util/index.ts
  37. 194 0
      src/mol-util/mouse-change.ts
  38. 65 0
      src/mol-util/mouse-event.ts
  39. 40 0
      src/mol-util/mouse-wheel.ts
  40. 21 0
      src/mol-util/parse-unit.ts
  41. 63 0
      src/mol-util/to-pixels.ts
  42. 6 5
      tsconfig.json
  43. 12 0
      web/render-test/index.html
  44. 23 0
      webpack.config.js

+ 3 - 1
.gitignore

@@ -4,4 +4,6 @@ node_modules/
 debug.log
 npm-debug.log
 
-*.sublime-workspace
+*.sublime-workspace
+
+web/render-test/index.js

+ 1 - 1
.vscode/settings.json

@@ -1,3 +1,3 @@
 {
-    "typescript.tsdk": "node_modules\\typescript\\lib"
+    "typescript.tsdk": "node_modules/typescript/lib"
 }

+ 5 - 0
.vscode/tasks.json

@@ -9,6 +9,11 @@
             "problemMatcher": [
                 "$tsc"
             ]
+        },
+        {
+            "type": "npm",
+            "script": "app-render-test",
+            "problemMatcher": []
         }
     ]
 }

+ 29 - 12
package.json

@@ -12,10 +12,13 @@
   },
   "scripts": {
     "lint": "tslint src/**/*.ts",
-    "build": "tsc",
+    "build": "cpx \"src/**/*.{vert,frag,glsl}\" build/node_modules/ && tsc",
     "watch": "tsc -watch",
+    "watch-shader": "cpx \"src/**/*.{vert,frag,glsl}\" build/node_modules/ --watch",
     "test": "jest",
-    "script": "node build/node_modules/script.js"
+    "script": "node build/node_modules/script.js",
+    "app-render-test": "webpack build/node_modules/apps/render-test/index.js --mode development -o web/render-test/index.js",
+    "app-render-test-watch": "webpack build/node_modules/apps/render-test/index.js -w --mode development -o web/render-test/index.js"
   },
   "jest": {
     "moduleFileExtensions": [
@@ -30,29 +33,41 @@
       "build/node_modules"
     ],
     "moduleNameMapper": {
-      "mol-task($|/.*)": "<rootDir>/src/mol-task$1",
-      "mol-comp($|/.*)": "<rootDir>/src/mol-comp$1",
-      "mol-util($|/.*)": "<rootDir>/src/mol-util$1",
       "mol-data($|/.*)": "<rootDir>/src/mol-data$1",
-      "mol-math($|/.*)": "<rootDir>/src/mol-math$1",
+      "mol-gl($|/.*)": "<rootDir>/src/mol-gl$1",
       "mol-io($|/.*)": "<rootDir>/src/mol-io$1",
-      "mol-model($|/.*)": "<rootDir>/src/mol-model$1"
+      "mol-math($|/.*)": "<rootDir>/src/mol-math$1",
+      "mol-model($|/.*)": "<rootDir>/src/mol-model$1",
+      "mol-ql($|/.*)": "<rootDir>/src/mol-ql$1",
+      "mol-task($|/.*)": "<rootDir>/src/mol-task$1",
+      "mol-util($|/.*)": "<rootDir>/src/mol-util$1"
     },
     "testRegex": "\\.spec\\.ts$"
   },
+  "glslify": {
+    "transform": [
+      "glslify-import"
+    ]
+  },
   "author": "",
   "license": "MIT",
   "devDependencies": {
     "@types/argparse": "^1.0.33",
     "@types/benchmark": "^1.0.31",
     "@types/express": "^4.11.1",
-    "@types/jest": "^22.2.2",
-    "@types/node": "^9.6.1",
+    "@types/jest": "^22.1.3",
+    "@types/node": "^9.6.0",
     "@types/node-fetch": "^1.6.7",
     "@types/react": "^16.1.0",
     "@types/react-dom": "^16.0.4",
     "benchmark": "^2.1.4",
-    "jest": "^22.4.3",
+    "copyfiles": "^2.0.0",
+    "cpx": "^1.5.0",
+    "extra-watch-webpack-plugin": "^1.0.1",
+    "glslify-import": "^3.1.0",
+    "glslify-loader": "^1.0.2",
+    "jest": "^22.4.2",
+    "raw-loader": "^0.5.1",
     "regl": "git+https://github.com/regl-project/regl.git#45c6ec570232420fca21567499c9c5a2a054432e",
     "rollup": "^0.56.5",
     "rollup-plugin-buble": "^0.19.2",
@@ -63,8 +78,10 @@
     "ts-jest": "^22.4.2",
     "tslint": "^5.9.1",
     "typescript": "^2.8.1",
-    "uglify-js": "^3.3.16",
-    "util.promisify": "^1.0.0"
+    "uglify-js": "^3.3.12",
+    "util.promisify": "^1.0.0",
+    "webpack": "^4.2.0",
+    "webpack-cli": "^2.0.13"
   },
   "dependencies": {
     "argparse": "^1.0.10",

+ 3 - 1
src/apps/render-test/index.tsx

@@ -5,7 +5,9 @@
  */
 
 import UI from './ui'
+import State from './state'
 import * as React from 'react'
 import * as ReactDOM from 'react-dom'
 
-ReactDOM.render(<UI/>, document.getElementById('app'));
+const state = new State()
+ReactDOM.render(<UI state={state} />, document.getElementById('app'));

+ 171 - 0
src/apps/render-test/state.ts

@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import REGL = require('regl');
+import * as glContext from 'mol-gl/context'
+import { Camera } from 'mol-gl/camera'
+import { Vec3, Mat4 } from 'mol-math/linear-algebra'
+import { PointRenderable, MeshRenderable } from 'mol-gl/renderable'
+import Attribute from 'mol-gl/attribute';
+import Model from 'mol-gl/model';
+import { createTransformAttributes } from 'mol-gl/renderable/util';
+import { calculateTextureInfo } from 'mol-gl/util';
+import Icosahedron from 'mol-geo/primitive/icosahedron'
+import Box from 'mol-geo/primitive/box'
+
+export default class State {
+    regl: REGL.Regl
+
+    initRegl (container: HTMLDivElement) {
+        const regl = glContext.create({
+            container,
+            extensions: [
+                'OES_texture_float',
+                'OES_texture_float_linear',
+                'OES_element_index_uint',
+                // 'ext_disjoint_timer_query',
+                'EXT_blend_minmax',
+                'ANGLE_instanced_arrays'
+            ],
+            // profile: true
+        })
+
+        const camera = Camera.create(regl, container, {
+            center: Vec3.create(0, 0, 0)
+        })
+
+        const p1 = Vec3.create(0, 4, 0)
+        const p2 = Vec3.create(-3, 0, 0)
+
+        const model1 = Model(regl)
+        const model2 = Model(regl, { position: p1 })
+        const model3 = Model(regl, { position: p2 })
+
+        const position = Attribute.create(regl, new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]), { size: 3 })
+        const normal = Attribute.create(regl, new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]), { size: 3 })
+
+        const transformArray1 = new Float32Array(16)
+        const transformArray2 = new Float32Array(16 * 3)
+        const m4 = Mat4.identity()
+        Mat4.toArray(m4, transformArray1, 0)
+        Mat4.toArray(m4, transformArray2, 0)
+        Mat4.setTranslation(m4, p1)
+        Mat4.toArray(m4, transformArray2, 16)
+        Mat4.setTranslation(m4, p2)
+        Mat4.toArray(m4, transformArray2, 32)
+
+
+
+        const colorTexInfo = calculateTextureInfo(3, 3)
+        const color = new Uint8Array(colorTexInfo.length)
+        color.set([
+            0, 0, 255,
+            0, 255, 0,
+            255, 0, 0
+        ])
+        console.log(color, colorTexInfo)
+        const colorTex = regl.texture({
+            width: colorTexInfo.width,
+            height: colorTexInfo.height,
+            format: 'rgb',
+            type: 'uint8',
+            wrapS: 'clamp',
+            wrapT: 'clamp',
+            data: color
+        })
+
+        // position.update((array: Float32Array) => {
+        //     positionFromModel({}, array, 0)
+        // })
+
+        const points = PointRenderable.create(regl, {
+            position,
+            ...createTransformAttributes(regl, transformArray1)
+        })
+        const mesh = MeshRenderable.create(regl,
+            {
+                position,
+                normal,
+                ...createTransformAttributes(regl, transformArray2)
+            },
+            {
+                colorTex,
+                colorTexSize: [ colorTexInfo.width, colorTexInfo.height ]
+            }
+        )
+
+        const sphere = Icosahedron(1, 1)
+        console.log(sphere)
+
+        const box = Box(1, 1, 1, 1, 1, 1)
+        console.log(box)
+
+        const points2 = PointRenderable.create(regl, {
+            position: Attribute.create(regl, new Float32Array(box.vertices), { size: 3 }),
+            ...createTransformAttributes(regl, transformArray1)
+        })
+
+        const mesh2 = MeshRenderable.create(regl,
+            {
+                position: Attribute.create(regl, new Float32Array(box.vertices), { size: 3 }),
+                normal: Attribute.create(regl, new Float32Array(box.normals), { size: 3 }),
+                ...createTransformAttributes(regl, transformArray2)
+            },
+            {
+                colorTex,
+                colorTexSize: [ colorTexInfo.width, colorTexInfo.height ],
+                'light.position': Vec3.create(0, 0, -20),
+                'light.color': Vec3.create(1.0, 1.0, 1.0),
+                'light.ambient': Vec3.create(0.5, 0.5, 0.5),
+                'light.falloff': 0,
+                'light.radius': 500
+            },
+            box.indices
+        )
+
+        const baseContext = regl({
+            context: {
+                model: Mat4.identity(),
+                transform: Mat4.setTranslation(Mat4.identity(), Vec3.create(6, 0, 0))
+            },
+            uniforms: {
+                model: regl.context('model' as any),
+                transform: regl.context('transform' as any),
+            }
+        })
+
+        regl.frame((ctx) => {
+            camera.update((state: any) => {
+                if (!camera.isDirty()) return
+                baseContext(() => {
+                    // console.log(ctx)
+                    regl.clear({color: [0, 0, 0, 1]})
+                    position.update(array => { array[0] = Math.random() })
+                    // points.update(a => { a.position[0] = Math.random() })
+                    // mesh.draw()
+                    // points.draw()
+                    mesh2.draw()
+                    points2.draw()
+                    // model1({}, ({ transform }) => {
+                    //     points.draw()
+                    // })
+                    // model2({}, ({ transform }) => {
+                    //     points.draw()
+                    //     model3({ transform }, () => {
+                    //         points.draw()
+                    //     })
+                    // })
+                })
+            }, undefined)
+        })
+
+        this.regl = regl
+    }
+
+    constructor() {
+
+    }
+}

+ 10 - 3
src/apps/render-test/ui.tsx

@@ -5,11 +5,18 @@
  */
 
 import * as React from 'react'
+import State from './state'
+
+export default class Root extends React.Component<{ state: State }, { }> {
+    private canvasContainer: HTMLDivElement | null = null;
+
+    componentDidMount() {
+        if (this.canvasContainer) this.props.state.initRegl(this.canvasContainer)
+    }
 
-export default class Root extends React.Component {
     render() {
-        return <div style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0, overflow: 'hidden' }}>
-            
+        return <div ref={elm => this.canvasContainer = elm} style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0, overflow: 'hidden' }}>
+
         </div>
     }
 }

+ 15 - 0
src/helpers.d.ts

@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+declare module Helpers {
+    export type Mutable<T> = {
+        -readonly [P in keyof T]: T[P]
+    }
+    export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array
+    export type NumberArray = TypedArray | number[]
+    export type UintArray = Uint8Array | Uint16Array | Uint32Array | number[]
+}

+ 95 - 0
src/mol-geo/primitive/box.ts

@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+// adapted from three.js, MIT License Copyright 2010-2018 three.js authors
+
+import { Vec3 } from 'mol-math/linear-algebra'
+
+export default function Box(width: number, height: number, depth: number, widthSegments: number, heightSegments: number, depthSegments: number) {
+    widthSegments = Math.floor(widthSegments)
+    heightSegments = Math.floor(heightSegments)
+    depthSegments = Math.floor(depthSegments)
+
+    // buffers
+    const indices: number[] = [];
+    const vertices: number[] = [];
+    const normals: number[] = [];
+
+    // helper variables
+    let numberOfVertices = 0;
+
+    // build each side of the box geometry
+    buildPlane(2, 1, 0, -1, -1, depth, height, width, depthSegments, heightSegments); // px
+    buildPlane(2, 1, 0, 1, -1, depth, height, -width, depthSegments, heightSegments); // nx
+    buildPlane(0, 2, 1, 1, 1, width, depth, height, widthSegments, depthSegments); // py
+    buildPlane(0, 2, 1, 1, -1, width, depth, -height, widthSegments, depthSegments); // ny
+    buildPlane(0, 1, 2, 1, -1, width, height, depth, widthSegments, heightSegments); // pz
+    buildPlane(0, 1, 2, -1, -1, width, height, -depth, widthSegments, heightSegments); // nz
+
+    return { vertices, indices, normals }
+
+    function buildPlane(u: number, v: number, w: number, udir: number, vdir: number, width: number, height: number, depth: number, gridX: number, gridY: number) {
+
+        const segmentWidth = width / gridX;
+        const segmentHeight = height / gridY;
+
+        const widthHalf = width / 2;
+        const heightHalf = height / 2;
+        const depthHalf = depth / 2;
+
+        const gridX1 = gridX + 1;
+        const gridY1 = gridY + 1;
+
+        let vertexCounter = 0;
+
+        const vector = Vec3.zero();
+
+        // generate vertices and normals
+        for (let iy = 0; iy < gridY1; ++iy) {
+            const y = iy * segmentHeight - heightHalf;
+            for (let ix = 0; ix < gridX1; ++ix) {
+                const x = ix * segmentWidth - widthHalf;
+
+                // set values to correct vector component
+                vector[ u ] = x * udir;
+                vector[ v ] = y * vdir;
+                vector[ w ] = depthHalf;
+
+                // now apply vector to vertex buffer
+                vertices.push(...vector);
+
+                // set values to correct vector component
+                vector[ u ] = 0;
+                vector[ v ] = 0;
+                vector[ w ] = depth > 0 ? 1 : -1;
+
+                // now apply vector to normal buffer
+                normals.push(...vector);
+
+                vertexCounter += 1;
+            }
+        }
+
+        // indices
+        // 1. you need three indices to draw a single face
+        // 2. a single segment consists of two faces
+        // 3. so we need to generate six (2*3) indices per segment
+        for (let iy = 0; iy < gridY; ++iy) {
+            for (let ix = 0; ix < gridX; ++ix) {
+                const a = numberOfVertices + ix + gridX1 * iy;
+                const b = numberOfVertices + ix + gridX1 * (iy + 1);
+                const c = numberOfVertices + (ix + 1) + gridX1 * (iy + 1);
+                const d = numberOfVertices + (ix + 1) + gridX1 * iy;
+
+                // faces
+                indices.push(a, b, d);
+                indices.push(b, c, d);
+            }
+        }
+
+        numberOfVertices += vertexCounter;
+    }
+}

+ 28 - 0
src/mol-geo/primitive/icosahedron.ts

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+// adapted from three.js, MIT License Copyright 2010-2018 three.js authors
+
+import Polyhedron from './polyhedron'
+
+const t = ( 1 + Math.sqrt( 5 ) ) / 2;
+
+const vertices = [
+    - 1, t, 0, 	1, t, 0, 	- 1, - t, 0, 	1, - t, 0,
+     0, - 1, t, 	0, 1, t,	0, - 1, - t, 	0, 1, - t,
+     t, 0, - 1, 	t, 0, 1, 	- t, 0, - 1, 	- t, 0, 1
+];
+
+const indices = [
+     0, 11, 5, 	0, 5, 1, 	0, 1, 7, 	0, 7, 10, 	0, 10, 11,
+     1, 5, 9, 	5, 11, 4,	11, 10, 2,	10, 7, 6,	7, 1, 8,
+     3, 9, 4, 	3, 4, 2,	3, 2, 6,	3, 6, 8,	3, 8, 9,
+     4, 9, 5, 	2, 4, 11,	6, 2, 10,	8, 6, 7,	9, 8, 1
+];
+
+export default function Icosahedron(radius: number, detail: number) {
+    return Polyhedron(vertices, indices, radius, detail)
+}

+ 94 - 0
src/mol-geo/primitive/polyhedron.ts

@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+// adapted from three.js, MIT License Copyright 2010-2018 three.js authors
+
+import { Vec3 } from 'mol-math/linear-algebra'
+import { computeVertexNormals, appplyRadius } from '../util'
+
+export default function Polyhedron(_vertices: Helpers.NumberArray, _indices: Helpers.NumberArray, radius: number, detail: number) {
+    radius = radius || 1;
+    detail = detail || 0;
+
+    const vertices: number[] = [];
+    const indices: number[] = [];
+
+    // the subdivision creates the vertex buffer data
+    subdivide(detail);
+
+    // all vertices should lie on a conceptual sphere with a given radius
+    appplyRadius(vertices, radius);
+
+    const normals = new Float32Array(vertices.length);
+    computeVertexNormals(vertices, normals)
+    // this.normalizeNormals(); // smooth normals
+
+    return { vertices, indices, normals }
+
+    // helper functions
+
+    function subdivide(detail: number) {
+        const a = Vec3.zero()
+        const b = Vec3.zero()
+        const c = Vec3.zero()
+
+        // iterate over all faces and apply a subdivison with the given detail value
+        for (let i = 0; i < _indices.length; i += 3) {
+            // get the vertices of the face
+            Vec3.fromArray(a, _vertices, _indices[ i + 0 ] * 3)
+            Vec3.fromArray(b, _vertices, _indices[ i + 1 ] * 3)
+            Vec3.fromArray(c, _vertices, _indices[ i + 2 ] * 3)
+
+            // perform subdivision
+            subdivideFace(a, b, c, detail)
+        }
+
+    }
+
+    function subdivideFace(a: Vec3, b: Vec3, c: Vec3, detail: number) {
+        const cols = Math.pow(2, detail)
+
+        // we use this multidimensional array as a data structure for creating the subdivision
+        const v: Vec3[][] = []
+
+        // construct all of the vertices for this subdivision
+        for (let i = 0; i <= cols; ++i) {
+            v[i] = []
+
+            const aj = Vec3.zero()
+            Vec3.lerp(aj, a, c, i / cols)
+
+            const bj = Vec3.zero()
+            Vec3.lerp(bj, b, c, i / cols)
+
+            const rows = cols - i
+            for (let j = 0; j <= rows; ++j) {
+                if (j === 0 && i === cols) {
+                    v[i][j] = aj
+                } else {
+                    const abj = Vec3.zero()
+                    Vec3.lerp(abj, aj, bj, j / rows)
+
+                    v[i][j] = abj
+                }
+            }
+        }
+
+        // construct all of the faces
+        for (let i = 0; i < cols; ++i) {
+            for (let j = 0; j < 2 * (cols - i) - 1; ++j) {
+                const k = Math.floor(j / 2)
+                if (j % 2 === 0) {
+                    vertices.push(...v[i][k + 1], ...v[i + 1][k], ...v[i][k])
+                } else {
+                    vertices.push(...v[i][k + 1], ...v[i + 1][k + 1], ...v[i + 1][k])
+                }
+            }
+        }
+
+        console.log(v)
+    }
+}

+ 0 - 0
src/mol-geo/representation/structure/spacefill.ts


+ 17 - 0
src/mol-geo/shape/point.ts

@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+
+export function countFromModel(model: any) {}
+export function positionFromModel(model: any, array: Float32Array, offset: number) {
+
+}
+export function colorFromModel(model: any, params: any, array: Float32Array, offset: number) {
+
+}
+export function sizeFromModel(model: any, params: any, array: Float32Array, offset: number) {
+
+}

+ 5 - 0
src/mol-geo/shape/sphere.ts

@@ -0,0 +1,5 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */

+ 113 - 0
src/mol-geo/util.ts

@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra'
+
+export function normalizeVec3array<T extends Helpers.NumberArray> (a: T) {
+    const n = a.length
+    for (let i = 0; i < n; i += 3) {
+        const x = a[ i ]
+        const y = a[ i + 1 ]
+        const z = a[ i + 2 ]
+        const s = 1 / Math.sqrt(x * x + y * y + z * z)
+        a[ i ] = x * s
+        a[ i + 1 ] = y * s
+        a[ i + 2 ] = z * s
+    }
+}
+
+export function setArrayZero(array: Helpers.NumberArray) {
+    const n = array.length
+    for (let i = 0; i < n; ++i) array[i] = 0
+}
+
+// iterate over the entire buffer and apply the radius to each vertex
+export function appplyRadius(vertices: Helpers.NumberArray, radius: number) {
+    const v = Vec3.zero()
+    const n = vertices.length
+    for (let i = 0; i < n; i += 3) {
+        Vec3.fromArray(v, vertices, i)
+        Vec3.normalize(v, v)
+        Vec3.scale(v, v, radius)
+        Vec3.toArray(v, vertices, i)
+    }
+}
+
+// indexed vertex normals weighted by triangle areas http://www.iquilezles.org/www/articles/normals/normals.htm
+// normal array must contain only zeros
+export function computeIndexedVertexNormals<T extends Helpers.NumberArray> (vertices: Helpers.NumberArray, indices: Helpers.NumberArray, normals: T) {
+    const a = Vec3.zero()
+    const b = Vec3.zero()
+    const c = Vec3.zero()
+    const cb = Vec3.zero()
+    const ab = Vec3.zero()
+
+    for (let i = 0, il = indices.length; i < il; i += 3) {
+        const ai = indices[ i ] * 3
+        const bi = indices[ i + 1 ] * 3
+        const ci = indices[ i + 2 ] * 3
+
+        Vec3.fromArray(a, vertices, ai)
+        Vec3.fromArray(b, vertices, bi)
+        Vec3.fromArray(c, vertices, ci)
+
+        Vec3.sub(cb, c, b)
+        Vec3.sub(ab, a, b)
+        Vec3.cross(cb, cb, ab)
+
+        normals[ ai ] += cb[ 0 ]
+        normals[ ai + 1 ] += cb[ 1 ]
+        normals[ ai + 2 ] += cb[ 2 ]
+
+        normals[ bi ] += cb[ 0 ]
+        normals[ bi + 1 ] += cb[ 1 ]
+        normals[ bi + 2 ] += cb[ 2 ]
+
+        normals[ ci ] += cb[ 0 ]
+        normals[ ci + 1 ] += cb[ 1 ]
+        normals[ ci + 2 ] += cb[ 2 ]
+    }
+
+    normalizeVec3array(normals)
+    return normals
+}
+
+// vertex normals for unindexed triangle soup
+// normal array must contain only zeros
+export function computeVertexNormals<T extends Helpers.NumberArray> (vertices: Helpers.NumberArray, normals: T) {
+    setArrayZero(normals)
+
+    const a = Vec3.zero()
+    const b = Vec3.zero()
+    const c = Vec3.zero()
+    const cb = Vec3.zero()
+    const ab = Vec3.zero()
+
+     for (let i = 0, il = vertices.length; i < il; i += 9) {
+        Vec3.fromArray(a, vertices, i)
+        Vec3.fromArray(b, vertices, i + 3)
+        Vec3.fromArray(c, vertices, i + 6)
+
+        Vec3.sub(cb, c, b)
+        Vec3.sub(ab, a, b)
+        Vec3.cross(cb, cb, ab)
+
+        normals[ i ] = cb[ 0 ]
+        normals[ i + 1 ] = cb[ 1 ]
+        normals[ i + 2 ] = cb[ 2 ]
+
+        normals[ i + 3 ] = cb[ 0 ]
+        normals[ i + 4 ] = cb[ 1 ]
+        normals[ i + 5 ] = cb[ 2 ]
+
+        normals[ i + 6 ] = cb[ 0 ]
+        normals[ i + 7 ] = cb[ 1 ]
+        normals[ i + 8 ] = cb[ 2 ]
+    }
+
+    normalizeVec3array(normals)
+    return normals
+}

+ 102 - 0
src/mol-gl/attribute.ts

@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import REGL = require('regl');
+
+// export type AttributeGroupMutator<T extends AttributesData> = (data: T) => (boolean | void)
+// export type AttributeGroupData = { [k: string]: Helpers.TypedArray }
+// export type AttributesBuffers<T extends AttributesData> = { [K in keyof T]: REGL.Buffer }
+
+// interface AttributeGroup<T extends AttributeGroup.Data> {
+//     readonly buffer: REGL.Buffer
+//     readonly length: number
+//     readonly data: T
+//     setCount(size: number): void
+//     update(mutator: AttributeGroup.Mutator<T>): void
+// }
+
+// namespace AttributeGroup {
+//     export type Data = { [k: string]: Helpers.TypedArray }
+//     export type Mutator<T extends Data> = (data: T) => (UpdateInfo<T> | void)
+//     export type UpdateInfo<T extends Data> = boolean | { [k in keyof T]: Attribute.UpdateInfo }
+//     export type Attributes<T extends Data> = { [K in keyof T]: Attribute<T[K]> }
+
+//     export function create<T extends Data>(regl: REGL.Regl, data: T): AttributeGroup<T> {
+//         const attributes: Attributes<any> = {}
+//         for (const k of Object.keys(data)) {
+//             attributes[k] = Attribute.create(regl, data[k])
+//         }
+//         return {
+//             update: (mutator: Mutator<T>) => {
+
+//             }
+//         }
+//     }
+// }
+
+
+interface Attribute<T extends Helpers.TypedArray> {
+    readonly buffer: REGL.AttributeConfig
+    getCount(): number
+    setCount(count: number): void
+    getArray(): T
+    set(index: number, ...values: number[]): void
+    update(mutator: Attribute.Mutator<T>): void
+    reload(): void
+}
+
+interface AttributeProps {
+    size: 1 | 2 | 3 | 4,
+    divisor?: number,
+    offset?: number,
+    stride?: number
+}
+
+namespace Attribute {
+    export type Mutator<T extends Helpers.TypedArray> = (data: T) => (UpdateInfo | void)
+    export type UpdateInfo = boolean | { offset: number, count: number }
+
+    export function create<T extends Helpers.TypedArray>(regl: REGL.Regl, array: T, props: AttributeProps): Attribute<T> {
+        const itemSize = props.size
+        let _array = array
+        let _count = _array.length / itemSize
+        if (props.stride) _count = _array.length / (props.stride / _array.BYTES_PER_ELEMENT)
+        console.log(_array.length, props.stride)
+        const buffer = regl.buffer(_array)
+        const attribute: REGL.AttributeConfig = { ...props, buffer }
+        const growIfNeeded = function(count: number) {
+            if (count * itemSize > _array.length) {
+                const newArray: T = new (_array as any).constructor(count * itemSize)
+                newArray.set(_array)
+                _array = newArray
+                buffer(_array)
+            }
+            _count = count
+        }
+        return {
+            buffer: attribute,
+            getCount: () =>  _count,
+            setCount: (count: number) => growIfNeeded(count),
+            getArray: () => _array,
+            set: (index: number, ...values: number[]) => {
+                if (values.length !== itemSize) throw new Error('wrong number of values given')
+                growIfNeeded(index)
+                for (let i = 0; i < itemSize; ++i) {
+                    _array[index * itemSize + i] = values[i]
+                }
+                buffer.subdata(values, index * itemSize * _array.BYTES_PER_ELEMENT)
+            },
+            update: (mutator: Mutator<T>, offset?: number, count?: number) => {
+                if (offset && count) growIfNeeded(offset + count)
+                mutator(_array)
+                buffer(_array)
+            },
+            reload: () => buffer(_array)
+        }
+    }
+}
+
+export default Attribute

+ 191 - 0
src/mol-gl/camera.ts

@@ -0,0 +1,191 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/regl-project/regl-camera,
+ * copyright (c) 2016 Mikola Lysenko. MIT License
+ */
+
+const isBrowser = typeof window !== 'undefined'
+
+import REGL = require('regl');
+
+import mouseChange, { MouseModifiers } from 'mol-util/mouse-change'
+import mouseWheel from 'mol-util/mouse-wheel'
+import { defaults } from 'mol-util'
+import { Mat4, Vec3 } from 'mol-math/linear-algebra/3d'
+import { clamp, damp } from 'mol-math/interpolate'
+
+export interface CameraUniforms {
+    projection: Mat4,
+}
+
+export interface CameraState {
+    center: Vec3,
+    theta: number,
+    phi: number,
+    distance: number,
+    eye: Vec3,
+    up: Vec3,
+    fovy: number,
+    near: number,
+    far: number,
+    noScroll: boolean,
+    flipY: boolean,
+    dtheta: number,
+    dphi: number,
+    rotationSpeed: number,
+    zoomSpeed: number,
+    renderOnDirty: boolean,
+    damping: number,
+    minDistance: number,
+    maxDistance: number,
+}
+
+export interface Camera {
+    update: (props: any, block: any) => void,
+    setState: (newState: CameraState) => void,
+    getState: () => CameraState,
+    isDirty: () => boolean
+}
+
+export namespace Camera {
+    export function create (regl: REGL.Regl, element: HTMLElement, initialState: Partial<CameraState> = {}): Camera {
+        const state: CameraState = {
+            center: defaults(initialState.center, Vec3.zero()),
+            theta: defaults(initialState.theta, 0),
+            phi: defaults(initialState.phi, 0),
+            distance: Math.log(defaults(initialState.distance, 10.0)),
+            eye: Vec3.zero(),
+            up: defaults(initialState.up, Vec3.create(0, 1, 0)),
+            fovy: defaults(initialState.fovy, Math.PI / 4.0),
+            near: defaults(initialState.near, 0.01),
+            far: defaults(initialState.far, 1000.0),
+            noScroll: defaults(initialState.noScroll, false),
+            flipY: defaults(initialState.flipY, false),
+            dtheta: 0,
+            dphi: 0,
+            rotationSpeed: defaults(initialState.rotationSpeed, 1),
+            zoomSpeed: defaults(initialState.zoomSpeed, 1),
+            renderOnDirty: defaults(initialState.renderOnDirty, false),
+            damping: defaults(initialState.damping, 0.9),
+            minDistance: Math.log(defaults(initialState.minDistance, 0.1)),
+            maxDistance: Math.log(defaults(initialState.maxDistance, 1000))
+        }
+
+        const view = Mat4.identity()
+        const projection = Mat4.identity()
+
+        const right = Vec3.create(1, 0, 0)
+        const front = Vec3.create(0, 0, 1)
+
+        let dirty = false
+        let ddistance = 0
+
+        let prevX = 0
+        let prevY = 0
+
+        if (isBrowser) {
+            const source = element || regl._gl.canvas
+
+            const getWidth = function () {
+                return element ? element.offsetWidth : window.innerWidth
+            }
+
+            const getHeight = function () {
+                return element ? element.offsetHeight : window.innerHeight
+            }
+
+            mouseChange(source, function (buttons: number, x: number, y: number, mods: MouseModifiers) {
+                if (buttons & 1) {
+                    const dx = (x - prevX) / getWidth()
+                    const dy = (y - prevY) / getHeight()
+
+                    state.dtheta += state.rotationSpeed * 4.0 * dx
+                    state.dphi += state.rotationSpeed * 4.0 * dy
+                    dirty = true;
+                }
+                prevX = x
+                prevY = y
+            })
+
+            mouseWheel(source, function (dx: number, dy: number) {
+                ddistance += dy / getHeight() * state.zoomSpeed
+                dirty = true;
+            }, state.noScroll)
+        }
+
+        function dampAndMarkDirty (x: number) {
+            const xd = damp(x, state.damping)
+            if (Math.abs(xd) < 0.1) return 0
+            dirty = true;
+            return xd
+        }
+
+        function setState (newState: Partial<CameraState> = {}) {
+            Object.assign(state, newState)
+
+            const { center, eye, up, dtheta, dphi } = state
+
+            state.theta += dtheta
+            state.phi = clamp(state.phi + dphi, -Math.PI / 2.0, Math.PI / 2.0)
+            state.distance = clamp(state.distance + ddistance, state.minDistance, state.maxDistance)
+
+            state.dtheta = dampAndMarkDirty(dtheta)
+            state.dphi = dampAndMarkDirty(dphi)
+            ddistance = dampAndMarkDirty(ddistance)
+
+            const theta = state.theta
+            const phi = state.phi
+            const r = Math.exp(state.distance)
+
+            const vf = r * Math.sin(theta) * Math.cos(phi)
+            const vr = r * Math.cos(theta) * Math.cos(phi)
+            const vu = r * Math.sin(phi)
+
+            for (let i = 0; i < 3; ++i) {
+                eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i]
+            }
+
+            Mat4.lookAt(view, eye, center, up)
+        }
+
+        const injectContext = regl({
+            context: {
+                view: () => view,
+                dirty: () => dirty,
+                projection: (context: REGL.DefaultContext) => {
+                    Mat4.perspective(
+                        projection,
+                        state.fovy,
+                        context.viewportWidth / context.viewportHeight,
+                        state.near,
+                        state.far
+                    )
+                    if (state.flipY) { projection[5] *= -1 }
+                    return projection
+                }
+            },
+            uniforms: {  // TODO
+                view: regl.context('view' as any),
+                projection: regl.context('projection' as any)
+            }
+        })
+
+        function update (props: any, block: any) {
+            setState()
+            injectContext(props, block)
+            dirty = false
+        }
+
+        return {
+            update,
+            setState,
+            getState: () => Object.assign({}, state),
+            isDirty: () => dirty
+        }
+    }
+}

+ 57 - 0
src/mol-gl/model.ts

@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import REGL = require('regl');
+import { Mat4, Quat, Vec3 } from 'mol-math/linear-algebra'
+import { defaults } from 'mol-util';
+
+const tmpMat4 = Mat4()
+
+type ModelProps = {
+    rotation?: Quat,
+    position?: Vec3,
+    scale?: Vec3
+}
+
+function createModel(regl: REGL.Regl, props: ModelProps = {}) {
+    const transform = Mat4.identity()
+
+    const rotation = defaults(props.rotation, Quat.identity())
+    const position = defaults(props.position, Vec3.zero())
+    const scale = defaults(props.scale, Vec3.create(1, 1, 1))
+
+    const draw = regl({
+        context: { transform, rotation, position, scale },
+
+        uniforms: {
+            model(ctx: REGL.DefaultContext, props: any = {}) {
+                const model = Mat4.identity()
+
+                if ('rotation' in props) Quat.copy(rotation, props.rotation)
+                if ('position' in props) Vec3.copy(position, props.position)
+                if ('scale' in props) Vec3.copy(scale, props.scale)
+
+                Mat4.translate(model, model, position)
+                Mat4.mul(model, model, Mat4.fromQuat(tmpMat4, rotation))
+                Mat4.scale(model, model, scale)
+
+                if ('transform' in props) Mat4.mul(model, props.transform, model)
+                Mat4.copy(transform, model)
+
+                return model
+            }
+        }
+    })
+
+    return Object.assign(draw, {
+        get transform() { return transform },
+        get position() { return position },
+        get rotation() { return rotation },
+        get scale() { return scale },
+    })
+}
+
+export default createModel

+ 21 - 0
src/mol-gl/renderable.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import REGL = require('regl');
+import Attribute from './attribute'
+import PointRenderable from './renderable/point'
+import MeshRenderable from './renderable/mesh'
+
+export type AttributesMutator<T extends AttributesData> = (data: T) => (boolean | void)
+export type AttributesData = { [k: string]: Helpers.TypedArray }
+export type Attributes<T extends AttributesData> = { [K in keyof T]: Attribute<T[K]> }
+export type AttributesBuffers<T extends AttributesData> = { [K in keyof T]: REGL.AttributeConfig }
+
+export interface Renderable<T extends AttributesData> {
+    draw(): void
+}
+
+export { PointRenderable, MeshRenderable }

+ 5 - 0
src/mol-gl/renderable/line.ts

@@ -0,0 +1,5 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */

+ 74 - 0
src/mol-gl/renderable/mesh.ts

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import REGL = require('regl');
+import { Renderable } from '../renderable'
+import { getBuffers } from './util'
+import Attribute from '../attribute';
+
+import { MeshShaders } from '../shaders'
+
+type Mesh = 'mesh'
+
+type Uniforms = { [k: string]: REGL.Uniform | REGL.Texture }
+
+export function fillSerial<T extends Helpers.NumberArray> (array: T) {
+    const n = array.length
+    for (let i = 0; i < n; ++i) array[ i ] = i
+    return array
+}
+
+namespace Mesh {
+    export type DataType = {
+        position: { type: Float32Array, itemSize: 3 }
+        normal: { type: Float32Array, itemSize: 3 }
+        transformColumn0: { type: Float32Array, itemSize: 4 }
+        transformColumn1: { type: Float32Array, itemSize: 4 }
+        transformColumn2: { type: Float32Array, itemSize: 4 }
+        transformColumn3: { type: Float32Array, itemSize: 4 }
+    }
+    export type Data = { [K in keyof DataType]: DataType[K]['type'] }
+    export type Attributes = { [K in keyof Data]: Attribute<Data[K]> }
+
+    export function create(regl: REGL.Regl, attributes: Attributes, uniforms: Uniforms, elements?: Helpers.UintArray): Renderable<Data> {
+        console.log('mesh', {
+            count: attributes.position.getCount(),
+            instances: attributes.transformColumn0.getCount(),
+            attributes,
+            uniforms
+        })
+        const instanceCount = attributes.transformColumn0.getCount()
+        const instanceId = fillSerial(new Float32Array(instanceCount))
+        console.log(instanceId)
+        const command = regl({
+            ...MeshShaders,
+            uniforms: {
+                objectId: uniforms.objectId || 0,
+                instanceCount,
+                ...uniforms
+            },
+            attributes: getBuffers({
+                instanceId: Attribute.create(regl, instanceId, { size: 1, divisor: 1 }),
+                ...attributes
+            }),
+            elements: elements && regl.elements({
+                data: new Uint16Array(elements),
+                primitive: 'triangles',
+                // type: 'uint16',
+                // count: elements.length / 3,
+                // length: elements.length * 2
+            }),
+            count: elements ? elements.length : attributes.position.getCount(),
+            instances: instanceCount,
+            primitive: 'triangles'
+        })
+        return {
+            draw: () => command(),
+        }
+    }
+}
+
+export default Mesh

+ 85 - 0
src/mol-gl/renderable/point.ts

@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import REGL = require('regl');
+import { Renderable } from '../renderable'
+import { getBuffers } from './util'
+import Attribute from '../attribute';
+import { PointShaders } from '../shaders'
+
+type Point = 'point'
+
+namespace Point {
+    export type DataType = {
+        position: { type: Float32Array, itemSize: 3 }
+        transformColumn0: { type: Float32Array, itemSize: 4 }
+        transformColumn1: { type: Float32Array, itemSize: 4 }
+        transformColumn2: { type: Float32Array, itemSize: 4 }
+        transformColumn3: { type: Float32Array, itemSize: 4 }
+    }
+    export type Data = { [K in keyof DataType]: DataType[K]['type'] }
+    export type Attributes = { [K in keyof Data]: Attribute<Data[K]> }
+
+    export function create(regl: REGL.Regl, attributes: Attributes): Renderable<Data> {
+        console.log('point', {
+            count: attributes.position.getCount(),
+            instances: attributes.transformColumn0.getCount(),
+        }, attributes)
+        const command = regl({
+            ...PointShaders,
+            attributes: getBuffers(attributes),
+            count: attributes.position.getCount(),
+            instances: attributes.transformColumn0.getCount(),
+            primitive: 'points'
+        })
+        return {
+            draw: () => command(),
+        }
+    }
+}
+
+export default Point
+
+// namespace Point {
+//     export type DataType = {
+//         position: { type: Float32Array, itemSize: 3 }
+//     }
+//     export type Data = { [K in keyof DataType]: DataType[K]['type'] }
+//     export type Attributes = { [K in keyof Data]: Attribute<Data[K]> }
+
+//     export function create(regl: REGL.Regl, dataOrCount: Data | number): Renderable<Data> {
+//         let count: number
+//         let data: Data
+//         if (typeof dataOrCount === 'number') {
+//             count = dataOrCount
+//             data = {
+//                 position: new Float32Array(count * 3)
+//             }
+//         } else {
+//             count = dataOrCount.position.length / 3
+//             data = dataOrCount
+//         }
+//         const attributes = createAttributes(regl, data)
+//         const command = regl({
+//             vert: pointVert,
+//             frag: pointFrag,
+//             attributes: getBuffers(attributes),
+//             count,
+//             primitive: 'points'
+//         })
+//         return {
+//             draw: () => command(),
+//             setCount: (newCount: number) => {
+//                 for (const k of Object.keys(data)) {
+//                     attributes[k as keyof Data].setCount(newCount)
+//                 }
+//                 count = newCount
+//             },
+//             getCount: () => count,
+//             attributes
+//         }
+//     }
+// }

+ 30 - 0
src/mol-gl/renderable/util.ts

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import REGL = require('regl');
+import { Attributes, AttributesData, AttributesBuffers } from '../renderable'
+import Attribute from '../attribute'
+
+export function createTransformAttributes (regl: REGL.Regl, transform: Float32Array) {
+    const size = 4
+    const divisor = 1
+    const bpe = transform.BYTES_PER_ELEMENT
+    const stride = 16 * bpe
+    return {
+        transformColumn0: Attribute.create(regl, transform, { size, divisor, offset: 0, stride }),
+        transformColumn1: Attribute.create(regl, transform, { size, divisor, offset: 4 * bpe, stride }),
+        transformColumn2: Attribute.create(regl, transform, { size, divisor, offset: 8 * bpe, stride }),
+        transformColumn3: Attribute.create(regl, transform, { size, divisor, offset: 12 * bpe, stride })
+    }
+}
+
+export function getBuffers<T extends AttributesData>(attributes: Attributes<T>): AttributesBuffers<T> {
+    const buffers: AttributesBuffers<any> = {}
+    for (const k of Object.keys(attributes)) {
+        buffers[k] = attributes[k].buffer
+    }
+    return buffers as AttributesBuffers<T>
+}

+ 14 - 0
src/mol-gl/shader/attenuation.glsl

@@ -0,0 +1,14 @@
+// by Tom Madams
+// Simple:
+// https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/
+//
+// Improved
+// https://imdoingitwrong.wordpress.com/2011/02/10/improved-light-attenuation/
+float attenuation(float r, float f, float d) {
+    float denom = d / r + 1.0;
+    float attenuation = 1.0 / (denom*denom);
+    float t = (attenuation - f) / (1.0 - f);
+    return max(t, 0.0);
+}
+
+#pragma glslify: export(attenuation)

+ 70 - 0
src/mol-gl/shader/inverse.glsl

@@ -0,0 +1,70 @@
+// (c) 2014 Mikola Lysenko. MIT License
+// https://github.com/glslify/glsl-inverse
+
+float inverse(float m) {
+  return 1.0 / m;
+}
+
+mat2 inverse(mat2 m) {
+  return mat2(m[1][1],-m[0][1],
+             -m[1][0], m[0][0]) / (m[0][0]*m[1][1] - m[0][1]*m[1][0]);
+}
+
+mat3 inverse(mat3 m) {
+  float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2];
+  float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2];
+  float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2];
+
+  float b01 = a22 * a11 - a12 * a21;
+  float b11 = -a22 * a10 + a12 * a20;
+  float b21 = a21 * a10 - a11 * a20;
+
+  float det = a00 * b01 + a01 * b11 + a02 * b21;
+
+  return mat3(b01, (-a22 * a01 + a02 * a21), (a12 * a01 - a02 * a11),
+              b11, (a22 * a00 - a02 * a20), (-a12 * a00 + a02 * a10),
+              b21, (-a21 * a00 + a01 * a20), (a11 * a00 - a01 * a10)) / det;
+}
+
+mat4 inverse(mat4 m) {
+  float
+      a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3],
+      a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3],
+      a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3],
+      a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3],
+
+      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,
+
+      det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+  return mat4(
+      a11 * b11 - a12 * b10 + a13 * b09,
+      a02 * b10 - a01 * b11 - a03 * b09,
+      a31 * b05 - a32 * b04 + a33 * b03,
+      a22 * b04 - a21 * b05 - a23 * b03,
+      a12 * b08 - a10 * b11 - a13 * b07,
+      a00 * b11 - a02 * b08 + a03 * b07,
+      a32 * b02 - a30 * b05 - a33 * b01,
+      a20 * b05 - a22 * b02 + a23 * b01,
+      a10 * b10 - a11 * b08 + a13 * b06,
+      a01 * b08 - a00 * b10 - a03 * b06,
+      a30 * b04 - a31 * b02 + a33 * b00,
+      a21 * b02 - a20 * b04 - a23 * b00,
+      a11 * b07 - a10 * b09 - a12 * b06,
+      a00 * b09 - a01 * b07 + a02 * b06,
+      a31 * b01 - a30 * b03 - a32 * b00,
+      a20 * b03 - a21 * b01 + a22 * b00) / det;
+}
+
+#pragma glslify: export(inverse)

+ 75 - 0
src/mol-gl/shader/mesh.frag

@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision mediump float;
+
+struct Light {
+    vec3 position;
+    vec3 color;
+    vec3 ambient;
+    float falloff;
+    float radius;
+};
+
+uniform Light light;
+uniform mat4 view;
+
+varying vec3 vNormal, vViewPosition, vColor;
+
+float phongSpecular(vec3 lightDirection, vec3 viewDirection, vec3 surfaceNormal, float shininess) {
+    //Calculate Phong power
+    vec3 R = -reflect(lightDirection, surfaceNormal);
+    return pow(max(0.0, dot(viewDirection, R)), shininess);
+}
+
+#define PI 3.14159265
+
+float orenNayarDiffuse(vec3 lightDirection, vec3 viewDirection, vec3 surfaceNormal, float roughness, float albedo) {
+    float LdotV = dot(lightDirection, viewDirection);
+    float NdotL = dot(lightDirection, surfaceNormal);
+    float NdotV = dot(surfaceNormal, viewDirection);
+
+    float s = LdotV - NdotL * NdotV;
+    float t = mix(1.0, max(NdotL, NdotV), step(0.0, s));
+
+    float sigma2 = roughness * roughness;
+    float A = 1.0 + sigma2 * (albedo / (sigma2 + 0.13) + 0.5 / (sigma2 + 0.33));
+    float B = 0.45 * sigma2 / (sigma2 + 0.09);
+
+    return albedo * max(0.0, NdotL) * (A + B * s / t) / PI;
+}
+
+#pragma glslify: attenuation = require(./attenuation.glsl)
+
+const float specularScale = 0.65;
+const float shininess = 10.0;
+const float roughness = 5.0;
+const float albedo = 0.95;
+
+void main() {
+    // determine surface to light direction
+    vec4 lightPosition = view * vec4(light.position, 1.0);
+    vec3 lightVector = lightPosition.xyz - vViewPosition;
+
+    // calculate attenuation
+    float lightDistance = length(lightVector);
+    float falloff = 1.0; // attenuation(light.radius, light.falloff, lightDistance);
+
+    vec3 L = normalize(lightVector); // light direction
+    vec3 V = normalize(vViewPosition); // eye direction
+    vec3 N = normalize(vNormal); // surface normal
+
+    // compute our diffuse & specular terms
+    float specular = phongSpecular(L, V, N, shininess) * specularScale * falloff;
+    vec3 diffuse = light.color * orenNayarDiffuse(L, V, N, roughness, albedo) * falloff;
+    vec3 ambient = light.ambient;
+
+    // add the lighting
+    vec3 color = vColor * (diffuse + ambient) + specular;
+
+    gl_FragColor.rgb = N;
+    gl_FragColor.a = 1.0;
+}

+ 58 - 0
src/mol-gl/shader/mesh.vert

@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+#define INSTANCE_COLOR
+
+precision mediump float;
+
+uniform mat4 projection, model, view;
+
+uniform int objectId;
+uniform int instanceCount;
+
+#if defined( ATTRIBUTE_COLOR )
+    attribute vec3 color;
+#elif defined( INSTANCE_COLOR ) || defined( ELEMENT_COLOR )
+    uniform vec2 colorTexSize;
+    uniform sampler2D colorTex;
+#endif
+
+attribute vec3 position;
+attribute vec3 normal;
+attribute vec4 transformColumn0, transformColumn1, transformColumn2, transformColumn3;
+attribute float instanceId;
+// attribute int elementId;
+
+varying vec3 vColor;
+varying vec3 vNormal;
+varying vec3 vViewPosition;
+
+#pragma glslify: inverse = require(./inverse.glsl)
+#pragma glslify: read_vec3 = require(./read-vec3.glsl)
+#pragma glslify: transpose = require(./transpose.glsl)
+
+void main(){
+    #if defined( ATTRIBUTE_COLOR )
+        vColor = color;
+    #elif defined( INSTANCE_COLOR )
+        vColor = read_vec3(colorTex, instanceId, colorTexSize);
+    // #elif defined( ELEMENT_COLOR )
+    //     vColor = read_vec3(colorTex, instanceId * instanceCount + elementId, colorTexSize);
+    #else
+        vColor = vec3(0.0, 1.0, 0.0);
+    #endif
+
+    mat4 transform = mat4(transformColumn0, transformColumn1, transformColumn2, transformColumn3);
+    mat4 modelView = view * model * transform;
+
+    vec4 mvPosition = modelView * vec4(position, 1.0);
+    vViewPosition = mvPosition.xyz;
+    gl_Position = projection * mvPosition;
+
+    // TODO do on CPU side
+    mat3 normalMatrix = transpose(inverse(mat3(modelView)));
+    vNormal = normalize(normalMatrix * normal);
+}

+ 9 - 0
src/mol-gl/shader/point.frag

@@ -0,0 +1,9 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+void main(){
+    gl_FragColor = vec4(1, 0, 0, 1);
+}

+ 26 - 0
src/mol-gl/shader/point.vert

@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision mediump float;
+
+uniform mat4 projection, model, view;
+
+attribute vec3 position; //, color;
+attribute vec4 transformColumn0, transformColumn1, transformColumn2, transformColumn3;
+// attribute int instanceId;
+
+// instanced
+// attribute mat4 transform;
+// uniform mat4 transform;
+
+// varying vec3 vColor;
+
+void main(){
+    mat4 transform = mat4(transformColumn0, transformColumn1, transformColumn2, transformColumn3);
+    // vColor = color;
+    gl_PointSize = 20.0;
+    gl_Position = projection * view * model * transform * vec4(position, 1.0);
+}

+ 13 - 0
src/mol-gl/shader/read-vec3.glsl

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+vec3 read_vec3 (sampler2D tex, float i, vec2 size) {
+    float x = mod(i, size.x);
+    float y = floor(i / size.x);
+    vec2 uv = (vec2(x, y) + 0.5) / size;
+    return texture2D(tex, uv).rgb;
+}
+#pragma glslify: export(read_vec3)

+ 26 - 0
src/mol-gl/shader/transpose.glsl

@@ -0,0 +1,26 @@
+// (c) 2014 Mikola Lysenko. MIT License
+// https://github.com/glslify/glsl-transpose
+
+float transpose(float m) {
+  return m;
+}
+
+mat2 transpose(mat2 m) {
+  return mat2(m[0][0], m[1][0],
+              m[0][1], m[1][1]);
+}
+
+mat3 transpose(mat3 m) {
+  return mat3(m[0][0], m[1][0], m[2][0],
+              m[0][1], m[1][1], m[2][1],
+              m[0][2], m[1][2], m[2][2]);
+}
+
+mat4 transpose(mat4 m) {
+  return mat4(m[0][0], m[1][0], m[2][0], m[3][0],
+              m[0][1], m[1][1], m[2][1], m[3][1],
+              m[0][2], m[1][2], m[2][2], m[3][2],
+              m[0][3], m[1][3], m[2][3], m[3][3]);
+}
+
+#pragma glslify: export(transpose)

+ 12 - 0
src/mol-gl/shaders.ts

@@ -0,0 +1,12 @@
+
+const PointShaders = {
+    vert: require('mol-gl/shader/point.vert'),
+    frag: require('mol-gl/shader/point.frag')
+}
+
+const MeshShaders = {
+    vert: require('mol-gl/shader/mesh.vert'),
+    frag: require('mol-gl/shader/mesh.frag')
+}
+
+export { PointShaders, MeshShaders }

+ 13 - 0
src/mol-gl/util.ts

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export function calculateTextureInfo (n: number, itemSize: number) {
+    const sqN = Math.sqrt(n * itemSize)
+    let width = Math.ceil(sqN)
+    width = width + (itemSize - (width % itemSize)) % itemSize
+    const height = width > 0 ? Math.ceil(n * itemSize / width) : 0
+    return { width, height, length: width * height * itemSize }
+}

+ 61 - 0
src/mol-math/interpolate.ts

@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export function normalize (value: number, min: number, max: number) {
+    return (value - min) / (max - min)
+}
+
+export function clamp (value: number, min: number, max: number) {
+    return Math.max(min, Math.min(max, value))
+}
+
+export function pclamp (value: number) {
+    return clamp(value, 0, 100)
+}
+
+export function saturate (value: number) {
+    return clamp(value, 0, 1)
+}
+
+export function damp (value: number, dampingFactor: number) {
+    const dampedValue = value * dampingFactor
+    return Math.abs(dampedValue) < 0.1 ? 0 : dampedValue
+}
+
+export function lerp (start: number, stop: number, alpha: number) {
+    return start + (stop - start) * alpha
+}
+
+export function spline (p0: number, p1: number, p2: number, p3: number, t: number, tension: number) {
+    const v0 = (p2 - p0) * tension
+    const v1 = (p3 - p1) * tension
+    const t2 = t * t
+    const t3 = t * t2
+    return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1
+}
+
+export function smoothstep (min: number, max: number, x: number) {
+    x = saturate(normalize(x, min, max))
+    return x * x * (3 - 2 * x)
+}
+
+export function smootherstep (min: number, max: number, x: number) {
+    x = saturate(normalize(x, min, max))
+    return x * x * x * (x * (x * 6 - 15) + 10)
+}
+
+export function smootheststep (min: number, max: number, x: number) {
+    x = saturate(normalize(x, min, max))
+    return -20 * Math.pow(x, 7) + 70 * Math.pow(x, 6) - 84 * Math.pow(x, 5) + 35 * Math.pow(x, 4)
+}
+
+export function almostIdentity (value: number, start: number, stop: number) {
+    if (value > start) return value
+    const a = 2 * stop - start
+    const b = 2 * start - 3 * stop
+    const t = value / start
+    return (a * t + b) * t * t + stop
+}

+ 673 - 4
src/mol-math/linear-algebra/3d.ts

@@ -2,6 +2,7 @@
  * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 /*
@@ -16,9 +17,11 @@
  * furnished to do so, subject to the following conditions:
  */
 
-export interface Mat4 { [d: number]: number, '@type': 'mat4' }
-export interface Vec3 { [d: number]: number, '@type': 'vec3' | 'vec4' }
-export interface Vec4 { [d: number]: number, '@type': 'vec4' }
+export interface Mat4 extends Array<number> { [d: number]: number, '@type': 'mat4', length: 16 }
+export interface Mat3 extends Array<number> { [d: number]: number, '@type': 'mat3', length: 9 }
+export interface Vec3 extends Array<number> { [d: number]: number, '@type': 'vec3', length: 3 }
+export interface Vec4 extends Array<number> { [d: number]: number, '@type': 'vec4', length: 4 }
+export interface Quat extends Array<number> { [d: number]: number, '@type': 'quat', length: 4 }
 
 const enum EPSILON { Value = 0.000001 }
 
@@ -26,6 +29,10 @@ export function Mat4() {
     return Mat4.zero();
 }
 
+export function Quat() {
+    return Quat.zero();
+}
+
 /**
  * Stores a 4x4 matrix in a column major (j * 4 + i indexing) format.
  */
@@ -77,7 +84,7 @@ export namespace Mat4 {
         mat[15] = 1;
         return mat;
     }
-    
+
     export function ofRows(rows: number[][]): Mat4 {
         const out = zero();
         for (let i = 0; i < 4; i++) {
@@ -105,6 +112,44 @@ export namespace Mat4 {
         a[4 * j + i] = value;
     }
 
+    export function toArray(a: Mat4, out: Helpers.NumberArray, offset: number) {
+        out[offset + 0] = a[0];
+        out[offset + 1] = a[1];
+        out[offset + 2] = a[2];
+        out[offset + 3] = a[3];
+        out[offset + 4] = a[4];
+        out[offset + 5] = a[5];
+        out[offset + 6] = a[6];
+        out[offset + 7] = a[7];
+        out[offset + 8] = a[8];
+        out[offset + 9] = a[9];
+        out[offset + 10] = a[10];
+        out[offset + 11] = a[11];
+        out[offset + 12] = a[12];
+        out[offset + 13] = a[13];
+        out[offset + 14] = a[14];
+        out[offset + 15] = a[15];
+    }
+
+    export function fromArray(a: Mat4, array: Helpers.NumberArray, offset: number) {
+        a[0] = array[offset + 0]
+        a[1] = array[offset + 1]
+        a[2] = array[offset + 2]
+        a[3] = array[offset + 3]
+        a[4] = array[offset + 4]
+        a[5] = array[offset + 5]
+        a[6] = array[offset + 6]
+        a[7] = array[offset + 7]
+        a[8] = array[offset + 8]
+        a[9] = array[offset + 9]
+        a[10] = array[offset + 10]
+        a[11] = array[offset + 11]
+        a[12] = array[offset + 12]
+        a[13] = array[offset + 13]
+        a[14] = array[offset + 14]
+        a[15] = array[offset + 15]
+    }
+
     export function copy(out: Mat4, a: Mat4) {
         out[0] = a[0];
         out[1] = a[1];
@@ -129,6 +174,45 @@ export namespace Mat4 {
         return Mat4.copy(Mat4.zero(), a);
     }
 
+    export function transpose(out: Mat4, a: Mat4) {
+        // If we are transposing ourselves we can skip a few steps but have to cache some values
+        if (out === a) {
+            const a01 = a[1], a02 = a[2], a03 = a[3];
+            const a12 = a[6], a13 = a[7];
+            const 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;
+    }
+
     export function invert(out: Mat4, a: Mat4) {
         const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
             a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
@@ -463,6 +547,213 @@ export namespace Mat4 {
         }
         return true;
     }
+
+    export function fromQuat(out: Mat4, q: Quat) {
+        const x = q[0], y = q[1], z = q[2], w = q[3];
+        const x2 = x + x;
+        const y2 = y + y;
+        const z2 = z + z;
+
+        const xx = x * x2;
+        const yx = y * x2;
+        const yy = y * y2;
+        const zx = z * x2;
+        const zy = z * y2;
+        const zz = z * z2;
+        const wx = w * x2;
+        const wy = w * y2;
+        const 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
+     */
+    export function frustum(out: Mat4, left: number, right: number, bottom: number, top: number, near: number, far: number) {
+        let rl = 1 / (right - left);
+        let tb = 1 / (top - bottom);
+        let 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
+     */
+    export function perspective(out: Mat4, fovy: number, aspect: number, near: number, far: number) {
+        let f = 1.0 / Math.tan(fovy / 2);
+        let 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
+     */
+    export function ortho(out: Mat4, left: number, right: number, bottom: number, top: number, near: number, far: number) {
+        let lr = 1 / (left - right);
+        let bt = 1 / (bottom - top);
+        let 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
+     */
+    export function lookAt(out: Mat4, eye: Vec3, center: Vec3, up: Vec3) {
+        let x0, x1, x2, y0, y1, y2, z0, z1, z2, len;
+        let eyex = eye[0];
+        let eyey = eye[1];
+        let eyez = eye[2];
+        let upx = up[0];
+        let upy = up[1];
+        let upz = up[2];
+        let centerx = center[0];
+        let centery = center[1];
+        let centerz = center[2];
+
+        if (Math.abs(eyex - centerx) < EPSILON.Value &&
+            Math.abs(eyey - centery) < EPSILON.Value &&
+            Math.abs(eyez - centerz) < EPSILON.Value
+        ) {
+            return setIdentity(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;
+    }
+}
+
+export namespace Mat3 {
+    export function zero(): Mat3 {
+        // force double backing array by 0.1.
+        const ret = [0.1, 0, 0, 0, 0, 0, 0, 0, 0];
+        ret[0] = 0.0;
+        return ret as any;
+    }
 }
 
 export namespace Vec3 {
@@ -488,6 +779,18 @@ export namespace Vec3 {
         return { x: v[0], y: v[1], z: v[2] };
     }
 
+    export function fromArray(v: Vec3, array: Helpers.NumberArray, offset: number) {
+        v[0] = array[offset + 0]
+        v[1] = array[offset + 1]
+        v[2] = array[offset + 2]
+    }
+
+    export function toArray(v: Vec3, out: Helpers.NumberArray, offset: number) {
+        out[offset + 0] = v[0]
+        out[offset + 1] = v[1]
+        out[offset + 2] = v[2]
+    }
+
     export function create(x: number, y: number, z: number): Vec3 {
         const out = zero();
         out[0] = x;
@@ -668,6 +971,14 @@ export namespace Vec4 {
         return out;
     }
 
+    export function copy(out: Vec4, a: Vec4) {
+        out[0] = a[0];
+        out[1] = a[1];
+        out[2] = a[2];
+        out[3] = a[3];
+        return out;
+    }
+
     export function set(out: Vec4, x: number, y: number, z: number, w: number) {
         out[0] = x;
         out[1] = y;
@@ -676,6 +987,14 @@ export namespace Vec4 {
         return out;
     }
 
+    export function add(out: Quat, a: Quat, b: Quat) {
+        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;
+    }
+
     export function distance(a: Vec4, b: Vec4) {
         const x = b[0] - a[0],
             y = b[1] - a[1],
@@ -716,4 +1035,354 @@ export namespace Vec4 {
         out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
         return out;
     }
+}
+
+export namespace Quat {
+    export function zero(): Quat {
+        // force double backing array by 0.1.
+        const ret = [0.1, 0, 0, 0];
+        ret[0] = 0.0;
+        return ret as any;
+    }
+
+    export function identity(): Quat {
+        const out = zero();
+        out[3] = 1;
+        return out;
+    }
+
+    export function create(x: number, y: number, z: number, w: number) {
+        const out = identity();
+        out[0] = x;
+        out[1] = y;
+        out[2] = z;
+        out[3] = w;
+        return out;
+    }
+
+    export function setAxisAngle(out: Quat, axis: Vec3, rad: number) {
+        rad = rad * 0.5;
+        let 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;
+    }
+
+    /**
+     * Gets the rotation axis and angle for a given
+     *  quaternion. If a quaternion is created with
+     *  setAxisAngle, this method will return the same
+     *  values as providied in the original parameter list
+     *  OR functionally equivalent values.
+     * Example: The quaternion formed by axis [0, 0, 1] and
+     *  angle -90 is the same as the quaternion formed by
+     *  [0, 0, 1] and 270. This method favors the latter.
+     */
+    export function getAxisAngle(out_axis: Vec3, q: Quat) {
+        let rad = Math.acos(q[3]) * 2.0;
+        let s = Math.sin(rad / 2.0);
+        if (s !== 0.0) {
+            out_axis[0] = q[0] / s;
+            out_axis[1] = q[1] / s;
+            out_axis[2] = q[2] / s;
+        } else {
+            // If s is zero, return any axis (no rotation - axis does not matter)
+            out_axis[0] = 1;
+            out_axis[1] = 0;
+            out_axis[2] = 0;
+        }
+        return rad;
+    }
+
+    export function multiply(out: Quat, a: Quat, b: Quat) {
+        let ax = a[0], ay = a[1], az = a[2], aw = a[3];
+        let 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;
+    }
+
+    export function rotateX(out: Quat, a: Quat, rad: number) {
+        rad *= 0.5;
+
+        let ax = a[0], ay = a[1], az = a[2], aw = a[3];
+        let 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;
+    }
+
+    export function rotateY(out: Quat, a: Quat, rad: number) {
+        rad *= 0.5;
+
+        let ax = a[0], ay = a[1], az = a[2], aw = a[3];
+        let 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;
+    }
+
+    export function rotateZ(out: Quat, a: Quat, rad: number) {
+        rad *= 0.5;
+
+        let ax = a[0], ay = a[1], az = a[2], aw = a[3];
+        let 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.
+     */
+    export function calculateW(out: Quat, a: Quat) {
+        let 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;
+    }
+
+    /**
+     * Performs a spherical linear interpolation between two quat
+     */
+    export function slerp(out: Quat, a: Quat, b: Quat, t: number) {
+        // benchmarks:
+        //    http://jsperf.com/quaternion-slerp-implementations
+        let ax = a[0], ay = a[1], az = a[2], aw = a[3];
+        let bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+        let 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;
+    }
+
+    export function invert(out: Quat, a: Quat) {
+        let a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3];
+        let dot = a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3;
+        let 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.
+     */
+    export function conjugate(out: Quat, a: Quat) {
+        out[0] = -a[0];
+        out[1] = -a[1];
+        out[2] = -a[2];
+        out[3] = a[3];
+        return out;
+    }
+
+    /**
+     * 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.
+     */
+    export function fromMat3(out: Quat, m: Mat3) {
+        // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes
+        // article "Quaternion Calculus and Fast Animation".
+        const fTrace = m[0] + m[4] + m[8];
+        let 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[5]-m[7])*fRoot;
+            out[1] = (m[6]-m[2])*fRoot;
+            out[2] = (m[1]-m[3])*fRoot;
+            } else {
+            // |w| <= 1/2
+            let i = 0;
+            if ( m[4] > m[0] ) i = 1;
+            if ( m[8] > m[i*3+i] ) i = 2;
+            let j = (i+1)%3;
+            let 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[j*3+k] - m[k*3+j]) * 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;
+    }
+
+    export function clone(a: Quat) {
+        const out = zero();
+        out[0] = a[0];
+        out[1] = a[1];
+        out[2] = a[2];
+        out[3] = a[3];
+        return out;
+    }
+
+    export function copy(out: Quat, a: Quat) {
+        out[0] = a[0];
+        out[1] = a[1];
+        out[2] = a[2];
+        out[3] = a[3];
+        return out;
+    }
+
+    export function set(out: Quat, x: number, y: number, z: number, w: number) {
+        out[0] = x;
+        out[1] = y;
+        out[2] = z;
+        out[3] = w;
+        return out;
+    }
+
+    export function add(out: Quat, a: Quat, b: Quat) {
+        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;
+    }
+
+    export function normalize(out: Quat, a: Quat) {
+        let x = a[0];
+        let y = a[1];
+        let z = a[2];
+        let w = a[3];
+        let len = x*x + y*y + z*z + w*w;
+        if (len > 0) {
+            len = 1 / Math.sqrt(len);
+            out[0] = x * len;
+            out[1] = y * len;
+            out[2] = z * len;
+            out[3] = w * len;
+        }
+        return out;
+    }
+
+    /**
+     * Sets a quaternion to represent the shortest rotation from one
+     * vector to another.
+     *
+     * Both vectors are assumed to be unit length.
+     */
+    const rotTmpVec3 = Vec3.zero();
+    const rotTmpVec3UnitX = Vec3.create(1, 0, 0);
+    const rotTmpVec3UnitY = Vec3.create(0, 1, 0);
+    export function rotationTo(out: Quat, a: Vec3, b: Vec3) {
+        let dot = Vec3.dot(a, b);
+        if (dot < -0.999999) {
+            Vec3.cross(rotTmpVec3, rotTmpVec3UnitX, a);
+            if (Vec3.magnitude(rotTmpVec3) < 0.000001)
+            Vec3.cross(rotTmpVec3, rotTmpVec3UnitY, a);
+            Vec3.normalize(rotTmpVec3, rotTmpVec3);
+            setAxisAngle(out, rotTmpVec3, 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(rotTmpVec3, a, b);
+            out[0] = rotTmpVec3[0];
+            out[1] = rotTmpVec3[1];
+            out[2] = rotTmpVec3[2];
+            out[3] = 1 + dot;
+            return normalize(out, out);
+        }
+    }
+
+    /**
+     * Performs a spherical linear interpolation with two control points
+     */
+    let sqlerpTemp1 = Quat.zero();
+    let sqlerpTemp2 = Quat.zero();
+    export function sqlerp(out: Quat, a: Quat, b: Quat, c: Quat, d: Quat, t: number) {
+        slerp(sqlerpTemp1, a, d, t);
+        slerp(sqlerpTemp2, b, c, t);
+        slerp(out, sqlerpTemp1, sqlerpTemp2, 2 * t * (1 - t));
+        return 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.
+     */
+    const axesTmpMat = Mat3.zero();
+    export function setAxes(out: Quat, view: Vec3, right: Vec3, up: Vec3) {
+        axesTmpMat[0] = right[0];
+        axesTmpMat[3] = right[1];
+        axesTmpMat[6] = right[2];
+
+        axesTmpMat[1] = up[0];
+        axesTmpMat[4] = up[1];
+        axesTmpMat[7] = up[2];
+
+        axesTmpMat[2] = -view[0];
+        axesTmpMat[5] = -view[1];
+        axesTmpMat[8] = -view[2];
+
+        return normalize(out, Quat.fromMat3(out, axesTmpMat));
+    }
 }

+ 4 - 0
src/mol-util/index.ts

@@ -21,4 +21,8 @@ export function arrayEqual<T>(arr1: T[], arr2: T[]) {
         }
     }
     return true
+}
+
+export function defaults (value: any, defaultValue: any) {
+    return value !== undefined ? value : defaultValue
 }

+ 194 - 0
src/mol-util/mouse-change.ts

@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/mouse-change,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import * as mouse from './mouse-event'
+
+export type MouseModifiers = {
+    shift: boolean,
+    alt: boolean,
+    control: boolean,
+    meta: boolean
+}
+export type MouseChangeCallback = (buttonState: number, x: number, y: number, mods: MouseModifiers) => void
+
+export default function mouseListen (element: Element, callback: MouseChangeCallback) {
+    let buttonState = 0
+    let x = 0
+    let y = 0
+    const mods: MouseModifiers = {
+        shift: false,
+        alt: false,
+        control: false,
+        meta: false
+    }
+    let attached = false
+
+    function updateMods (event: MouseEvent | KeyboardEvent) {
+        let changed = false
+        if ('altKey' in event) {
+            changed = changed || event.altKey !== mods.alt
+            mods.alt = !!event.altKey
+        }
+        if ('shiftKey' in event) {
+            changed = changed || event.shiftKey !== mods.shift
+            mods.shift = !!event.shiftKey
+        }
+        if ('ctrlKey' in event) {
+            changed = changed || event.ctrlKey !== mods.control
+            mods.control = !!event.ctrlKey
+        }
+        if ('metaKey' in event) {
+            changed = changed || event.metaKey !== mods.meta
+            mods.meta = !!event.metaKey
+        }
+        return changed
+    }
+
+    function handleEvent (nextButtons: number, event: MouseEvent) {
+        const nextX = mouse.x(event)
+        const nextY = mouse.y(event)
+        if ('buttons' in event) {
+            nextButtons = event.buttons | 0
+        }
+        if (nextButtons !== buttonState || nextX !== x || nextY !== y || updateMods(event) ) {
+            buttonState = nextButtons | 0
+            x = nextX || 0
+            y = nextY || 0
+            callback && callback(buttonState, x, y, mods)
+        }
+    }
+
+    function clearState (event: MouseEvent) {
+        handleEvent(0, event)
+    }
+
+    function handleBlur () {
+        if (buttonState || x || y || mods.shift || mods.alt || mods.meta || mods.control) {
+            x = y = 0
+            buttonState = 0
+            mods.shift = mods.alt = mods.control = mods.meta = false
+            callback && callback(0, 0, 0, mods)
+        }
+    }
+
+    function handleMods (event: MouseEvent | KeyboardEvent) {
+        if (updateMods(event)) {
+            callback && callback(buttonState, x, y, mods)
+        }
+    }
+
+    function handleMouseMove (event: MouseEvent) {
+        if (mouse.buttons(event) === 0) {
+            handleEvent(0, event)
+        } else {
+            handleEvent(buttonState, event)
+        }
+    }
+
+    function handleMouseDown (event: MouseEvent) {
+        handleEvent(buttonState | mouse.buttons(event), event)
+    }
+
+    function handleMouseUp (event: MouseEvent) {
+        handleEvent(buttonState & ~mouse.buttons(event), event)
+    }
+
+    function attachListeners () {
+        if (attached) return
+        attached = true
+
+        element.addEventListener('mousemove', handleMouseMove as EventListener)
+        element.addEventListener('mousedown', handleMouseDown as EventListener)
+        element.addEventListener('mouseup', handleMouseUp as EventListener)
+
+        element.addEventListener('mouseleave', clearState as EventListener)
+        element.addEventListener('mouseenter', clearState as EventListener)
+        element.addEventListener('mouseout', clearState as EventListener)
+        element.addEventListener('mouseover', clearState as EventListener)
+
+        element.addEventListener('blur', handleBlur)
+        element.addEventListener('keyup', handleMods as EventListener)
+        element.addEventListener('keydown', handleMods as EventListener)
+        element.addEventListener('keypress', handleMods as EventListener)
+
+        if (!(element instanceof Window)) {
+            window.addEventListener('blur', handleBlur)
+            window.addEventListener('keyup', handleMods)
+            window.addEventListener('keydown', handleMods)
+            window.addEventListener('keypress', handleMods)
+        }
+    }
+
+    function detachListeners () {
+        if (!attached) return
+        attached = false
+
+        element.removeEventListener('mousemove', handleMouseMove as EventListener)
+        element.removeEventListener('mousedown', handleMouseDown as EventListener)
+        element.removeEventListener('mouseup', handleMouseUp as EventListener)
+
+        element.removeEventListener('mouseleave', clearState as EventListener)
+        element.removeEventListener('mouseenter', clearState as EventListener)
+        element.removeEventListener('mouseout', clearState as EventListener)
+        element.removeEventListener('mouseover', clearState as EventListener)
+
+        element.removeEventListener('blur', handleBlur)
+        element.removeEventListener('keyup', handleMods as EventListener)
+        element.removeEventListener('keydown', handleMods as EventListener)
+        element.removeEventListener('keypress', handleMods as EventListener)
+
+        if (!(element instanceof Window)) {
+            window.removeEventListener('blur', handleBlur)
+            window.removeEventListener('keyup', handleMods)
+            window.removeEventListener('keydown', handleMods)
+            window.removeEventListener('keypress', handleMods)
+        }
+    }
+
+    // Attach listeners
+    attachListeners()
+
+    const result = {
+        element: element
+    }
+
+    Object.defineProperties(result, {
+        enabled: {
+            get: function () { return attached },
+            set: function (f) {
+                if (f) {
+                    attachListeners()
+                } else {
+                    detachListeners()
+                }
+            },
+            enumerable: true
+        },
+        buttons: {
+            get: function () { return buttonState },
+            enumerable: true
+        },
+        x: {
+            get: function () { return x },
+            enumerable: true
+        },
+        y: {
+            get: function () { return y },
+            enumerable: true
+        },
+        mods: {
+            get: function () { return mods },
+            enumerable: true
+        }
+    })
+
+    return result
+}

+ 65 - 0
src/mol-util/mouse-event.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/mouse-event,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+export function buttons(event: MouseEvent) {
+    if (typeof event === 'object') {
+        if ('buttons' in event) {
+            return event.buttons
+        } else if ('which' in event) {
+            const b = (event as any).which  // 'any' to support older browsers
+            if (b === 2) {
+                return 4
+            } else if (b === 3) {
+                return 2
+            } else if (b > 0) {
+                return 1<<(b-1)
+            }
+        } else if ('button' in event) {
+            const b = (event as any).button  // 'any' to support older browsers
+            if (b === 1) {
+                return 4
+            } else if (b === 2) {
+                return 2
+            } else if (b >= 0) {
+                return 1<<b
+            }
+        }
+    }
+    return 0
+}
+
+export function element(event: MouseEvent) {
+    return event.target as Element || event.srcElement || window
+}
+
+export function x(event: MouseEvent) {
+    if (typeof event === 'object') {
+        if ('offsetX' in event) {
+            return event.offsetX
+        }
+        const target = element(event)
+        const bounds = target.getBoundingClientRect()
+        return (event as any).clientX - bounds.left  // 'any' to support older browsers
+    }
+    return 0
+}
+
+export function y(event: MouseEvent) {
+    if (typeof event === 'object') {
+        if ('offsetY' in event) {
+            return event.offsetY
+        }
+        const target = element(event)
+        const bounds = target.getBoundingClientRect()
+        return (event as any).clientY - bounds.top  // 'any' to support older browsers
+    }
+    return 0
+}

+ 40 - 0
src/mol-util/mouse-wheel.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/mouse-wheel,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import toPixels from './to-pixels'
+
+export type MouseWheelCallback = (dx: number, dy: number, dz: number, event: MouseWheelEvent) => void
+
+export default function mouseWheelListen(element: Element, callback: MouseWheelCallback, noScroll = false) {
+    const lineHeight = toPixels('ex', element)
+    const listener = function (event: MouseWheelEvent) {
+        if (noScroll) {
+            event.preventDefault()
+        }
+        const mode = event.deltaMode
+        let dx = event.deltaX || 0
+        let dy = event.deltaY || 0
+        let dz = event.deltaZ || 0
+        let scale = 1
+        switch (mode) {
+            case 1: scale = lineHeight; break
+            case 2: scale = window.innerHeight; break
+        }
+        dx *= scale
+        dy *= scale
+        dz *= scale
+        if (dx || dy || dz) {
+            return callback(dx, dy, dz, event)
+        }
+    }
+    element.addEventListener('wheel', listener)
+    return listener
+}

+ 21 - 0
src/mol-util/parse-unit.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mattdesl/parse-unit,
+ * copyright (c) 2014 Matt DesLauriers. MIT License
+ */
+
+const reUnit = /[\d.\-\+]*\s*(.*)/
+
+export default function parseUnit(str: string, out: [number, string] = [ 0, '' ]) {
+    str = String(str)
+    const num = parseFloat(str)
+    out[0] = num
+    const m = str.match(reUnit)
+    if (m) out[1] = m[1] || ''
+    return out
+}

+ 63 - 0
src/mol-util/to-pixels.ts

@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/to-px,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import parseUnit from './parse-unit'
+
+const PIXELS_PER_INCH = 96
+
+function getPropertyInPX(element: Element, prop: string) {
+    const parts = parseUnit(getComputedStyle(element).getPropertyValue(prop))
+    return parts[0] * toPixels(parts[1], element)
+}
+
+// This brutal hack is needed
+function getSizeBrutal(unit: string, element: Element) {
+    const testDIV = document.createElement('div')
+    testDIV.style.setProperty('font-size', '128' + unit)
+    element.appendChild(testDIV)
+    const size = getPropertyInPX(testDIV, 'font-size') / 128
+    element.removeChild(testDIV)
+    return size
+}
+
+export default function toPixels(str: string, element: Element = document.body): number {
+    str = (str || 'px').trim().toLowerCase()
+    switch (str) {
+        case '%':  // Ambiguous, not sure if we should use width or height
+            return element.clientHeight / 100.0
+        case 'ch':
+        case 'ex':
+            return getSizeBrutal(str, element)
+        case 'em':
+            return getPropertyInPX(element, 'font-size')
+        case 'rem':
+            return getPropertyInPX(document.body, 'font-size')
+        case 'vw':
+            return window.innerWidth/100
+        case 'vh':
+            return window.innerHeight/100
+        case 'vmin':
+            return Math.min(window.innerWidth, window.innerHeight) / 100
+        case 'vmax':
+            return Math.max(window.innerWidth, window.innerHeight) / 100
+        case 'in':
+            return PIXELS_PER_INCH
+        case 'cm':
+            return PIXELS_PER_INCH / 2.54
+        case 'mm':
+            return PIXELS_PER_INCH / 25.4
+        case 'pt':
+            return PIXELS_PER_INCH / 72
+        case 'pc':
+            return PIXELS_PER_INCH / 6
+  }
+  return 1
+}

+ 6 - 5
tsconfig.json

@@ -14,13 +14,14 @@
         "outDir": "build/node_modules",
         "baseUrl": "src",
         "paths": {
-            "mol-task": ["./mol-task", "./mol-task/index.ts"],
-            "mol-comp": ["./mol-comp", "./mol-comp/index.ts"],
-            "mol-util": ["./mol-util", "./mol-util/index.ts"],
             "mol-data": ["./mol-data", "./mol-data/index.ts"],
-            "mol-math": ["./mol-math"],
+            "mol-gl": ["./mol-gl"],
             "mol-io": ["./mol-io"],
-            "mol-model": ["./mol-model"]
+            "mol-math": ["./mol-math"],
+            "mol-model": ["./mol-model"],
+            "mol-ql": ["./mol-ql"],
+            "mol-task": ["./mol-task", "./mol-task/index.ts"],
+            "mol-util": ["./mol-util", "./mol-util/index.ts"],
         }
     },
     "include": [ "**/*" ]

+ 12 - 0
web/render-test/index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=1" />
+        <title>Mol* Render Test</title>
+    </head>
+    <body>
+        <div id="app"></div>
+        <script src="./index.js"></script>
+    </body>
+</html>

+ 23 - 0
webpack.config.js

@@ -0,0 +1,23 @@
+const path = require('path');
+const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin');
+module.exports = {
+    module: {
+        rules: [
+            {
+                loader: 'raw-loader',
+                test: /\.(glsl|frag|vert)$/,
+                include: [ path.resolve(__dirname, "build/node_modules/") ],
+            },
+            {
+                loader: 'glslify-loader',
+                test: /\.(glsl|frag|vert)$/,
+                include: [ path.resolve(__dirname, "build/node_modules/") ]
+            }
+        ]
+    },
+    plugins: [
+        new ExtraWatchWebpackPlugin({
+            files: [ './**/*.vert', './**/*.frag', './**/*.glsl' ],
+        }),
+    ],
+}