Skip to content
Snippets Groups Projects
Commit b75ba4b4 authored by David Sehnal's avatar David Sehnal
Browse files

mol-plugin: screenshot controls

parent baa80d08
No related branches found
No related tags found
No related merge requests found
/**
* 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 { setCanvasSize } from '../mol-canvas3d/util';
interface ImageControlsState extends CollapsableState {
showPreview: boolean
size: 'canvas' | 'custom'
width: number
height: number
isDisabled: boolean
}
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
get imagePass() {
return this.plugin.helpers.viewportScreenshot!.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' && this.plugin.canvas3d ? {
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.pixelRatio || 1
const pw = Math.round(w * pixelRatio)
const ph = Math.round(h * pixelRatio)
const imageData = this.imagePass.getImageData(pw, ph)
this.canvasContext.putImageData(imageData, 0, 0)
}
private download = () => {
this.plugin.helpers.viewportScreenshot?.download();
}
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.plugin.helpers.viewportScreenshot!.size = this.getSize();
this.handlePreview()
}
componentDidMount() {
if (!this.plugin.canvas3d) return;
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()
})
this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v }))
}
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: 128, max, step: 1 }),
height: PD.Numeric(height, { min: 128, 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,
isDisabled: false
} as S
}
protected renderControls() {
return <div>
<div className='msp-control-row'>
<button className='msp-btn msp-btn-block' onClick={this.download} disabled={this.state.isDisabled}>Download</button>
</div>
<ParameterControls params={this.params} values={this.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
<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>
}
}
\ No newline at end of file
......@@ -19,7 +19,6 @@ import { StateTransform } from '../mol-state';
import { UpdateTransformControl } from './state/update-transform';
import { SequenceView } from './sequence';
import { Toasts } from './toast';
import { ImageControls } from './image';
import { SectionHeader } from './controls/common';
import { LeftPanelControls } from './left-panel';
......@@ -128,7 +127,6 @@ export class ControlsWrapper extends PluginUIComponent {
{/* <AnimationControlsWrapper /> */}
{/* <CameraSnapshots /> */}
<StructureToolsWrapper />
<ImageControls />
</div>;
}
}
......
......@@ -376,19 +376,19 @@
position: relative;
background: $default-background;
margin-top: 1px;
display: flex;
justify-content: center;
> canvas {
max-height: 200px;
border-width: 0px 1px 0px 1px;
border-style: solid;
border-color: $border-color;
background-color: $default-background;
background-image: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey),
linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
text-align: center;
padding: $control-spacing;
img {
max-height: 180px;
max-width: 100%;
display: 'block';
}
> span {
margin-top: 6px;
display: block;
text-align: center;
font-size: 80%;
}
}
\ No newline at end of file
......@@ -236,3 +236,7 @@
.msp-icon-address:before {
content: "\e841";
}
.msp-icon-download:before {
content: "\e82d";
}
\ No newline at end of file
......@@ -12,9 +12,11 @@ import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base';
import { ControlGroup, IconButton } from './controls/common';
import { SimpleSettingsControl } from './viewport/simple-settings';
import { DownloadScreenshotControls } from './viewport/screenshot';
interface ViewportControlsState {
isSettingsExpanded: boolean,
isScreenshotExpanded: boolean,
isHelpExpanded: boolean
}
......@@ -22,24 +24,28 @@ interface ViewportControlsProps {
}
export class ViewportControls extends PluginUIComponent<ViewportControlsProps, ViewportControlsState> {
state = {
private allCollapsedState: ViewportControlsState = {
isSettingsExpanded: false,
isScreenshotExpanded: false,
isHelpExpanded: false
};
state = { ...this.allCollapsedState } as ViewportControlsState;
resetCamera = () => {
PluginCommands.Camera.Reset.dispatch(this.plugin, {});
}
toggleSettingsExpanded = (e?: React.MouseEvent<HTMLButtonElement>) => {
this.setState({ isSettingsExpanded: !this.state.isSettingsExpanded, isHelpExpanded: false });
private toggle(panel: keyof ViewportControlsState) {
return (e?: React.MouseEvent<HTMLButtonElement>) => {
this.setState({ ...this.allCollapsedState, [panel]: !this.state[panel] });
e?.currentTarget.blur();
};
}
toggleHelpExpanded = (e?: React.MouseEvent<HTMLButtonElement>) => {
this.setState({ isSettingsExpanded: false, isHelpExpanded: !this.state.isHelpExpanded });
e?.currentTarget.blur();
}
toggleSettingsExpanded = this.toggle('isSettingsExpanded');
toggleHelpExpanded = this.toggle('isHelpExpanded');
toggleScreenshotExpanded = this.toggle('isScreenshotExpanded');
toggleControls = () => {
PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.state.showControls } });
......@@ -89,7 +95,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
</div>
<div>
<div className='msp-semi-transparent-background' />
{this.icon('screenshot', this.screenshot, 'Download Screenshot')}
{this.icon('screenshot', this.toggleScreenshotExpanded, 'Screenshot', this.state.isScreenshotExpanded)}
</div>
<div>
<div className='msp-semi-transparent-background' />
......@@ -107,19 +113,15 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
<HelpContent />
</ControlGroup>
</div>} */}
{this.state.isScreenshotExpanded && <div className='msp-viewport-controls-panel'>
<ControlGroup header='Screenshot' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleScreenshotExpanded} topRightIcon='off'>
<DownloadScreenshotControls close={this.toggleScreenshotExpanded} />
</ControlGroup>
</div>}
{this.state.isSettingsExpanded && <div className='msp-viewport-controls-panel'>
<ControlGroup header='Basic Settings' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleSettingsExpanded} topRightIcon='off'>
<SimpleSettingsControl />
</ControlGroup>
{/* <ControlGroup header='Layout' initialExpanded={true}>
<ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} />
</ControlGroup>
<ControlGroup header='Interactivity' initialExpanded={true}>
<ParameterControls params={Interactivity.Params} values={this.plugin.interactivity.props} onChange={this.setInteractivityProps} />
</ControlGroup>
{this.plugin.canvas3d && <ControlGroup header='Viewport' initialExpanded={true}>
<ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
</ControlGroup>} */}
</div>}
</div>
}
......
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ParameterControls } from '../controls/parameters';
import { PluginUIComponent } from '../base';
import { Icon } from '../controls/common';
import { debounceTime } from 'rxjs/operators';
import { Subject } from 'rxjs';
interface ImageControlsState {
showPreview: boolean
size: 'canvas' | 'custom'
width: number
height: number
isDisabled: boolean
}
export class DownloadScreenshotControls extends PluginUIComponent<{ close: () => void }, ImageControlsState> {
state: ImageControlsState = {
showPreview: true,
...this.plugin.helpers.viewportScreenshot?.size,
isDisabled: false
} as ImageControlsState
private imgRef = React.createRef<HTMLImageElement>()
private updateQueue = new Subject();
get imagePass() {
return this.plugin.helpers.viewportScreenshot!.imagePass;
}
private preview = async () => {
if (!this.imgRef.current) return;
this.imgRef.current!.src = await this.plugin.helpers.viewportScreenshot!.imageData();
}
private download = () => {
this.plugin.helpers.viewportScreenshot?.download();
this.props.close();
}
private handlePreview() {
if (this.state.showPreview) {
this.preview()
}
}
componentDidUpdate() {
this.updateQueue.next();
}
componentDidMount() {
if (!this.plugin.canvas3d) return;
this.subscribe(debounceTime(250)(this.updateQueue), () => this.handlePreview());
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => {
this.imagePass.setProps({
multiSample: { mode: 'on', sampleLevel: 2 },
postprocessing: this.plugin.canvas3d?.props.postprocessing
})
this.updateQueue.next();
})
this.subscribe(debounceTime(250)(this.plugin.canvas3d.didDraw), () => {
if (this.state.isDisabled) return;
this.updateQueue.next();
})
this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
this.setState({ isDisabled: v })
if (!v) this.updateQueue.next();
})
this.handlePreview();
}
private setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
if (p.name === 'size') {
if (p.value.name === 'custom') {
this.plugin.helpers.viewportScreenshot!.size.type = 'custom';
this.plugin.helpers.viewportScreenshot!.size.width = p.value.params.width;
this.plugin.helpers.viewportScreenshot!.size.height = p.value.params.height;
this.setState({ size: p.value.name, width: p.value.params.width, height: p.value.params.height })
} else {
this.plugin.helpers.viewportScreenshot!.size.type = 'canvas';
this.setState({ size: p.value.name })
}
}
}
render() {
return <div>
<div className='msp-image-preview'>
<img ref={this.imgRef} /><br />
<span>Right-click the image to Copy.</span>
</div>
<div className='msp-control-row'>
<button className='msp-btn msp-btn-block' onClick={this.download} disabled={this.state.isDisabled}><Icon name='download' /> Download</button>
</div>
<ParameterControls params={this.plugin.helpers.viewportScreenshot!.params} values={this.plugin.helpers.viewportScreenshot!.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
</div>
}
}
\ No newline at end of file
......@@ -9,11 +9,33 @@ import { PluginContext } from '../context';
import { ImagePass } from '../../mol-canvas3d/passes/image';
import { StateSelection } from '../../mol-state';
import { PluginStateObject } from '../state/objects';
import { Task } from '../../mol-task';
import { Task, RuntimeContext } from '../../mol-task';
import { canvasToBlob } from '../../mol-canvas3d/util';
import { download } from '../../mol-util/download';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { SyncRuntimeContext } from '../../mol-task/execution/synchronous';
export class ViewportScreenshotWrapper {
get params() {
const max = Math.min(this.plugin.canvas3d ? this.plugin.canvas3d.webgl.maxRenderbufferSize : 4096, 4096)
const { width, height } = this.size
return {
size: PD.MappedStatic(this.size.type, {
canvas: PD.Group({}),
custom: PD.Group({
width: PD.Numeric(width, { min: 128, max, step: 1 }),
height: PD.Numeric(height, { min: 128, max, step: 1 }),
}, { isFlat: true })
}, { options: [['canvas', 'Canvas'], ['custom', 'Custom']] })
}
}
get values() {
return this.size.type === 'canvas'
? { size: { name: 'canvas', params: {} } }
: { size: { name: 'custom', params: { width: this.size.width, height: this.size.height } } }
}
private getCanvasSize() {
return {
width: this.plugin.canvas3d?.webgl.gl.drawingBufferWidth || 0,
......@@ -21,7 +43,16 @@ export class ViewportScreenshotWrapper {
};
}
size = this.getCanvasSize();
size = {
type: 'custom' as 'canvas' | 'custom',
width: 1920,
height: 1080
}
getSize() {
if (this.size.type === 'canvas') return this.getCanvasSize();
return { width: this.size.width, height: this.size.height };
}
private _imagePass: ImagePass;
......@@ -44,28 +75,42 @@ export class ViewportScreenshotWrapper {
return `${idString || 'molstar-image'}.png`
}
private downloadTask() {
return Task.create('Download Image', async ctx => {
const { width, height } = this.size
if (width <= 0 || height <= 0) return
private canvas = function () {
const canvas = document.createElement('canvas');
return canvas;
}();
private async draw(ctx: RuntimeContext) {
const { width, height } = this.getSize();
if (width <= 0 || height <= 0) return;
await ctx.update('Rendering image...')
const imageData = this.imagePass.getImageData(width, height)
const imageData = this.imagePass.getImageData(width, height);
await ctx.update('Encoding image...')
const canvas = document.createElement('canvas')
const canvas = this.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)
return;
}
private downloadTask() {
return Task.create('Download Image', async ctx => {
this.draw(ctx);
await ctx.update('Downloading image...')
const blob = await canvasToBlob(canvas, 'png')
const blob = await canvasToBlob(this.canvas, 'png')
download(blob, this.getFilename())
})
}
async imageData() {
await this.draw(SyncRuntimeContext)
return this.canvas.toDataURL();
}
download() {
this.plugin.runTask(this.downloadTask());
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment