molstar
Version:
A comprehensive macromolecular library.
420 lines (419 loc) • 17.6 kB
JavaScript
"use strict";
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ViewportScreenshotHelper = void 0;
const camera_helper_1 = require("../../mol-canvas3d/helper/camera-helper");
const util_1 = require("../../mol-canvas3d/util");
const common_1 = require("../../mol-math/linear-algebra/3d/common");
const component_1 = require("../../mol-plugin-state/component");
const objects_1 = require("../../mol-plugin-state/objects");
const mol_state_1 = require("../../mol-state");
const mol_task_1 = require("../../mol-task");
const color_1 = require("../../mol-util/color");
const download_1 = require("../../mol-util/download");
const param_definition_1 = require("../../mol-util/param-definition");
const set_1 = require("../../mol-util/set");
function checkWebPSupport() {
// adapted from https://stackoverflow.com/a/27232658
const elem = document.createElement('canvas');
if (!!(elem.getContext && elem.getContext('2d'))) {
// was able or not to get WebP representation
return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
else {
// very old browser like IE 8, canvas not supported
return false;
}
}
class ViewportScreenshotHelper extends component_1.PluginComponent {
createParams() {
let max = 8192;
if (this.plugin.canvas3d) {
const { webgl } = this.plugin.canvas3d;
max = Math.floor(Math.min(webgl.maxRenderbufferSize, webgl.maxTextureSize) / 2);
}
return {
resolution: param_definition_1.ParamDefinition.MappedStatic('viewport', {
viewport: param_definition_1.ParamDefinition.Group({}),
hd: param_definition_1.ParamDefinition.Group({}),
'full-hd': param_definition_1.ParamDefinition.Group({}),
'ultra-hd': param_definition_1.ParamDefinition.Group({}),
custom: param_definition_1.ParamDefinition.Group({
width: param_definition_1.ParamDefinition.Numeric(1920, { min: 128, max, step: 1 }),
height: param_definition_1.ParamDefinition.Numeric(1080, { min: 128, max, step: 1 }),
}, { isFlat: true })
}, {
options: [
['viewport', 'Viewport'],
['hd', 'HD (1280 x 720)'],
['full-hd', 'Full HD (1920 x 1080)'],
['ultra-hd', 'Ultra HD (3840 x 2160)'],
['custom', 'Custom']
]
}),
format: param_definition_1.ParamDefinition.MappedStatic('png', {
png: param_definition_1.ParamDefinition.Group({}),
webp: param_definition_1.ParamDefinition.Group({
quality: param_definition_1.ParamDefinition.Numeric(0.9, { min: 0, max: 1, step: 0.01 })
}),
jpeg: param_definition_1.ParamDefinition.Group({
quality: param_definition_1.ParamDefinition.Numeric(0.9, { min: 0, max: 1, step: 0.01 })
}),
}, {
options: [
['png', 'PNG'],
['jpeg', 'JPEG'],
...(checkWebPSupport() ? [['webp', 'WebP']] : []),
]
}),
transparent: param_definition_1.ParamDefinition.Boolean(false),
axes: camera_helper_1.CameraHelperParams.axes,
illumination: param_definition_1.ParamDefinition.Group({
extraIterations: param_definition_1.ParamDefinition.Numeric(1, { min: 0, max: 5, step: 1 }),
targetIterationTimeMs: param_definition_1.ParamDefinition.Numeric(300, { min: 100, max: 3000, step: 10 }),
}),
};
}
get params() {
if (this._params)
return this._params;
return this._params = this.createParams();
}
get values() {
return this.behaviors.values.value;
}
get cropParams() {
return this.behaviors.cropParams.value;
}
get relativeCrop() {
return this.behaviors.relativeCrop.value;
}
getCanvasSize() {
var _a, _b;
return {
width: ((_a = this.plugin.canvas3d) === null || _a === void 0 ? void 0 : _a.webgl.gl.drawingBufferWidth) || 0,
height: ((_b = this.plugin.canvas3d) === null || _b === void 0 ? void 0 : _b.webgl.gl.drawingBufferHeight) || 0
};
}
getSize() {
const values = this.values;
switch (values.resolution.name) {
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: values.resolution.params.width, height: values.resolution.params.height };
}
}
getPostprocessingProps() {
const c = this.plugin.canvas3d;
const aoProps = c.props.postprocessing.occlusion;
return {
...c.props.postprocessing,
occlusion: aoProps.name === 'on'
? { name: 'on', params: { ...aoProps.params, samples: 128, resolutionScale: c.webgl.pixelRatio, transparentThreshold: 1 } }
: aoProps
};
}
getIlluminationProps(isPreview) {
const c = this.plugin.canvas3d;
const giProps = c.props.illumination;
const { extraIterations, targetIterationTimeMs } = this.values.illumination;
return {
...giProps,
enabled: isPreview ? false : giProps.enabled,
maxIterations: Math.ceil(Math.log2(Math.pow(2, giProps.maxIterations + extraIterations) * giProps.rendersPerFrame[1])),
targetFps: 1000 / targetIterationTimeMs,
denoiseThreshold: [giProps.denoiseThreshold[0], giProps.denoiseThreshold[0]],
rendersPerFrame: [1, 1],
};
}
createPass(isPreview) {
const c = this.plugin.canvas3d;
const { colorBufferFloat, textureFloat } = c.webgl.extensions;
return c.getImagePass({
transparentBackground: this.values.transparent,
cameraHelper: { axes: this.values.axes },
multiSample: {
...c.props.multiSample,
mode: isPreview ? 'off' : 'on',
sampleLevel: colorBufferFloat && textureFloat ? 4 : 2,
reuseOcclusion: false,
},
postprocessing: this.getPostprocessingProps(),
marking: { ...c.props.marking },
illumination: this.getIlluminationProps(isPreview),
});
}
get previewPass() {
return this._previewPass || (this._previewPass = this.createPass(true));
}
get imagePass() {
if (this._imagePass) {
const c = this.plugin.canvas3d;
this._imagePass.setProps({
cameraHelper: { axes: this.values.axes },
transparentBackground: this.values.transparent,
postprocessing: this.getPostprocessingProps(),
marking: { ...c.props.marking },
illumination: this.getIlluminationProps(false),
});
return this._imagePass;
}
return this._imagePass = this.createPass(false);
}
getFilename(extension) {
if (typeof extension !== 'string')
extension = this.extension;
const models = this.plugin.state.data.select(mol_state_1.StateSelection.Generators.rootsOfType(objects_1.PluginStateObject.Molecule.Model)).map(s => s.obj.data);
const uniqueIds = new Set();
models.forEach(m => uniqueIds.add(m.entryId.toUpperCase()));
const idString = set_1.SetUtils.toArray(uniqueIds).join('-');
return `${idString || 'molstar-image'}${extension}`;
}
resetCrop() {
this.behaviors.relativeCrop.next({ x: 0, y: 0, width: 1, height: 1 });
}
toggleAutocrop() {
if (this.cropParams.auto) {
this.behaviors.cropParams.next({ ...this.cropParams, auto: false });
this.resetCrop();
}
else {
this.behaviors.cropParams.next({ ...this.cropParams, auto: true });
}
}
get isFullFrame() {
const crop = this.relativeCrop;
return (0, common_1.equalEps)(crop.x, 0, 1e-5) && (0, common_1.equalEps)(crop.y, 0, 1e-5) && (0, common_1.equalEps)(crop.width, 1, 1e-5) && (0, common_1.equalEps)(crop.height, 1, 1e-5);
}
autocrop(relativePadding = this.cropParams.relativePadding) {
const { data, width, height } = this.previewData.image;
const isTransparent = this.previewData.transparent;
const bgColor = isTransparent ? this.previewData.background : 0xff000000 | this.previewData.background;
let l = width, r = 0, t = height, b = 0;
for (let j = 0; j < height; j++) {
const jj = j * width;
for (let i = 0; i < width; i++) {
const o = 4 * (jj + i);
if (isTransparent) {
if (data[o + 3] === 0)
continue;
}
else {
const c = (data[o] << 16) | (data[o + 1] << 8) | (data[o + 2]) | (data[o + 3] << 24);
if (c === bgColor)
continue;
}
if (i < l)
l = i;
if (i > r)
r = i;
if (j < t)
t = j;
if (j > b)
b = j;
}
}
if (l > r) {
const x = l;
l = r;
r = x;
}
if (t > b) {
const x = t;
t = b;
b = x;
}
const tw = r - l + 1, th = b - t + 1;
l -= relativePadding * tw;
r += relativePadding * tw;
t -= relativePadding * th;
b += relativePadding * th;
const crop = {
x: Math.max(0, l / width),
y: Math.max(0, t / height),
width: Math.min(1, (r - l + 1) / width),
height: Math.min(1, (b - t + 1) / height)
};
this.behaviors.relativeCrop.next(crop);
}
async getPreview(ctx, maxDim = 320) {
const { width, height } = this.getSize();
if (width <= 0 || height <= 0)
return;
const f = width / height;
let w = 0, h = 0;
if (f > 1) {
w = maxDim;
h = Math.round(maxDim / f);
}
else {
h = maxDim;
w = Math.round(maxDim * f);
}
const canvasProps = this.plugin.canvas3d.props;
this.previewPass.setProps({
cameraHelper: { axes: this.values.axes },
transparentBackground: this.values.transparent,
postprocessing: canvasProps.postprocessing,
marking: canvasProps.marking,
});
const imageData = await this.previewPass.getImageData(ctx, w, h);
const canvas = this.previewCanvas;
canvas.width = imageData.width;
canvas.height = imageData.height;
this.previewData.image = imageData;
this.previewData.background = canvasProps.renderer.backgroundColor;
this.previewData.transparent = this.values.transparent;
const canvasCtx = canvas.getContext('2d');
if (!canvasCtx)
throw new Error('Could not create canvas 2d context');
canvasCtx.putImageData(imageData, 0, 0);
if (this.cropParams.auto)
this.autocrop();
this.events.previewed.next(void 0);
return { canvas, width: w, height: h };
}
getSizeAndViewport() {
const { width, height } = this.getSize();
const crop = this.relativeCrop;
const viewport = {
x: Math.floor(crop.x * width),
y: Math.floor(crop.y * height),
width: Math.ceil(crop.width * width),
height: Math.ceil(crop.height * height)
};
if (viewport.width + viewport.x > width)
viewport.width = width - viewport.x;
if (viewport.height + viewport.y > height)
viewport.height = height - viewport.y;
return { width, height, viewport };
}
async draw(ctx) {
var _a, _b;
const { width, height, viewport } = this.getSizeAndViewport();
if (width <= 0 || height <= 0)
return;
(_a = this.plugin.canvas3d) === null || _a === void 0 ? void 0 : _a.pause(true);
try {
await ctx.update('Rendering image...');
const pass = this.imagePass;
await pass.updateBackground();
const imageData = await pass.getImageData(ctx, width, height, viewport);
await ctx.update('Encoding image...');
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);
}
finally {
(_b = this.plugin.canvas3d) === null || _b === void 0 ? void 0 : _b.animate();
}
return;
}
copyToClipboardTask() {
const cb = navigator.clipboard;
if (!(cb === null || cb === void 0 ? void 0 : cb.write)) {
this.plugin.log.error('clipboard.write not supported!');
return;
}
return mol_task_1.Task.create('Copy Image', async (ctx) => {
await this.draw(ctx);
await ctx.update('Converting image...');
const mime = this.mimeType;
const blob = await (0, util_1.canvasToBlob)(this.canvas, mime, this.quality);
const item = new ClipboardItem({ [mime]: blob });
await cb.write([item]);
this.plugin.log.message('Image copied to clipboard.');
});
}
get mimeType() {
var _a, _b;
return `image/${(_b = (_a = this.values.format) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'png'}`;
}
get extension() {
var _a, _b, _c;
switch ((_a = this.values.format) === null || _a === void 0 ? void 0 : _a.name) {
case 'jpeg':
return '.jpg';
default:
return `.${(_c = (_b = this.values.format) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : 'png'}`;
}
}
get quality() {
var _a, _b, _c, _d;
switch ((_a = this.values.format) === null || _a === void 0 ? void 0 : _a.name) {
case 'webp':
case 'jpeg':
return (_d = (_c = (_b = this.values.format) === null || _b === void 0 ? void 0 : _b.params) === null || _c === void 0 ? void 0 : _c.quality) !== null && _d !== void 0 ? _d : 0.9;
default:
return undefined;
}
}
getImageDataUri() {
return this.plugin.runTask(mol_task_1.Task.create('Generate Image', async (ctx) => {
await this.draw(ctx);
await ctx.update('Converting image...');
return this.canvas.toDataURL(this.mimeType);
}));
}
copyToClipboard() {
const task = this.copyToClipboardTask();
if (!task)
return;
return this.plugin.runTask(task);
}
downloadTask(filename) {
return mol_task_1.Task.create('Download Image', async (ctx) => {
await this.draw(ctx);
await ctx.update('Downloading image...');
const blob = await (0, util_1.canvasToBlob)(this.canvas, this.mimeType, this.quality);
(0, download_1.download)(blob, filename !== null && filename !== void 0 ? filename : this.getFilename(this.extension));
});
}
download(filename) {
return this.plugin.runTask(this.downloadTask(filename), { useOverlay: true });
}
constructor(plugin) {
super();
this.plugin = plugin;
this._params = void 0;
this.behaviors = {
values: this.ev.behavior({
transparent: this.params.transparent.defaultValue,
format: { name: 'png', params: {} },
axes: { name: 'off', params: {} },
resolution: this.params.resolution.defaultValue,
illumination: this.params.illumination.defaultValue,
}),
cropParams: this.ev.behavior({ auto: true, relativePadding: 0.1 }),
relativeCrop: this.ev.behavior({ x: 0, y: 0, width: 1, height: 1 }),
};
this.events = {
previewed: this.ev()
};
this.canvas = function () {
const canvas = document.createElement('canvas');
return canvas;
}();
this.previewCanvas = function () {
const canvas = document.createElement('canvas');
return canvas;
}();
this.previewData = {
image: { data: new Uint8ClampedArray(1), width: 1, height: 0 },
background: (0, color_1.Color)(0),
transparent: false
};
}
}
exports.ViewportScreenshotHelper = ViewportScreenshotHelper;