Skip to content
Snippets Groups Projects
Commit 42317e49 authored by Alexander Rose's avatar Alexander Rose
Browse files

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

parent 6059b0b9
No related branches found
No related tags found
No related merge requests found
/**
* 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
...@@ -13,6 +13,7 @@ import { ViewportController } from '../../controller/visualization/viewport' ...@@ -13,6 +13,7 @@ import { ViewportController } from '../../controller/visualization/viewport'
import { View } from '../view'; import { View } from '../view';
import { HelpBox, Toggle, Button } from '../controls/common' import { HelpBox, Toggle, Button } from '../controls/common'
import { Slider } from '../controls/slider' import { Slider } from '../controls/slider'
import { ImageCanvas } from './image-canvas';
export class ViewportControls extends View<ViewportController, { showSceneOptions?: boolean, showHelp?: boolean }, {}> { export class ViewportControls extends View<ViewportController, { showSceneOptions?: boolean, showHelp?: boolean }, {}> {
state = { showSceneOptions: false, showHelp: false }; state = { showSceneOptions: false, showHelp: false };
...@@ -93,22 +94,32 @@ export const Logo = () => ...@@ -93,22 +94,32 @@ export const Logo = () =>
</div> </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 container: HTMLDivElement | null = null;
private canvas: HTMLCanvasElement | null = null; private canvas: HTMLCanvasElement | null = null;
private defaultBg = { r: 1, g: 1, b: 1 } private defaultBg = { r: 1, g: 1, b: 1 }
state = { noWebGl: false, showLogo: true }; state = { noWebGl: false, showLogo: true, imageData: undefined, aspectRatio: 1 };
componentDidMount() { componentDidMount() {
if (!this.canvas || !this.container || !this.controller.context.initStage(this.canvas, this.container)) { if (!this.canvas || !this.container || !this.controller.context.initStage(this.canvas, this.container)) {
this.setState({ noWebGl: true }); 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({ this.setState({
showLogo: false showLogo: false
// showLogo: count === 0 // 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() { componentWillUnmount() {
...@@ -129,6 +140,14 @@ export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean, ...@@ -129,6 +140,14 @@ export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean,
render() { render() {
if (this.state.noWebGl) return this.renderMissing(); 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; 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})` }}> 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'> <div ref={elm => this.container = elm} className='molstar-viewport-container'>
...@@ -136,6 +155,7 @@ export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean, ...@@ -136,6 +155,7 @@ export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean,
</div> </div>
{this.state.showLogo ? <Logo /> : void 0} {this.state.showLogo ? <Logo /> : void 0}
<ViewportControls controller={this.controller} /> <ViewportControls controller={this.controller} />
{image}
</div>; </div>;
} }
} }
\ No newline at end of file
...@@ -9,13 +9,14 @@ import { Viewport } from 'mol-view/camera/util'; ...@@ -9,13 +9,14 @@ import { Viewport } from 'mol-view/camera/util';
import { Camera } from 'mol-view/camera/base'; import { Camera } from 'mol-view/camera/base';
import Scene from './scene'; import Scene from './scene';
import { Context } from './webgl/context'; import { Context, createImageData } from './webgl/context';
import { Mat4, Vec3 } from 'mol-math/linear-algebra'; import { Mat4, Vec3 } from 'mol-math/linear-algebra';
import { Renderable } from './renderable'; import { Renderable } from './renderable';
import { Color } from 'mol-util/color'; import { Color } from 'mol-util/color';
import { ValueCell } from 'mol-util'; import { ValueCell } from 'mol-util';
import { RenderableValues, GlobalUniformValues } from './renderable/schema'; import { RenderableValues, GlobalUniformValues } from './renderable/schema';
import { RenderObject } from './render-object'; import { RenderObject } from './render-object';
import { BehaviorSubject } from 'rxjs';
export interface RendererStats { export interface RendererStats {
renderableCount: number renderableCount: number
...@@ -35,6 +36,9 @@ interface Renderer { ...@@ -35,6 +36,9 @@ interface Renderer {
setViewport: (viewport: Viewport) => void setViewport: (viewport: Viewport) => void
setClearColor: (color: Color) => void setClearColor: (color: Color) => void
getImageData: () => ImageData
didDraw: BehaviorSubject<number>
stats: RendererStats stats: RendererStats
dispose: () => void dispose: () => void
...@@ -56,6 +60,9 @@ namespace Renderer { ...@@ -56,6 +60,9 @@ namespace Renderer {
let { clearColor, viewport: _viewport } = { ...DefaultRendererProps, ...props } let { clearColor, viewport: _viewport } = { ...DefaultRendererProps, ...props }
const scene = Scene.create(ctx) const scene = Scene.create(ctx)
const startTime = performance.now()
const didDraw = new BehaviorSubject(0)
const model = Mat4.identity() const model = Mat4.identity()
const viewport = Viewport.clone(_viewport) const viewport = Viewport.clone(_viewport)
const pixelRatio = getPixelRatio() const pixelRatio = getPixelRatio()
...@@ -126,6 +133,8 @@ namespace Renderer { ...@@ -126,6 +133,8 @@ namespace Renderer {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.BLEND) gl.enable(gl.BLEND)
scene.eachTransparent(drawObject) scene.eachTransparent(drawObject)
didDraw.next(performance.now() - startTime)
} }
return { return {
...@@ -149,6 +158,15 @@ namespace Renderer { ...@@ -149,6 +158,15 @@ namespace Renderer {
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height) gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height)
ValueCell.update(globalUniforms.uViewportHeight, 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 { get stats(): RendererStats {
return { return {
......
...@@ -28,9 +28,28 @@ function unbindResources (gl: WebGLRenderingContext) { ...@@ -28,9 +28,28 @@ function unbindResources (gl: WebGLRenderingContext) {
gl.bindBuffer(gl.ARRAY_BUFFER, null) gl.bindBuffer(gl.ARRAY_BUFFER, null)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null)
gl.bindRenderbuffer(gl.RENDERBUFFER, null) gl.bindRenderbuffer(gl.RENDERBUFFER, null)
unbindFramebuffer(gl)
}
function unbindFramebuffer(gl: WebGLRenderingContext) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null) 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 = { type Extensions = {
angleInstancedArrays: ANGLE_instanced_arrays angleInstancedArrays: ANGLE_instanced_arrays
standardDerivatives: OES_standard_derivatives standardDerivatives: OES_standard_derivatives
...@@ -47,6 +66,8 @@ export interface Context { ...@@ -47,6 +66,8 @@ export interface Context {
bufferCount: number bufferCount: number
textureCount: number textureCount: number
vaoCount: number vaoCount: number
readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => void
unbindFramebuffer: () => void
destroy: () => void destroy: () => void
} }
...@@ -79,6 +100,14 @@ export function createContext(gl: WebGLRenderingContext): Context { ...@@ -79,6 +100,14 @@ export function createContext(gl: WebGLRenderingContext): Context {
bufferCount: 0, bufferCount: 0,
textureCount: 0, textureCount: 0,
vaoCount: 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: () => { destroy: () => {
unbindResources(gl) unbindResources(gl)
programCache.dispose() programCache.dispose()
......
...@@ -18,6 +18,7 @@ import { PerspectiveCamera } from './camera/perspective' ...@@ -18,6 +18,7 @@ import { PerspectiveCamera } from './camera/perspective'
import { resizeCanvas } from './util'; import { resizeCanvas } from './util';
import { createContext } from 'mol-gl/webgl/context'; import { createContext } from 'mol-gl/webgl/context';
import { Representation } from 'mol-geo/representation'; import { Representation } from 'mol-geo/representation';
import { render } from 'react-dom';
interface Viewer { interface Viewer {
center: (p: Vec3) => void center: (p: Vec3) => void
...@@ -34,10 +35,12 @@ interface Viewer { ...@@ -34,10 +35,12 @@ interface Viewer {
requestDraw: () => void requestDraw: () => void
animate: () => void animate: () => void
reprCount: BehaviorSubject<number> reprCount: BehaviorSubject<number>
didDraw: BehaviorSubject<number>
handleResize: () => void handleResize: () => void
resetCamera: () => void resetCamera: () => void
downloadScreenshot: () => void downloadScreenshot: () => void
getImageData: () => ImageData
input: InputObserver input: InputObserver
stats: RendererStats stats: RendererStats
...@@ -165,7 +168,11 @@ namespace Viewer { ...@@ -165,7 +168,11 @@ namespace Viewer {
downloadScreenshot: () => { downloadScreenshot: () => {
// TODO // TODO
}, },
getImageData: () => {
return renderer.getImageData()
},
reprCount, reprCount,
didDraw: renderer.didDraw,
get input() { get input() {
return input return input
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment