diff --git a/package-lock.json b/package-lock.json
index adc4e46e5fb1e29dd1bde04c49b8f6cce5f3e0de..4ce0ce891139fdffd9528576f7e5180db3a8fb39 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index a663aa7ad1187a1a7f40d4496ebd70f56fa34fca..70f4cd4152c0ebca7bd4ee3eea6456d08e04ca97 100644
--- a/package.json
+++ b/package.json
@@ -80,8 +80,10 @@
   "dependencies": {
     "argparse": "^1.0.10",
     "express": "^4.16.3",
+    "material-ui": "^1.0.0-beta.41",
     "node-fetch": "^2.1.2",
     "react": "^16.3.1",
-    "react-dom": "^16.3.1"
+    "react-dom": "^16.3.1",
+    "rxjs": "^6.0.0-beta.4"
   }
 }
diff --git a/src/apps/render-test/components/file-input.tsx b/src/apps/render-test/components/file-input.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f83d200ab54414550b912eade40e726ce625d7e3
--- /dev/null
+++ b/src/apps/render-test/components/file-input.tsx
@@ -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>
+ */
+
+import * as React from 'react'
+import { WithStyles } from 'material-ui/styles';
+import TextField from 'material-ui/TextField';
+// import FileUpload from '@material-ui/icons/FileUpload';
+
+import State from '../state'
+import Observer from './observer';
+
+export default class FileInput extends Observer<{ state: State } & WithStyles, { loading: boolean }> {
+    state = { loading: false }
+
+    componentDidMount() {
+        this.subscribe(this.props.state.loading, value => {
+           this.setState({ loading: value });
+        });
+    }
+
+    render() {
+        const { classes, state } = this.props;
+
+        return <TextField
+            label='PDB ID'
+            className={classes.textField}
+            disabled={this.state.loading}
+            margin='normal'
+            onChange={(event) => {
+                state.pdbId = event.target.value
+            }}
+            onKeyPress={(event) => {
+                if (event.key === 'Enter') state.loadPdbId()
+            }}
+        />
+    }
+}
\ No newline at end of file
diff --git a/src/apps/render-test/components/observer.tsx b/src/apps/render-test/components/observer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..67e48fb23485513daf57f4b7f57fc9bba76bd26a
--- /dev/null
+++ b/src/apps/render-test/components/observer.tsx
@@ -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 * as React from 'react'
+import { Observable, Subscription } from 'rxjs';
+
+export default class Observer<S, P> extends React.Component<S, P> {
+    private _subs: Subscription[] = []
+
+    subscribe<T>(obs: Observable<T>, onNext: (v: T) => void) {
+        this._subs.push(obs.subscribe(onNext));
+    }
+
+    componentWillUnmount() {
+        for (const s of this._subs) s.unsubscribe();
+        this._subs = [];
+    }
+}
\ No newline at end of file
diff --git a/src/apps/render-test/components/viewport.tsx b/src/apps/render-test/components/viewport.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1536cd5a41f75f1efce8035cd2258296b05b34d5
--- /dev/null
+++ b/src/apps/render-test/components/viewport.tsx
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import State from '../state'
+
+export default class Viewport extends React.Component<{ state: State }, { initialized: boolean }> {
+    private canvasContainer: HTMLDivElement | null = null;
+    state = { initialized: false }
+
+    componentDidMount() {
+        if (this.canvasContainer) this.props.state.initRenderer(this.canvasContainer).then(() => this.setState({ initialized: true }))
+    }
+
+    render() {
+        return <div ref={elm => this.canvasContainer = elm} style={{ height: '100%' }}>
+        </div>
+    }
+}
\ No newline at end of file
diff --git a/src/apps/render-test/index.tsx b/src/apps/render-test/index.tsx
index 246502b305987115ab9d1930e4e73ecc42ea8876..2e87d291faecababed3fca35c7a2799742704ce9 100644
--- a/src/apps/render-test/index.tsx
+++ b/src/apps/render-test/index.tsx
@@ -10,4 +10,4 @@ import * as React from 'react'
 import * as ReactDOM from 'react-dom'
 
 const state = new State()
-ReactDOM.render(<UI state={state} />, document.getElementById('app'));
\ No newline at end of file
+ReactDOM.render(<UI state={ state } />, document.getElementById('app'));
\ No newline at end of file
diff --git a/src/apps/render-test/state.ts b/src/apps/render-test/state.ts
index bfc4e2a2a2f2fb61a2b214224964869b0c176a06..abb3245e41f5c80ba25b9d5d7245134c10ece93f 100644
--- a/src/apps/render-test/state.ts
+++ b/src/apps/render-test/state.ts
@@ -4,127 +4,47 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
+import { BehaviorSubject } from 'rxjs';
 
