diff --git a/src/mol-app/ui/visualization/image-canvas.tsx b/src/mol-app/ui/visualization/image-canvas.tsx new file mode 100644 index 0000000000000000000000000000000000000000..643a087529ed8ee49c8e7340ba567d0be6415573 --- /dev/null +++ b/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>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/visualization/viewport.tsx b/src/mol-app/ui/visualization/viewport.tsx index 71b3afdeb2376e5d5dcb9c01cb6f24c8c4d5fbd1..707f7116e38761699a7c026e6df16e4fc62cd656 100644 --- a/src/mol-app/ui/visualization/viewport.tsx +++ b/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>; } } \ No newline at end of file diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index 56b9e18fabda2d644b7c4f08a8b938cffdeb53ed..be6d05324fbf4fd0f1f984cba24c05e52eb89d18 100644 --- a/src/mol-gl/renderer.ts +++ b/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 { diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index d24699688ad90a696d62b5e152bfb3c3f9056c56..2087c4c4f78f718919f8fbfe5316874d9ab1de9a 100644 --- a/src/mol-gl/webgl/context.ts +++ b/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() diff --git a/src/mol-view/viewer.ts b/src/mol-view/viewer.ts index 3c76984d731eba110c03c6b174a86cb248782dac..36e038fef43cdda47fa8e89f2df269476273affb 100644 --- a/src/mol-view/viewer.ts +++ b/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