|
@@ -0,0 +1,204 @@
|
|
|
|
+/**
|
|
|
|
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
|
|
|
+ *
|
|
|
|
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+import * as React from 'react';
|
|
|
|
+import { CollapsableControls, CollapsableState } from './base';
|
|
|
|
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
|
|
|
+import { ParameterControls } from './controls/parameters';
|
|
|
|
+import { ImagePass } from '../../mol-canvas3d/passes/image';
|
|
|
|
+import { download } from '../../mol-util/download';
|
|
|
|
+import { setCanvasSize, canvasToBlob } from '../../mol-canvas3d/util';
|
|
|
|
+import { Task } from '../../mol-task';
|
|
|
|
+
|
|
|
|
+interface ImageControlsState extends CollapsableState {
|
|
|
|
+ showPreview: boolean
|
|
|
|
+
|
|
|
|
+ size: 'canvas' | 'custom'
|
|
|
|
+ width: number
|
|
|
|
+ height: number
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const maxWidthUi = 260
|
|
|
|
+const maxHeightUi = 180
|
|
|
|
+
|
|
|
|
+export class ImageControls<P, S extends ImageControlsState> extends CollapsableControls<P, S> {
|
|
|
|
+ private canvasRef = React.createRef<HTMLCanvasElement>()
|
|
|
|
+
|
|
|
|
+ private canvas: HTMLCanvasElement
|
|
|
|
+ private canvasContext: CanvasRenderingContext2D
|
|
|
|
+
|
|
|
|
+ private imagePass: ImagePass
|
|
|
|
+
|
|
|
|
+ constructor(props: P, context?: any) {
|
|
|
|
+ super(props, context)
|
|
|
|
+
|
|
|
|
+ this.subscribe(this.plugin.events.canvas3d.initialized, () => this.forceUpdate())
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private getSize() {
|
|
|
|
+ return this.state.size === 'canvas' ? {
|
|
|
|
+ width: this.plugin.canvas3d.webgl.gl.drawingBufferWidth,
|
|
|
|
+ height: this.plugin.canvas3d.webgl.gl.drawingBufferHeight
|
|
|
|
+ } : {
|
|
|
|
+ width: this.state.width,
|
|
|
|
+ height: this.state.height
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private preview = () => {
|
|
|
|
+ const { width, height } = this.getSize()
|
|
|
|
+ if (width <= 0 || height <= 0) return
|
|
|
|
+
|
|
|
|
+ let w: number, h: number
|
|
|
|
+ const aH = maxHeightUi / height
|
|
|
|
+ const aW = maxWidthUi / width
|
|
|
|
+ if (aH < aW) {
|
|
|
|
+ h = Math.round(Math.min(maxHeightUi, height))
|
|
|
|
+ w = Math.round(width * (h / height))
|
|
|
|
+ } else {
|
|
|
|
+ w = Math.round(Math.min(maxWidthUi, width))
|
|
|
|
+ h = Math.round(height * (w / width))
|
|
|
|
+ }
|
|
|
|
+ setCanvasSize(this.canvas, w, h)
|
|
|
|
+ const { pixelRatio } = this.plugin.canvas3d.webgl
|
|
|
|
+ const imageData = this.imagePass.getImageData(w * pixelRatio, h * pixelRatio)
|
|
|
|
+ this.canvasContext.putImageData(imageData, 0, 0)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private downloadTask = () => {
|
|
|
|
+ return Task.create('Download Image', async ctx => {
|
|
|
|
+ const { width, height } = this.getSize()
|
|
|
|
+ if (width <= 0 || height <= 0) return
|
|
|
|
+
|
|
|
|
+ await ctx.update('Rendering image...')
|
|
|
|
+ const imageData = this.imagePass.getImageData(width, height)
|
|
|
|
+
|
|
|
|
+ await ctx.update('Encoding image...')
|
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
|
+ canvas.width = imageData.width
|
|
|
|
+ canvas.height = imageData.height
|
|
|
|
+ const canvasCtx = canvas.getContext('2d')
|
|
|
|
+ if (!canvasCtx) throw new Error('Could not create canvas 2d context')
|
|
|
|
+ canvasCtx.putImageData(imageData, 0, 0)
|
|
|
|
+
|
|
|
|
+ await ctx.update('Downloading image...')
|
|
|
|
+ const blob = await canvasToBlob(canvas)
|
|
|
|
+ download(blob, 'molstar-image')
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private download = () => {
|
|
|
|
+ this.plugin.runTask(this.downloadTask())
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private syncCanvas() {
|
|
|
|
+ if (!this.canvasRef.current) return
|
|
|
|
+ if (this.canvasRef.current === this.canvas) return
|
|
|
|
+
|
|
|
|
+ this.canvas = this.canvasRef.current
|
|
|
|
+ const ctx = this.canvas.getContext('2d')
|
|
|
|
+ if (!ctx) throw new Error('Could not get canvas 2d context')
|
|
|
|
+ this.canvasContext = ctx
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private handlePreview() {
|
|
|
|
+ if (this.state.showPreview) {
|
|
|
|
+ this.syncCanvas()
|
|
|
|
+ this.preview()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ componentDidUpdate() {
|
|
|
|
+ this.handlePreview()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ componentDidMount() {
|
|
|
|
+ this.imagePass = this.plugin.canvas3d.getImagePass()
|
|
|
|
+ this.imagePass.setProps({
|
|
|
|
+ multiSample: { mode: 'on', sampleLevel: 2 },
|
|
|
|
+ postprocessing: this.plugin.canvas3d.props.postprocessing
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ this.handlePreview()
|
|
|
|
+
|
|
|
|
+ this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => {
|
|
|
|
+ this.imagePass.setProps({
|
|
|
|
+ multiSample: { mode: 'on', sampleLevel: 2 },
|
|
|
|
+ postprocessing: this.plugin.canvas3d.props.postprocessing
|
|
|
|
+ })
|
|
|
|
+ this.handlePreview()
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ this.subscribe(this.plugin.canvas3d.didDraw, () => this.handlePreview())
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private togglePreview = () => this.setState({ showPreview: !this.state.showPreview })
|
|
|
|
+
|
|
|
|
+ private setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
|
|
|
|
+ if (p.name === 'size') {
|
|
|
|
+ if (p.value.name === 'custom') {
|
|
|
|
+ this.setState({ size: p.value.name, width: p.value.params.width, height: p.value.params.height })
|
|
|
|
+ } else {
|
|
|
|
+ this.setState({ size: p.value.name })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private get params () {
|
|
|
|
+ const max = Math.min(this.plugin.canvas3d ? this.plugin.canvas3d.webgl.maxRenderbufferSize : 4096, 8192)
|
|
|
|
+ const { width, height } = this.defaultState()
|
|
|
|
+ return {
|
|
|
|
+ size: PD.MappedStatic('custom', {
|
|
|
|
+ canvas: PD.Group({}),
|
|
|
|
+ custom: PD.Group({
|
|
|
|
+ width: PD.Numeric(width, { min: 1, max, step: 1 }),
|
|
|
|
+ height: PD.Numeric(height, { min: 1, max, step: 1 }),
|
|
|
|
+ }, { isFlat: true })
|
|
|
|
+ }, { options: [['canvas', 'Canvas'], ['custom', 'Custom']] })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private get values () {
|
|
|
|
+ return this.state.size === 'canvas'
|
|
|
|
+ ? { size: { name: 'canvas', params: {} } }
|
|
|
|
+ : { size: { name: 'custom', params: { width: this.state.width, height: this.state.height } } }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected defaultState() {
|
|
|
|
+ return {
|
|
|
|
+ isCollapsed: false,
|
|
|
|
+ header: 'Create Image',
|
|
|
|
+
|
|
|
|
+ showPreview: false,
|
|
|
|
+
|
|
|
|
+ size: 'canvas',
|
|
|
|
+ width: 1920,
|
|
|
|
+ height: 1080
|
|
|
|
+ } as S
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected renderControls() {
|
|
|
|
+ return <div>
|
|
|
|
+ <div className='msp-control-row'>
|
|
|
|
+ <button className='msp-btn msp-btn-block' onClick={this.download}>Download</button>
|
|
|
|
+ </div>
|
|
|
|
+ <ParameterControls params={this.params} values={this.values} onChange={this.setProps} />
|
|
|
|
+ <div className='msp-control-group-wrapper'>
|
|
|
|
+ <div className='msp-control-group-header'>
|
|
|
|
+ <button className='msp-btn msp-btn-block' onClick={this.togglePreview}>
|
|
|
|
+ <span className={`msp-icon msp-icon-${this.state.showPreview ? 'collapse' : 'expand'}`} />
|
|
|
|
+ Preview
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ {this.state.showPreview && <div className='msp-control-offset'>
|
|
|
|
+ <div className='msp-image-preview'>
|
|
|
|
+ <canvas width='0px' height='0px' ref={this.canvasRef} />
|
|
|
|
+ </div>
|
|
|
|
+ </div>}
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ }
|
|
|
|
+}
|