-import { Vec3, Mat4 } from 'mol-math/linear-algebra'
-import { createRenderer, createRenderObject } from 'mol-gl/renderer'
-import { createColorTexture } from 'mol-gl/util';
+// import { ValueCell } from 'mol-util/value-cell'
+
+// import { Vec3, Mat4 } from 'mol-math/linear-algebra'
+import { createRenderer, Renderer } from 'mol-gl/renderer'
+// import { createColorTexture } from 'mol-gl/util';
 // import Icosahedron from 'mol-geo/primitive/icosahedron'
 // import Box from 'mol-geo/primitive/box'
 import Spacefill from 'mol-geo/representation/structure/spacefill'
 import Point from 'mol-geo/representation/structure/point'
 
-import CIF from 'mol-io/reader/cif'
-import { Run, Progress } from 'mol-task'
-import { Structure, Symmetry } from 'mol-model/structure'
-
-function log(progress: Progress) {
-    const p = progress.root.progress
-    console.log(`${p.message} ${(p.current/p.max*100).toFixed(2)}%`)
-}
-
-async function parseCif(data: string|Uint8Array) {
-    const comp = CIF.parse(data)
-    const parsed = await Run(comp, log, 100);
-    if (parsed.isError) throw parsed;
-    return parsed
-}
-
-async function getPdb(pdb: string) {
-    const data = await fetch(`https://files.rcsb.org/download/${pdb}.cif`)
-    const parsed = await parseCif(await data.text())
-    const structure = Structure.ofData({ kind: 'mmCIF', data: CIF.schema.mmCIF(parsed.result.blocks[0]) })
-    return structure
-}
+import { Run } from 'mol-task'
+import { Symmetry } from 'mol-model/structure'
 
