Browse Source

added picture-in-picture canvas to viewer app for development

Alexander Rose 6 years ago
parent
commit
42317e49b6

+ 81 - 0
src/mol-app/ui/visualization/image-canvas.tsx

@@ -0,0 +1,81 @@
+/**
+ * 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'
+
+type State = { imageData: ImageData, width: number, height: number }
+
+function getExtend(aspectRatio: number, maxWidth: number, maxHeight: number) {
+    let width = maxWidth
+    let height = width / aspectRatio
+    if (height > maxHeight) {
+        height = maxHeight
+        width = height * aspectRatio
+    }
+    return { width, height }
+}
+
+export class ImageCanvas extends React.Component<{ imageData: ImageData, aspectRatio: number, maxWidth: number, maxHeight: number }, State> {
+    private canvas: HTMLCanvasElement | null = null;
+    private ctx: CanvasRenderingContext2D | null = null;
+
+    componentWillMount() {
+        this.setState({
+            imageData: this.props.imageData,
+            ...getExtend(this.props.aspectRatio, this.props.maxWidth, this.props.maxHeight)
+        })
+    }
+
+    componentDidMount() {
+        if (this.canvas) {
+            this.canvas.width = this.state.imageData.width
+            this.canvas.height = this.state.imageData.height
+            this.ctx = this.canvas.getContext('2d')
+        }
+        if (this.ctx) {
+            this.ctx.putImageData(this.state.imageData, 0, 0)
+        }
+    }
+
+    componentWillReceiveProps() {
+        this.setState({
+            imageData: this.props.imageData,
+            ...getExtend(this.props.aspectRatio, this.props.maxWidth, this.props.maxHeight)
+        })
+    }
+
+    componentDidUpdate() {
+        if (this.canvas) {
+            this.canvas.width = this.state.imageData.width
+            this.canvas.height = this.state.imageData.height
+        }
+        if (this.ctx) {
+            this.ctx.putImageData(this.state.imageData, 0, 0)
+        }
+    }
+
+    render() {
+        return <div
+            className='molstar-image-canvas'
+            style={{
+                width: this.state.width + 6,
+                height: this.state.height + 6,
+                position: 'absolute',
+                border: '3px white solid',
+                bottom: 10,
+                left: 10,
+            }}
+        >
+            <canvas
+                ref={elm => this.canvas = elm}
+                style={{
+                    width: this.state.width,
+                    height: this.state.height,
+                }}
+            />
+        </div>;
+    }
+}

+ 23 - 3
src/mol-app/ui/visualization/viewport.tsx

@@ -13,6 +13,7 @@ import { ViewportController } from '../../controller/visualization/viewport'
 import { View } from '../view';
 import { HelpBox, Toggle, Button } from '../controls/common'
 import { Slider } from '../controls/slider'
+import { ImageCanvas } from './image-canvas';
 
 export class ViewportControls extends View<ViewportController, { showSceneOptions?: boolean, showHelp?: boolean }, {}> {
     state = { showSceneOptions: false, showHelp: false };
@@ -93,22 +94,32 @@ export const Logo = () =>
     </div>
 
 
-export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean, showLogo?: boolean }> {
+export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean, showLogo?: boolean, imageData?: ImageData, aspectRatio: number }> {
     private container: HTMLDivElement | null = null;
     private canvas: HTMLCanvasElement | null = null;
     private defaultBg = { r: 1, g: 1, b: 1 }
-    state = { noWebGl: false, showLogo: true };
+    state = { noWebGl: false, showLogo: true, imageData: undefined, aspectRatio: 1 };
 
     componentDidMount() {
         if (!this.canvas || !this.container || !this.controller.context.initStage(this.canvas, this.container)) {
             this.setState({ noWebGl: true });
         }
-        this.controller.context.stage.viewer.reprCount.subscribe(count => {
+
+        const viewer = this.controller.context.stage.viewer
+
+        viewer.reprCount.subscribe(count => {
             this.setState({
                 showLogo: false
                 // showLogo: count === 0
             })
         })
+
+        viewer.didDraw.subscribe(() => this.setState({ imageData: viewer.getImageData() }))
+        viewer.didDraw.subscribe(() => this.setState({ imageData: viewer.getImageData() }))
+
+        if (this.container) {
+            this.setState({ aspectRatio: this.container.clientWidth / this.container.clientHeight })
+        }
     }
 
     componentWillUnmount() {
@@ -129,6 +140,14 @@ export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean,
     render() {
         if (this.state.noWebGl) return this.renderMissing();
 
+        // const imageData = new ImageData(256, 128)
+
+        let image: JSX.Element | undefined
+        const imageData = this.state.imageData
+        if (imageData) {
+            image = <ImageCanvas imageData={imageData} aspectRatio={this.state.aspectRatio} maxWidth={256} maxHeight={256} />
+        }
+
         const color = this.controller.latestState.clearColor! || this.defaultBg;
         return <div className='molstar-viewport' style={{ backgroundColor: `rgb(${255 * color.r}, ${255 * color.g}, ${255 * color.b})` }}>
             <div ref={elm => this.container = elm} className='molstar-viewport-container'>
@@ -136,6 +155,7 @@ export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean,
             </div>
             {this.state.showLogo ? <Logo /> : void 0}
             <ViewportControls controller={this.controller} />
+            {image}
         </div>;
     }
 }

+ 19 - 1
src/mol-gl/renderer.ts

@@ -9,13 +9,14 @@ import { Viewport } from 'mol-view/camera/util';
 import { Camera } from 'mol-view/camera/base';
 
 import Scene from './scene';
-import { Context } from './webgl/context';
+import { Context, createImageData } from './webgl/context';
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { Renderable } from './renderable';
 import { Color } from 'mol-util/color';
 import { ValueCell } from 'mol-util';
 import { RenderableValues, GlobalUniformValues } from './renderable/schema';
 import { RenderObject } from './render-object';
+import { BehaviorSubject } from 'rxjs';
 
 export interface RendererStats {
     renderableCount: number
@@ -35,6 +36,9 @@ interface Renderer {
 
     setViewport: (viewport: Viewport) => void
     setClearColor: (color: Color) => void
+    getImageData: () => ImageData
+
+    didDraw: BehaviorSubject<number>
 
     stats: RendererStats
     dispose: () => void
@@ -56,6 +60,9 @@ namespace Renderer {
         let { clearColor, viewport: _viewport } = { ...DefaultRendererProps, ...props }
         const scene = Scene.create(ctx)
 
+        const startTime = performance.now()
+        const didDraw = new BehaviorSubject(0)
+
         const model = Mat4.identity()
         const viewport = Viewport.clone(_viewport)
         const pixelRatio = getPixelRatio()
@@ -126,6 +133,8 @@ namespace Renderer {
             gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
             gl.enable(gl.BLEND)
             scene.eachTransparent(drawObject)
+
+            didDraw.next(performance.now() - startTime)
         }
 
         return {
@@ -149,6 +158,15 @@ namespace Renderer {
                 gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height)
                 ValueCell.update(globalUniforms.uViewportHeight, viewport.height)
             },
+            getImageData: () => {
+                const { width, height } = viewport
+                const buffer = new Uint8Array(width * height * 4)
+                ctx.unbindFramebuffer()
+                ctx.readPixels(0, 0, width, height, buffer)
+                return createImageData(buffer, width, height)
+            },
+
+            didDraw,
 
             get stats(): RendererStats {
                 return {

+ 29 - 0
src/mol-gl/webgl/context.ts

@@ -28,9 +28,28 @@ function unbindResources (gl: WebGLRenderingContext) {
     gl.bindBuffer(gl.ARRAY_BUFFER, null)
     gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null)
     gl.bindRenderbuffer(gl.RENDERBUFFER, null)
+    unbindFramebuffer(gl)
+}
+
+function unbindFramebuffer(gl: WebGLRenderingContext) {
     gl.bindFramebuffer(gl.FRAMEBUFFER, null)
 }
 
+export function createImageData(buffer: Uint8Array, width: number, height: number) {
+    const w = width * 4
+    const h = height
+    const data = new Uint8ClampedArray(width * height * 4)
+    for (let i = 0, maxI = h / 2; i < maxI; ++i) {
+        for (let j = 0, maxJ = w; j < maxJ; ++j) {
+            const index1 = i * w + j;
+            const index2 = (h-i-1) * w + j;
+            data[index1] = buffer[index2];
+            data[index2] = buffer[index1];
+        }
+    }
+    return new ImageData(data, width, height);
+}
+
 type Extensions = {
     angleInstancedArrays: ANGLE_instanced_arrays
     standardDerivatives: OES_standard_derivatives
@@ -47,6 +66,8 @@ export interface Context {
     bufferCount: number
     textureCount: number
     vaoCount: number
+    readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => void
+    unbindFramebuffer: () => void
     destroy: () => void
 }
 
@@ -79,6 +100,14 @@ export function createContext(gl: WebGLRenderingContext): Context {
         bufferCount: 0,
         textureCount: 0,
         vaoCount: 0,
+        readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
+            if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
+                gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)
+            } else {
+                console.error('Reading pixels failed. Framebuffer not complete.')
+            }
+        },
+        unbindFramebuffer: () => unbindFramebuffer(gl),
         destroy: () => {
             unbindResources(gl)
             programCache.dispose()

+ 7 - 0
src/mol-view/viewer.ts

@@ -18,6 +18,7 @@ import { PerspectiveCamera } from './camera/perspective'
 import { resizeCanvas } from './util';
 import { createContext } from 'mol-gl/webgl/context';
 import { Representation } from 'mol-geo/representation';
+import { render } from 'react-dom';
 
 interface Viewer {
     center: (p: Vec3) => void
@@ -34,10 +35,12 @@ interface Viewer {
     requestDraw: () => void
     animate: () => void
     reprCount: BehaviorSubject<number>
+    didDraw: BehaviorSubject<number>
 
     handleResize: () => void
     resetCamera: () => void
     downloadScreenshot: () => void
+    getImageData: () => ImageData
 
     input: InputObserver
     stats: RendererStats
@@ -165,7 +168,11 @@ namespace Viewer {
             downloadScreenshot: () => {
                 // TODO
             },
+            getImageData: () => {
+                return renderer.getImageData()
+            },
             reprCount,
+            didDraw: renderer.didDraw,
 
             get input() {
                 return input