Browse Source

mol-plugin: screenshot UI improvements

David Sehnal 5 years ago
parent
commit
42b4c25ca3

+ 4 - 0
src/mol-plugin-ui/skin/base/icons.scss

@@ -239,4 +239,8 @@
 
 .msp-icon-download:before {
 	content: "\e82d";
+}
+
+.msp-icon-export:before {
+	content: "\e835";
 }

+ 2 - 14
src/mol-plugin-ui/viewport.tsx

@@ -16,8 +16,7 @@ import { DownloadScreenshotControls } from './viewport/screenshot';
 
 interface ViewportControlsState {
     isSettingsExpanded: boolean,
-    isScreenshotExpanded: boolean,
-    isHelpExpanded: boolean
+    isScreenshotExpanded: boolean
 }
 
 interface ViewportControlsProps {
@@ -26,8 +25,7 @@ interface ViewportControlsProps {
 export class ViewportControls extends PluginUIComponent<ViewportControlsProps, ViewportControlsState> {
     private allCollapsedState: ViewportControlsState = {
         isSettingsExpanded: false,
-        isScreenshotExpanded: false,
-        isHelpExpanded: false
+        isScreenshotExpanded: false
     };
 
     state = { ...this.allCollapsedState } as ViewportControlsState;
@@ -44,7 +42,6 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
     }
 
     toggleSettingsExpanded = this.toggle('isSettingsExpanded');
-    toggleHelpExpanded = this.toggle('isHelpExpanded');
     toggleScreenshotExpanded = this.toggle('isScreenshotExpanded');
 
     toggleControls = () => {
@@ -103,16 +100,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
                     {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}
                     {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}
                 </div>
-                {/* <div>
-                    <div className='msp-semi-transparent-background' />
-                    {this.icon('help-circle', this.toggleHelpExpanded, 'Help', this.state.isHelpExpanded)}
-                </div> */}
             </div>
-            {/* {this.state.isHelpExpanded && <div className='msp-viewport-controls-panel'>
-                <ControlGroup header='Help' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleHelpExpanded} topRightIcon='off'>
-                    <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} />

+ 2 - 2
src/mol-plugin-ui/viewport/help.tsx

@@ -158,8 +158,8 @@ export class HelpContent extends PluginUIComponent {
             </HelpGroup> */}
             <HelpGroup header='Create an Image'>
                 <HelpText>
-                    <p>Use the <Icon name='screenshot' /> icon in the viewport or go to the <i>Create Image</i> panel and click <i>download</i> to get the same image you see on the 3D canvas.</p>
-                    <p>To adjust the size of the image, select <i>Custom</i> from the <i>Size</i> dropdown in the <i>Create Image</i> panel. Adjust the <i>Width</i> and <i>Height</i> using the sliders. To see an image preview with the correct aspect ratio, activate the preview by expanding the <i>Preview</i> panel.</p>
+                    <p>Use the <Icon name='screenshot' /> icon in the viewport to bring up the screenshot controls.</p>
+                    <p>To adjust the size of the image, use the <i>Resolution</i> dropdown.</p>
                 </HelpText>
             </HelpGroup>
 

+ 37 - 20
src/mol-plugin-ui/viewport/screenshot.tsx

@@ -12,31 +12,25 @@ import { PluginUIComponent } from '../base';
 import { Icon } from '../controls/common';
 import { debounceTime } from 'rxjs/operators';
 import { Subject } from 'rxjs';
+import { ViewportScreenshotHelper } from '../../mol-plugin/util/viewport-screenshot';
 
 interface ImageControlsState {
     showPreview: boolean
 
-    size: 'canvas' | 'custom'
-    width: number
-    height: number
-
+    resolution?: ViewportScreenshotHelper.ResolutionSettings,
     isDisabled: boolean
 }
 
 export class DownloadScreenshotControls extends PluginUIComponent<{ close: () => void }, ImageControlsState> {
     state: ImageControlsState = {
         showPreview: true,
-        ...this.plugin.helpers.viewportScreenshot?.size,
+        resolution: this.plugin.helpers.viewportScreenshot?.currentResolution,
         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();
@@ -47,6 +41,28 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
         this.props.close();
     }
 
+    private openTab = () => {
+        // modified from https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript/16245768#16245768
+
+        const base64 = this.imgRef.current!.src;
+        const byteCharacters = atob(base64.substr(`data:image/png;base64,`.length));
+        const byteArrays = [];
+
+        const sliceSize = Math.min(byteCharacters.length, 1024 * 1024);
+        for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+            const byteNumbers = new Uint8Array(Math.min(sliceSize, byteCharacters.length - offset));
+            for (let i = 0, _i = byteNumbers.length; i < _i; i++) {
+                byteNumbers[i] = byteCharacters.charCodeAt(offset + i);
+            }
+            byteArrays.push(byteNumbers);
+        }
+        const blob = new Blob(byteArrays, { type: 'image/png' });
+        const blobUrl = URL.createObjectURL(blob);
+
+        window.open(blobUrl, '_blank');
+        this.props.close();
+    }
+
     private handlePreview() {
         if (this.state.showPreview) {
             this.preview()
@@ -63,7 +79,7 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
         this.subscribe(debounceTime(250)(this.updateQueue), () => this.handlePreview());
 
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => {
-            this.imagePass.setProps({
+            this.plugin.helpers.viewportScreenshot!.imagePass.setProps({
                 multiSample: { mode: 'on', sampleLevel: 2 },
                 postprocessing: this.plugin.canvas3d?.props.postprocessing
             })
@@ -84,16 +100,16 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
     }
 
     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 })
+        if (p.name === 'resolution') {
+            const resolution = p.value as ViewportScreenshotHelper.ResolutionSettings
+            if (resolution.name === 'custom') {
+                this.plugin.helpers.viewportScreenshot!.currentResolution.type = 'custom';
+                this.plugin.helpers.viewportScreenshot!.currentResolution.width = resolution.params.width;
+                this.plugin.helpers.viewportScreenshot!.currentResolution.height = resolution.params.height;
             } else {
-                this.plugin.helpers.viewportScreenshot!.size.type = 'canvas';
-                this.setState({ size: p.value.name })
+                this.plugin.helpers.viewportScreenshot!.currentResolution.type = resolution.name;
             }
+            this.setState({ resolution });
         }
     }
 
@@ -103,8 +119,9 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
                 <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 className='msp-btn-row-group'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.download} disabled={this.state.isDisabled}><Icon name='download' /> Download</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.openTab} disabled={this.state.isDisabled}><Icon name='export' /> Open in new Tab</button>
             </div>
             <ParameterControls params={this.plugin.helpers.viewportScreenshot!.params} values={this.plugin.helpers.viewportScreenshot!.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
         </div>

+ 3 - 3
src/mol-plugin/context.ts

@@ -42,7 +42,7 @@ import { StructureSelectionHelper } from './util/structure-selection-helper';
 import { StructureOverpaintHelper } from './util/structure-overpaint-helper';
 import { PluginToastManager } from './state/toast';
 import { StructureMeasurementManager } from './util/structure-measurement';
-import { ViewportScreenshotWrapper } from './util/viewport-screenshot';
+import { ViewportScreenshotHelper } from './util/viewport-screenshot';
 import { StructureRepresentationManager } from './state/representation/structure';
 
 interface Log {
@@ -139,7 +139,7 @@ export class PluginContext {
         structureRepresentation: new StructureRepresentationHelper(this),
         structureOverpaint: new StructureOverpaintHelper(this),
         substructureParent: new SubstructureParentHelper(this),
-        viewportScreenshot: void 0 as ViewportScreenshotWrapper | undefined
+        viewportScreenshot: void 0 as ViewportScreenshotHelper | undefined
     } as const;
 
     /**
@@ -159,7 +159,7 @@ export class PluginContext {
             const renderer = this.canvas3d!.props.renderer;
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { renderer: { ...renderer, backgroundColor: Color(0xFCFBF9) } } });
             this.canvas3d!.animate();
-            (this.helpers.viewportScreenshot as ViewportScreenshotWrapper) = new ViewportScreenshotWrapper(this);
+            (this.helpers.viewportScreenshot as ViewportScreenshotHelper) = new ViewportScreenshotHelper(this);
             return true;
         } catch (e) {
             this.log.error('' + e);

+ 75 - 18
src/mol-plugin/util/viewport-screenshot.ts

@@ -15,25 +15,47 @@ 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() {
+export { ViewportScreenshotHelper }
+
+namespace ViewportScreenshotHelper {
+    export type ResolutionSettings = PD.Values<ReturnType<ViewportScreenshotHelper['createParams']>>['resolution']
+    export type ResolutionTypes = ResolutionSettings['name']
+}
+
+class ViewportScreenshotHelper {
+    private createParams() {
         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({}),
+            resolution: PD.MappedStatic('full-hd', {
+                viewport: PD.Group({}),
+                hd: PD.Group({}),
+                'full-hd': PD.Group({}),
+                'ultra-hd': PD.Group({}),
                 custom: PD.Group({
-                    width: PD.Numeric(width, { min: 128, max, step: 1 }),
-                    height: PD.Numeric(height, { min: 128, max, step: 1 }),
+                    width: PD.Numeric(1920, { min: 128, max, step: 1 }),
+                    height: PD.Numeric(1080, { min: 128, max, step: 1 }),
                 }, { isFlat: true })
-            }, { options: [['canvas', 'Canvas'], ['custom', 'Custom']] })
+            }, {
+                options: [
+                    ['viewport', 'Viewport'],
+                    ['hd', 'HD (1280 x 720)'],
+                    ['full-hd', 'Full HD (1920 x 1080)'],
+                    ['ultra-hd', 'Ultra HD (3840 x 2160)'],
+                    ['custom', 'Custom']
+                ]
+            })
         }
     }
+    private _params: ReturnType<ViewportScreenshotHelper['createParams']> = void 0 as any;
+    get params() {
+        if (this._params) return this._params;
+        return this._params = this.createParams();
+    }
 
     get values() {
-        return this.size.type === 'canvas'
-            ? { size: { name: 'canvas', params: {} } }
-            : { size: { name: 'custom', params: { width: this.size.width, height: this.size.height } } }
+        return this.currentResolution.type === 'custom'
+            ? { resolution: { name: 'custom', params: { width: this.currentResolution.width, height: this.currentResolution.height } } }
+            : { resolution: { name: this.currentResolution.type, params: { } } };
     }
 
     private getCanvasSize() {
@@ -43,15 +65,20 @@ export class ViewportScreenshotWrapper {
         };
     }
 
-    size = {
-        type: 'custom' as 'canvas' | 'custom',
+    currentResolution = {
+        type: 'full-hd' as ViewportScreenshotHelper.ResolutionTypes,
         width: 1920,
         height: 1080
-    }
-
-    getSize() {
-        if (this.size.type === 'canvas') return this.getCanvasSize();
-        return { width: this.size.width, height: this.size.height };
+    };
+
+    private getSize() {
+        switch (this.currentResolution.type ) {
+            case 'viewport': return this.getCanvasSize();
+            case 'hd': return { width: 1280, height: 720 };
+            case 'full-hd': return { width: 1920, height: 1080 };
+            case 'ultra-hd': return { width: 3840, height: 2160 };
+            default: return { width: this.currentResolution.width, height: this.currentResolution.height }
+        }
     }
 
     private _imagePass: ImagePass;
@@ -80,6 +107,28 @@ export class ViewportScreenshotWrapper {
         return canvas;
     }();
 
+    // 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 async draw(ctx: RuntimeContext) {
         const { width, height } = this.getSize();
         if (width <= 0 || height <= 0) return;
@@ -106,6 +155,14 @@ export class ViewportScreenshotWrapper {
         })
     }
 
+    downloadCurrent() {
+        return this.plugin.runTask(Task.create('Download Image', async ctx => {
+            await ctx.update('Downloading image...')
+            const blob = await canvasToBlob(this.canvas, 'png')
+            download(blob, this.getFilename())
+        }));
+    }
+
     async imageData() {
         await this.draw(SyncRuntimeContext)
         return this.canvas.toDataURL();