-import mcubes from './mcubes'
+// import mcubes from './utils/mcubes'
+import { getStructuresFromPdbId } from './utils'
 import { StructureRepresentation } from 'mol-geo/representation/structure';
 // import Cylinder from 'mol-geo/primitive/cylinder';
 
 export default class State {
+    renderer: Renderer
+    pdbId = '1crn'
+    initialized = new BehaviorSubject<boolean>(false)
+    loading = new BehaviorSubject<boolean>(false)
+
     async initRenderer (container: HTMLDivElement) {
-        const renderer = createRenderer(container)
-
-        const p1 = Vec3.create(0, 4, 0)
-        const p2 = Vec3.create(-3, 0, 0)
-
-        // const position = ValueCell.create(new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]))
-        // const normal = ValueCell.create(new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]))
-
-        const transformArray1 = ValueCell.create(new Float32Array(16))
-        const transformArray2 = ValueCell.create(new Float32Array(16 * 3))
-        const m4 = Mat4.identity()
-        Mat4.toArray(m4, transformArray1.ref.value, 0)
-        Mat4.toArray(m4, transformArray2.ref.value, 0)
-        Mat4.setTranslation(m4, p1)
-        Mat4.toArray(m4, transformArray2.ref.value, 16)
-        Mat4.setTranslation(m4, p2)
-        Mat4.toArray(m4, transformArray2.ref.value, 32)
-
-        const color = ValueCell.create(createColorTexture(3))
-        color.ref.value.set([
-            0, 0, 255,
-            0, 255, 0,
-            255, 0, 0
-        ])
-
-        // const points = createRenderObject('point', {
-        //     position,
-        //     transform: transformArray1
-        // })
-        // // renderer.add(points)
-
-        // const mesh = createRenderObject('mesh', {
-        //     position,
-        //     normal,
-        //     color,
-        //     transform: transformArray2
-        // })
-        // renderer.add(mesh)
-
-        // const cylinder = Cylinder({ height: 3, radiusBottom: 0.5, radiusTop: 0.5 })
-        // console.log(cylinder)
-        // const cylinderMesh = createRenderObject('mesh', {
-        //     position: ValueCell.create(cylinder.vertices),
-        //     normal: ValueCell.create(cylinder.normals),
-        //     color,
-        //     transform: transformArray2
-        // }, cylinder.indices)
-        // renderer.add(cylinderMesh)
-
-        // const sphere = Icosahedron()
-        // console.log(sphere)
-
-        // const box = Box()
-        // console.log(box)
-
-        // const points2 = createRenderObject('point', {
-        //     position: ValueCell.create(new Float32Array(box.vertices)),
-        //     transform: transformArray1
-        // })
-        // renderer.add(points2)
-
-        let rr = 0.7;
-        function cubesF(x: number, y: number, z: number) {
-            return x * x + y * y + z * z - rr * rr;
-        }
-        let cubes = await mcubes(cubesF);
-
-        const makeCubesMesh = () => createRenderObject('mesh', {
-            position: cubes.surface.vertexBuffer,
-            normal: cubes.surface.normalBuffer,
-            color,
-            transform: transformArray2,
-            elements: cubes.surface.indexBuffer,
-
-            instanceCount: transformArray2.ref.value.length / 16,
-            elementCount: cubes.surface.triangleCount,
-            positionCount: cubes.surface.vertexCount
-        }, {});
-        const mesh2 = makeCubesMesh();
-        renderer.add(mesh2)
-
-        const structures = await getPdb('1rb8')
+        this.renderer = createRenderer(container)
+        this.initialized.next(true)
+        this.loadPdbId()
+        this.renderer.frame()
+    }
+
+    async loadPdbId () {
+        const { renderer, pdbId } = this
+        renderer.clear()
+
+        if (pdbId.length !== 4) return
+        this.loading.next(true)
+
+        const structures = await getStructuresFromPdbId(pdbId)
         const struct = Symmetry.buildAssembly(structures[0], '1')
 
         const structPointRepr = StructureRepresentation(Point)
@@ -135,6 +55,91 @@ export default class State {
         await Run(structSpacefillRepr.create(struct))
         structSpacefillRepr.renderObjects.forEach(renderer.add)
 
-        renderer.frame()
+        renderer.draw(true)
+
+        this.loading.next(false)
     }
 }
+
+
+
+// async foo () {
+//     const p1 = Vec3.create(0, 4, 0)
+//     const p2 = Vec3.create(-3, 0, 0)
+
+//     // const position = ValueCell.create(new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]))
+//     // const normal = ValueCell.create(new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]))
+
+//     const transformArray1 = ValueCell.create(new Float32Array(16))
+//     const transformArray2 = ValueCell.create(new Float32Array(16 * 3))
+//     const m4 = Mat4.identity()
+//     Mat4.toArray(m4, transformArray1.ref.value, 0)
+//     Mat4.toArray(m4, transformArray2.ref.value, 0)
+//     Mat4.setTranslation(m4, p1)
+//     Mat4.toArray(m4, transformArray2.ref.value, 16)
+//     Mat4.setTranslation(m4, p2)
+//     Mat4.toArray(m4, transformArray2.ref.value, 32)
+
+//     const color = ValueCell.create(createColorTexture(3))
+//     color.ref.value.set([
+//         0, 0, 255,
+//         0, 255, 0,
+//         255, 0, 0
+//     ])
+
+//     // const points = createRenderObject('point', {
+//     //     position,
+//     //     transform: transformArray1
+//     // })
+//     // // renderer.add(points)
+
+//     // const mesh = createRenderObject('mesh', {
+//     //     position,
+//     //     normal,
+//     //     color,
+//     //     transform: transformArray2
+//     // })
+//     // renderer.add(mesh)
+
+//     // const cylinder = Cylinder({ height: 3, radiusBottom: 0.5, radiusTop: 0.5 })
+//     // console.log(cylinder)
+//     // const cylinderMesh = createRenderObject('mesh', {
+//     //     position: ValueCell.create(cylinder.vertices),
+//     //     normal: ValueCell.create(cylinder.normals),
+//     //     color,
+//     //     transform: transformArray2
+//     // }, cylinder.indices)
+//     // renderer.add(cylinderMesh)
+
+//     // const sphere = Icosahedron()
+//     // console.log(sphere)
+
+//     // const box = Box()
+//     // console.log(box)
+
+//     // const points2 = createRenderObject('point', {
+//     //     position: ValueCell.create(new Float32Array(box.vertices)),
+//     //     transform: transformArray1
+//     // })
+//     // renderer.add(points2)
+
+//     // let rr = 0.7;
+//     // function cubesF(x: number, y: number, z: number) {
+//     //     return x * x + y * y + z * z - rr * rr;
+//     // }
+//     // let cubes = await mcubes(cubesF);
+
+//     // const makeCubesMesh = () => createRenderObject('mesh', {
+//     //     position: cubes.surface.vertexBuffer,
+//     //     normal: cubes.surface.normalBuffer,
+//     //     color,
+//     //     transform: transformArray2,
+//     //     elements: cubes.surface.indexBuffer,
+
+//     //     instanceCount: transformArray2.ref.value.length / 16,
+//     //     elementCount: cubes.surface.triangleCount,
+//     //     positionCount: cubes.surface.vertexCount
+//     // }, {});
+//     // const mesh2 = makeCubesMesh();
+//     // renderer.add(mesh2)
+// }
\ No newline at end of file
diff --git a/src/apps/render-test/ui.tsx b/src/apps/render-test/ui.tsx
index dbdf41f3b72e11bacaf22bf4055e17d29b2ffc4d..e0ca7ec6086096cdf968bade7862f2c1a119615e 100644
--- a/src/apps/render-test/ui.tsx
+++ b/src/apps/render-test/ui.tsx
@@ -5,18 +5,75 @@
  */
 
 import * as React from 'react'
+import { withStyles, WithStyles, Theme, StyleRulesCallback } from 'material-ui/styles';
+import Typography from 'material-ui/Typography';
+import Toolbar from 'material-ui/Toolbar';
+import AppBar from 'material-ui/AppBar';
+import Drawer from 'material-ui/Drawer';
+
+import Viewport from './components/viewport'
+import FileInput from './components/file-input'
 import State from './state'
 
-export default class Root extends React.Component<{ state: State }, { initialized: boolean }> {
-    private canvasContainer: HTMLDivElement | null = null;
-    state = { initialized: false }
+const styles: StyleRulesCallback<any> = (theme: Theme) => ({
+    root: {
+        flexGrow: 1,
+        height: 830,
+        zIndex: 1,
+        overflow: 'hidden',
+        position: 'relative',
+        display: 'flex',
+    },
+    appBar: {
+        zIndex: theme.zIndex.drawer + 1,
+    },
+    drawerPaper: {
+        position: 'relative',
+        width: 240,
+    },
+    content: {
+        flexGrow: 1,
+        backgroundColor: theme.palette.background.default,
+        padding: theme.spacing.unit * 3,
+        minWidth: 0, // So the Typography noWrap works
+    },
+    toolbar: theme.mixins.toolbar,
+    textField: {
+        marginLeft: theme.spacing.unit,
+        marginRight: theme.spacing.unit,
+        width: 200,
+    },
+});
 
-    componentDidMount() {
-        if (this.canvasContainer) this.props.state.initRenderer(this.canvasContainer).then(() => this.setState({ initialized: true }))
-    }
+const decorate = withStyles(styles);
 
+interface Props {
+    state: State;
+};
+
+class UI extends React.Component<{ state: State } & WithStyles, {  }> {
     render() {
-        return <div ref={elm => this.canvasContainer = elm} style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0, overflow: 'hidden' }}>
-        </div>
+        const { classes, state } = this.props;
+        return (
+            <div className={classes.root}>
+                <AppBar position='absolute' className={classes.appBar}>
+                    <Toolbar>
+                    <Typography variant='title' color='inherit' noWrap>
+                        Mol* Render Test
+                    </Typography>
+                    </Toolbar>
+                </AppBar>
+                <Drawer variant='permanent' classes={{ paper: classes.drawerPaper, }}>
+                    <div className={classes.toolbar} />
+                    <FileInput state={state} classes={classes}></FileInput>
+                </Drawer>
+                <main className={classes.content}>
+                    <div className={classes.toolbar} />
+                    <Viewport state={state}></Viewport>
+                </main>
+            </div>
+        );
     }
-}
\ No newline at end of file
+}
+
+export default decorate<Props>(UI)
\ No newline at end of file
diff --git a/src/apps/render-test/utils/index.ts b/src/apps/render-test/utils/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..67a7eb9a307501f67f245856ee0c58a5e4a8716a
--- /dev/null
+++ b/src/apps/render-test/utils/index.ts
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import CIF from 'mol-io/reader/cif'
+import { Run, Progress } from 'mol-task'
+import { Structure } from 'mol-model/structure'
+
+export function log(progress: Progress) {
+    const p = progress.root.progress
+    console.log(`${p.message} ${(p.current/p.max*100).toFixed(2)}%`)
+}
+
+export async function parseCif(data: string|Uint8Array) {
+    const comp = CIF.parse(data)
+    const parsed = await Run(comp, log, 100);
+    if (parsed.isError) throw parsed;
+    return parsed
+}
+
+export async function getStructuresFromPdbId(pdbid: string) {
+    const data = await fetch(`https://files.rcsb.org/download/${pdbid}.cif`)
+    const parsed = await parseCif(await data.text())
+    return Structure.ofData({ kind: 'mmCIF', data: CIF.schema.mmCIF(parsed.result.blocks[0]) })
+}
\ No newline at end of file
diff --git a/src/apps/render-test/mcubes.ts b/src/apps/render-test/utils/mcubes.ts
similarity index 100%
rename from src/apps/render-test/mcubes.ts
rename to src/apps/render-test/utils/mcubes.ts
diff --git a/src/mol-gl/camera.ts b/src/mol-gl/camera.ts
index f3a3ffa04848428f9f7c1c5d4600bbe6f1e681e7..623e2467284f005393e2358348a52d82b2fa60f0 100644
--- a/src/mol-gl/camera.ts
+++ b/src/mol-gl/camera.ts
@@ -49,7 +49,7 @@ export interface Camera {
     update: (props: any, block: any) => void,
     setState: (newState: CameraState) => void,
     getState: () => CameraState,
-    isDirty: () => boolean
+    dirty: boolean
 }
 
 export namespace Camera {
@@ -185,7 +185,8 @@ export namespace Camera {
             update,
             setState,
             getState: () => Object.assign({}, state),
-            isDirty: () => dirty
+            get dirty() { return dirty },
+            set dirty(value: boolean) { dirty = value }
         }
     }
 }
