|
@@ -6,21 +6,21 @@
|
|
|
*/
|
|
|
|
|
|
import * as React from 'react';
|
|
|
-import { ParameterControls } from '../controls/parameters';
|
|
|
-import { PluginUIComponent } from '../base';
|
|
|
-import { debounceTime } from 'rxjs/operators';
|
|
|
+import { useRef, useState } from 'react';
|
|
|
import { Subject } from 'rxjs';
|
|
|
-import { ViewportScreenshotHelper, ViewportScreenshotHelperParams } from '../../mol-plugin/util/viewport-screenshot';
|
|
|
-import { Button, ExpandGroup } from '../controls/common';
|
|
|
+import { debounceTime } from 'rxjs/operators';
|
|
|
+import { Viewport } from '../../mol-canvas3d/camera/util';
|
|
|
import { CameraHelperProps } from '../../mol-canvas3d/helper/camera-helper';
|
|
|
+import { equalEps } from '../../mol-math/linear-algebra/3d/common';
|
|
|
import { PluginCommands } from '../../mol-plugin/commands';
|
|
|
-import { StateExportImportControls, LocalStateSnapshotParams } from '../state/snapshots';
|
|
|
-import { CopySvg, GetAppSvg, RefreshSvg } from '../controls/icons';
|
|
|
import { PluginContext } from '../../mol-plugin/context';
|
|
|
+import { ViewportScreenshotHelper, ViewportScreenshotHelperParams } from '../../mol-plugin/util/viewport-screenshot';
|
|
|
+import { PluginUIComponent } from '../base';
|
|
|
+import { Button, ExpandGroup, ToggleButton } from '../controls/common';
|
|
|
+import { CopySvg, CropFreeSvg, CropOrginalSvg, CropSvg, GetAppSvg } from '../controls/icons';
|
|
|
+import { ParameterControls } from '../controls/parameters';
|
|
|
import { useBehavior } from '../hooks/use-behavior';
|
|
|
-import { Viewport } from '../../mol-canvas3d/camera/util';
|
|
|
-import { useEffect, useRef, useState } from 'react';
|
|
|
-import { equalEps } from '../../mol-math/linear-algebra/3d/common';
|
|
|
+import { LocalStateSnapshotParams, StateExportImportControls } from '../state/snapshots';
|
|
|
|
|
|
interface ImageControlsState {
|
|
|
showPreview: boolean
|
|
@@ -55,9 +55,27 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
|
|
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
const frame = getViewportFrame(width, height, w, h);
|
|
|
+ if (this.plugin.helpers.viewportScreenshot?.values.transparent) {
|
|
|
+ this.drawCheckerboard(ctx, frame);
|
|
|
+ }
|
|
|
ctx.drawImage(canvas, frame.x, frame.y, frame.width, frame.height);
|
|
|
}
|
|
|
|
|
|
+ private drawCheckerboard(ctx: CanvasRenderingContext2D, frame: Viewport) {
|
|
|
+ // must be odd number!
|
|
|
+ const s = 13;
|
|
|
+ for (let i = 0; i < frame.width; i += s) {
|
|
|
+ for (let j = 0; j < frame.height; j += s) {
|
|
|
+ ctx.fillStyle = (i + j) % 2 ? '#ffffff' : '#bfbfbf';
|
|
|
+
|
|
|
+ const x = frame.x + i, y = frame.y + j;
|
|
|
+ const w = i + s > frame.width ? frame.width - i : s;
|
|
|
+ const h = j + s > frame.height ? frame.height - j : s;
|
|
|
+ ctx.fillRect(x, y, w, h);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private download = () => {
|
|
|
this.plugin.helpers.viewportScreenshot?.download();
|
|
|
this.props.close();
|
|
@@ -96,10 +114,6 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
|
|
|
this.updateQueue.next();
|
|
|
});
|
|
|
|
|
|
- this.subscribe(this.plugin.helpers.viewportScreenshot!.behaviors.relativeCrop, () => {
|
|
|
- this.forceUpdate();
|
|
|
- });
|
|
|
-
|
|
|
this.preview();
|
|
|
}
|
|
|
|
|
@@ -120,28 +134,24 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
|
|
|
PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: e.target.files![0] });
|
|
|
}
|
|
|
|
|
|
- onCanvasClick = (e: React.MouseEvent) => {
|
|
|
+ onCanvasContextMenu = (e: React.MouseEvent) => {
|
|
|
e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
};
|
|
|
|
|
|
render() {
|
|
|
const values = this.plugin.helpers.viewportScreenshot!.values;
|
|
|
- const crop = this.plugin.helpers.viewportScreenshot!.relativeCrop;
|
|
|
|
|
|
return <div>
|
|
|
<div className='msp-image-preview'>
|
|
|
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
|
|
- <canvas ref={this.canvasRef} onClick={this.onCanvasClick} onContextMenu={this.onCanvasClick} style={{ width: '100%', height: '180px' }} className={values.transparent ? 'msp-transparent-screenshot' : void 0}></canvas>
|
|
|
+ <canvas ref={this.canvasRef} onClick={this.onCanvasContextMenu} onContextMenu={this.onCanvasContextMenu} style={{ width: '100%', height: '180px' }}></canvas>
|
|
|
<ViewportFrame plugin={this.plugin} canvasRef={this.canvasRef} />
|
|
|
</div>
|
|
|
- <span>
|
|
|
- Drag the frame to crop the image.
|
|
|
- {!isFullFrame(crop) && <Button icon={RefreshSvg} title='Reset Crop' inline
|
|
|
- style={{ width: 'auto', background: 'transparent', height: '15px', lineHeight: '15px', padding: '0 4px', marginLeft: '8px', border: 'none' }}
|
|
|
- onClick={() => this.plugin.helpers.viewportScreenshot!.behaviors.relativeCrop.next({ x: 0, y: 0, width: 1, height: 1 })} />}
|
|
|
- </span>
|
|
|
+ <CropControls plugin={this.plugin} />
|
|
|
</div>
|
|
|
<div className='msp-flex-row'>
|
|
|
+ {/* TODO: figure out how to do copy/paste in Firefox */}
|
|
|
{!!(navigator.clipboard as any).write && <Button icon={CopySvg} onClick={this.copy} disabled={this.state.isDisabled}>Copy</Button>}
|
|
|
<Button icon={GetAppSvg} onClick={this.download} disabled={this.state.isDisabled}>Download</Button>
|
|
|
</div>
|
|
@@ -160,19 +170,33 @@ function isFullFrame(crop: Viewport) {
|
|
|
return equalEps(crop.x, 0, 1e-5) && equalEps(crop.y, 0, 1e-5) && equalEps(crop.width, 1, 1e-5) && equalEps(crop.height, 1, 1e-5);
|
|
|
}
|
|
|
|
|
|
-export function ViewportFrame({ plugin, canvasRef }: { plugin: PluginContext, canvasRef: React.RefObject<HTMLCanvasElement> }) {
|
|
|
+function CropControls({ plugin }: { plugin: PluginContext }) {
|
|
|
const helper = plugin.helpers.viewportScreenshot;
|
|
|
const params = useBehavior(helper?.behaviors.values);
|
|
|
const crop = useBehavior(helper?.behaviors.relativeCrop);
|
|
|
- const cropFrameRef = useRef<Viewport>({ x: 0, y: 0, width: 0, height: 0 });
|
|
|
|
|
|
+ if (!params || !crop || !helper) return null;
|
|
|
+
|
|
|
+ return <div style={{ width: '100%', height: '24px' }}>
|
|
|
+ <ToggleButton icon={CropOrginalSvg} title='Auto-crop' inline isSelected={params.autoCrop}
|
|
|
+ style={{ background: 'transparent', float: 'left', width: 'auto', height: '24px', lineHeight: '24px' }}
|
|
|
+ toggle={() => helper.behaviors.values.next({ ...params, autoCrop: !params.autoCrop })} label={'Auto-crop ' + (params.autoCrop ? 'On' : 'Off') } />
|
|
|
+
|
|
|
+ {!params.autoCrop && <Button icon={CropSvg} title='Crop'
|
|
|
+ style={{ background: 'transparent', float: 'right', height: '24px', lineHeight: '24px', width: '24px', padding: '0' }}
|
|
|
+ onClick={() => helper.autocrop()} />}
|
|
|
+ {!isFullFrame(crop) && <Button icon={CropFreeSvg} title='Reset Crop'
|
|
|
+ style={{ background: 'transparent', float: 'right', height: '24px', lineHeight: '24px', width: '24px', padding: '0' }}
|
|
|
+ onClick={() => helper.resetCrop()} />}
|
|
|
+ </div>;
|
|
|
+}
|
|
|
+
|
|
|
+function ViewportFrame({ plugin, canvasRef }: { plugin: PluginContext, canvasRef: React.RefObject<HTMLCanvasElement> }) {
|
|
|
+ const helper = plugin.helpers.viewportScreenshot;
|
|
|
+ const params = useBehavior(helper?.behaviors.values);
|
|
|
+ const crop = useBehavior(helper?.behaviors.relativeCrop);
|
|
|
+ const cropFrameRef = useRef<Viewport>({ x: 0, y: 0, width: 0, height: 0 });
|
|
|
useBehavior(params?.resolution.name === 'viewport' ? plugin.canvas3d?.resized : void 0);
|
|
|
- useEffect(() => {
|
|
|
- return () => {
|
|
|
- if (onMove) document.removeEventListener('mousemove', onMove);
|
|
|
- if (onEnd) document.removeEventListener('mouseup', onEnd);
|
|
|
- };
|
|
|
- }, []);
|
|
|
|
|
|
const [drag, setDrag] = React.useState<string>('');
|
|
|
const [start, setStart] = useState([0, 0]);
|
|
@@ -234,8 +258,8 @@ export function ViewportFrame({ plugin, canvasRef }: { plugin: PluginContext, ca
|
|
|
|
|
|
cropFrame.x = rectCrop.l;
|
|
|
cropFrame.y = rectCrop.t;
|
|
|
- cropFrame.width = rectCrop.r - rectCrop.l;
|
|
|
- cropFrame.height = rectCrop.b - rectCrop.t;
|
|
|
+ cropFrame.width = rectCrop.r - rectCrop.l + 1;
|
|
|
+ cropFrame.height = rectCrop.b - rectCrop.t + 1;
|
|
|
|
|
|
cropFrameRef.current = cropFrame;
|
|
|
|
|
@@ -250,15 +274,18 @@ export function ViewportFrame({ plugin, canvasRef }: { plugin: PluginContext, ca
|
|
|
const p = [e.pageX, e.pageY];
|
|
|
setStart(p);
|
|
|
setCurrent(p);
|
|
|
- document.addEventListener('mouseup', onEnd);
|
|
|
- document.addEventListener('mousemove', onMove);
|
|
|
+ window.addEventListener('mouseup', onEnd);
|
|
|
+ window.addEventListener('mousemove', onMove);
|
|
|
};
|
|
|
|
|
|
const onEnd = () => {
|
|
|
- document.removeEventListener('mouseup', onEnd);
|
|
|
- document.removeEventListener('mousemove', onMove);
|
|
|
+ window.removeEventListener('mouseup', onEnd);
|
|
|
+ window.removeEventListener('mousemove', onMove);
|
|
|
|
|
|
const cropFrame = cropFrameRef.current;
|
|
|
+ if (params?.autoCrop) {
|
|
|
+ helper.behaviors.values.next({ ...params, autoCrop: false });
|
|
|
+ }
|
|
|
helper?.behaviors.relativeCrop.next({
|
|
|
x: (cropFrame.x - frame.x) / frame.width,
|
|
|
y: (cropFrame.y - frame.y) / frame.height,
|
|
@@ -271,29 +298,32 @@ export function ViewportFrame({ plugin, canvasRef }: { plugin: PluginContext, ca
|
|
|
setCurrent(p);
|
|
|
};
|
|
|
|
|
|
+ const contextMenu = (e: React.MouseEvent) => {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ };
|
|
|
+
|
|
|
const d = 4;
|
|
|
- const border = `6px solid rgba(255, 87, 45, 0.75)`;
|
|
|
+ const border = `3px solid rgba(255, 87, 45, 0.75)`;
|
|
|
const transparent = 'transparent';
|
|
|
|
|
|
return <>
|
|
|
- <div style={{ position: 'absolute', left: frame.x, top: frame.y, width: frame.width, height: frame.height, border: '1px solid rgba(0, 0, 0, 0.25)' }} />
|
|
|
-
|
|
|
- <div data-drag='move' style={{ position: 'absolute', left: cropFrame.x, top: cropFrame.y, width: cropFrame.width, height: cropFrame.height, border, cursor: 'move' }} onMouseDown={onStart} draggable={false} />
|
|
|
+ <div data-drag='move' style={{ position: 'absolute', left: cropFrame.x, top: cropFrame.y, width: cropFrame.width, height: cropFrame.height, border, cursor: 'move' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
|
|
|
- <div data-drag='left' style={{ position: 'absolute', left: cropFrame.x - d, top: cropFrame.y + d, width: 4 * d, height: cropFrame.height - d, background: transparent, cursor: 'w-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
- <div data-drag='right' style={{ position: 'absolute', left: rectCrop.r - 2 * d, top: cropFrame.y, width: 4 * d, height: cropFrame.height - d, background: transparent, cursor: 'w-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
- <div data-drag='top' style={{ position: 'absolute', left: cropFrame.x - d, top: cropFrame.y - d, width: cropFrame.width + 2 * d, height: 4 * d, background: transparent, cursor: 'n-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
- <div data-drag='bottom' style={{ position: 'absolute', left: cropFrame.x - d, top: rectCrop.b - 2 * d, width: cropFrame.width + 2 * d, height: 4 * d, background: transparent, cursor: 'n-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
+ <div data-drag='left' style={{ position: 'absolute', left: cropFrame.x - d, top: cropFrame.y + d, width: 4 * d, height: cropFrame.height - d, background: transparent, cursor: 'w-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
+ <div data-drag='right' style={{ position: 'absolute', left: rectCrop.r - 2 * d, top: cropFrame.y, width: 4 * d, height: cropFrame.height - d, background: transparent, cursor: 'w-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
+ <div data-drag='top' style={{ position: 'absolute', left: cropFrame.x - d, top: cropFrame.y - d, width: cropFrame.width + 2 * d, height: 4 * d, background: transparent, cursor: 'n-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
+ <div data-drag='bottom' style={{ position: 'absolute', left: cropFrame.x - d, top: rectCrop.b - 2 * d, width: cropFrame.width + 2 * d, height: 4 * d, background: transparent, cursor: 'n-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
|
|
|
- <div data-drag='top, left' style={{ position: 'absolute', left: rectCrop.l - d, top: rectCrop.t - d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'nw-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
- <div data-drag='bottom, right' style={{ position: 'absolute', left: rectCrop.r - 2 * d, top: rectCrop.b - 2 * d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'nw-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
- <div data-drag='top, right' style={{ position: 'absolute', left: rectCrop.r - 2 * d, top: rectCrop.t - d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'ne-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
- <div data-drag='bottom, left' style={{ position: 'absolute', left: rectCrop.l - d, top: rectCrop.b - 2 * d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'ne-resize' }} onMouseDown={onStart} draggable={false} />
|
|
|
+ <div data-drag='top, left' style={{ position: 'absolute', left: rectCrop.l - d, top: rectCrop.t - d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'nw-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
+ <div data-drag='bottom, right' style={{ position: 'absolute', left: rectCrop.r - 2 * d, top: rectCrop.b - 2 * d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'nw-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
+ <div data-drag='top, right' style={{ position: 'absolute', left: rectCrop.r - 2 * d, top: rectCrop.t - d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'ne-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
+ <div data-drag='bottom, left' style={{ position: 'absolute', left: rectCrop.l - d, top: rectCrop.b - 2 * d, width: 4 * d, height: 4 * d, background: transparent, cursor: 'ne-resize' }} onMouseDown={onStart} draggable={false} onContextMenu={contextMenu} />
|
|
|
</>;
|
|
|
}
|
|
|
|
|
|
function toRect(viewport: Viewport) {
|
|
|
- return { l: viewport.x, t: viewport.y, r: viewport.x + viewport.width, b: viewport.y + viewport.height };
|
|
|
+ return { l: viewport.x, t: viewport.y, r: viewport.x + viewport.width - 1, b: viewport.y + viewport.height - 1 };
|
|
|
}
|
|
|
|
|
|
function getViewportFrame(srcWidth: number, srcHeight: number, w: number, h: number): Viewport {
|