diff --git a/src/mol-gl/renderable/mesh.ts b/src/mol-gl/renderable/mesh.ts
index a7fa373f9e9760c9c702e507752c4f4b42099664..52c2244c735739eda229b4cbbd9e21512de60a8b 100644
--- a/src/mol-gl/renderable/mesh.ts
+++ b/src/mol-gl/renderable/mesh.ts
@@ -31,7 +31,6 @@ namespace Mesh {
     }
 
     export function create(regl: REGL.Regl, data: Data, uniforms: Uniforms): Renderable {
-        console.log(data)
         const instanceId = ValueCell.create(fillSerial(new Float32Array(data.instanceCount)))
         const command = regl({
             ...MeshShaders,
diff --git a/src/mol-gl/renderable/point.ts b/src/mol-gl/renderable/point.ts
index 99b31165599afc448cf0e5bb589a87387c49e2bf..5bc8aed303ec225c9e2abb893285e045c9f74a98 100644
--- a/src/mol-gl/renderable/point.ts
+++ b/src/mol-gl/renderable/point.ts
@@ -24,7 +24,6 @@ namespace Point {
     }
 
     export function create(regl: REGL.Regl, data: Data): Renderable {
-        console.log(data)
         const instanceId = ValueCell.create(fillSerial(new Float32Array(data.instanceCount)))
         const command = regl({
             ...PointShaders,
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index f4c25f0f64248e9b651e9496759106d08ec9113a..eef94cf27441e2c057a7e721e79e05ca342c14ca 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -39,7 +39,8 @@ export function createRenderObject(type: 'mesh' | 'point', data: PointRenderable
 export interface Renderer {
     add: (o: RenderObject) => void
     remove: (o: RenderObject) => void
-    draw: () => void
+    clear: () => void
+    draw: (force: boolean) => void
     frame: () => void
 }
 
@@ -54,18 +55,33 @@ export function createRenderer(container: HTMLDivElement): Renderer {
     const renderableList: Renderable[] = []
     const objectIdRenderableMap: { [k: number]: Renderable } = {}
 
-    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
-    })
+    let regl: REGL.Regl
+    try {
+        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
+        })
+    } catch (e) {
+        regl = glContext.create({
+            container,
+            extensions: [
+                'OES_texture_float',
+                'OES_texture_float_linear',
+                'OES_element_index_uint',
+                'EXT_blend_minmax',
+                'ANGLE_instanced_arrays'
+            ],
+            profile: true
+        })
+    }
 
     const camera = Camera.create(regl, container, {
         center: Vec3.create(0, 0, 0),
@@ -91,12 +107,12 @@ export function createRenderer(container: HTMLDivElement): Renderer {
         }
     })
 
-    const draw = () => {
+    const draw = (force = false) => {
         camera.update((state: any) => {
-            if (!camera.isDirty()) return;
+            if (!force && !camera.dirty) return;
             baseContext(() => {
                 // console.log(ctx)
-                regl.clear({color: [0, 0, 0, 1]})
+                regl.clear({ color: [0, 0, 0, 1] })
                 // TODO painters sort, filter visible, filter picking, visibility culling?
                 renderableList.forEach(r => {
                     r.draw()
@@ -118,6 +134,15 @@ export function createRenderer(container: HTMLDivElement): Renderer {
                 delete objectIdRenderableMap[o.id]
             }
         },
+        clear: () => {
+            for (const id in objectIdRenderableMap) {
+                // TODO
+                // objectIdRenderableMap[id].destroy()
+                delete objectIdRenderableMap[id]
+            }
+            renderableList.length = 0
+            camera.dirty = true
+        },
         draw,
         frame: () => {
             regl.frame((ctx) => draw())