Skip to content

Commit

Permalink
Merge pull request Kitware#1907 from Kitware/resizable-screenshots
Browse files Browse the repository at this point in the history
fix(RenderWindow): Support custom sizes for screenshots
  • Loading branch information
jourdain authored May 21, 2021
2 parents 10e4e6b + ab45a23 commit 12f97e6
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 11 deletions.
3 changes: 2 additions & 1 deletion Sources/Proxy/Core/ViewProxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ function vtkViewProxy(publicAPI, model) {

// --------------------------------------------------------------------------

publicAPI.captureImage = () => model.renderWindow.captureImages()[0];
publicAPI.captureImage = ({ format = 'image/png', ...opts } = {}) =>
model.renderWindow.captureImages(format, opts)[0];

// --------------------------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions Sources/Rendering/Core/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ function vtkRenderWindow(publicAPI, model) {
return results;
};

publicAPI.captureImages = (format = 'image/png') => {
publicAPI.captureImages = (format = 'image/png', opts = {}) => {
macro.setImmediate(publicAPI.render);
return model.views
.map((view) =>
view.captureNextImage ? view.captureNextImage(format) : undefined
view.captureNextImage ? view.captureNextImage(format, opts) : undefined
)
.filter((i) => !!i);
};
Expand Down
13 changes: 13 additions & 0 deletions Sources/Rendering/OpenGL/RenderWindow/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,16 @@ Initialize the rendering window. This will setup all system-specific
resources. This method and Finalize() must be symmetric and it
should be possible to call them multiple times, even changing WindowId
in-between. This is what WindowRemap does.

### captureNextImage(format, options);

Capture a screenshot of the contents of this renderwindow. The options object
can include a `size` array (`[w, h]`) or a `scale` floating point value, as well
as a `resetCamera` boolean. If `size` is provided, the captured screenshot will
be of the given size (and `resetCamera` could be useful in this case if the
aspect ratio of `size` does not match the current renderwindow size). Otherwise,
if `scale` is provided, it will be multiplied by the current renderwindow size
to compute the screenshot size. If no `size` or `scale` are provided, the
current renderwindow size is assumed. The default format is "image/png".

Returns a promise that resolves to the captured screenshot.
98 changes: 94 additions & 4 deletions Sources/Rendering/OpenGL/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants';

const { vtkDebugMacro, vtkErrorMacro } = macro;
const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
const SCREENSHOT_PLACEHOLDER = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};

function checkRenderTargetSupport(gl, format, type) {
// create temporary frame buffer and texture
Expand Down Expand Up @@ -522,19 +529,102 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
publicAPI.invokeImageReady(screenshot);
}

publicAPI.captureNextImage = (format = 'image/png') => {
publicAPI.captureNextImage = (
format = 'image/png',
{ resetCamera = false, size = null, scale = 1 } = {}
) => {
if (model.deleted) {
return null;
}
model.imageFormat = format;
const previous = model.notifyStartCaptureImage;
model.notifyStartCaptureImage = true;

model._screenshot = {
size:
!!size || scale !== 1
? size || model.size.map((val) => val * scale)
: null,
};

return new Promise((resolve, reject) => {
const subscription = publicAPI.onImageReady((imageURL) => {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
resolve(imageURL);
if (model._screenshot.size === null) {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
if (model._screenshot.placeHolder) {
// resize the main canvas back to its original size and show it
model.size = model._screenshot.originalSize;

// process the resize
publicAPI.modified();

// restore the saved camera parameters, if applicable
if (model._screenshot.cameras) {
model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) =>
restoreParamsFn(arg)
);
}

// Trigger a render at the original size
publicAPI.traverseAllPasses();

// Remove and clean up the placeholder, revealing the original
model.el.removeChild(model._screenshot.placeHolder);
model._screenshot.placeHolder.remove();
model._screenshot = null;
}
resolve(imageURL);
} else {
// Create a placeholder image overlay while we resize and render
const tmpImg = document.createElement('img');
tmpImg.style = SCREENSHOT_PLACEHOLDER;
tmpImg.src = imageURL;
model._screenshot.placeHolder = model.el.appendChild(tmpImg);

// hide the main canvas
model.canvas.style.display = 'none';

// remember the main canvas original size, then resize it
model._screenshot.originalSize = model.size;
model.size = model._screenshot.size;
model._screenshot.size = null;

// process the resize
publicAPI.modified();

if (resetCamera) {
// If resetCamera was requested, we first save camera parameters
// from all the renderers, so we can restore them later
model._screenshot.cameras = model.renderable
.getRenderers()
.map((renderer) => {
const camera = renderer.getActiveCamera();
const params = camera.get(
'focalPoint',
'position',
'parallelScale'
);

return {
resetCameraFn: renderer.resetCamera,
restoreParamsFn: camera.set,
// "clone" the params so we don't keep refs to properties
arg: JSON.parse(JSON.stringify(params)),
};
});

// Perform the resetCamera() on each renderer only after capturing
// the params from all active cameras, in case there happen to be
// linked cameras among the renderers.
model._screenshot.cameras.forEach(({ resetCameraFn }) =>
resetCameraFn()
);
}

// Trigger a render at the custom size
publicAPI.traverseAllPasses();
}
});
});
};
Expand Down
98 changes: 94 additions & 4 deletions Sources/Rendering/WebGPU/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderW

const { vtkErrorMacro } = macro;
// const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
const SCREENSHOT_PLACEHOLDER = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};

// ----------------------------------------------------------------------------
// vtkWebGPURenderWindow methods
Expand Down Expand Up @@ -244,19 +251,102 @@ function vtkWebGPURenderWindow(publicAPI, model) {
publicAPI.invokeImageReady(screenshot);
}

publicAPI.captureNextImage = (format = 'image/png') => {
publicAPI.captureNextImage = (
format = 'image/png',
{ resetCamera = false, size = null, scale = 1 } = {}
) => {
if (model.deleted) {
return null;
}
model.imageFormat = format;
const previous = model.notifyStartCaptureImage;
model.notifyStartCaptureImage = true;

model._screenshot = {
size:
!!size || scale !== 1
? size || model.size.map((val) => val * scale)
: null,
};

return new Promise((resolve, reject) => {
const subscription = publicAPI.onImageReady((imageURL) => {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
resolve(imageURL);
if (model._screenshot.size === null) {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
if (model._screenshot.placeHolder) {
// resize the main canvas back to its original size and show it
model.size = model._screenshot.originalSize;

// process the resize
publicAPI.modified();

// restore the saved camera parameters, if applicable
if (model._screenshot.cameras) {
model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) =>
restoreParamsFn(arg)
);
}

// Trigger a render at the original size
publicAPI.traverseAllPasses();

// Remove and clean up the placeholder, revealing the original
model.el.removeChild(model._screenshot.placeHolder);
model._screenshot.placeHolder.remove();
model._screenshot = null;
}
resolve(imageURL);
} else {
// Create a placeholder image overlay while we resize and render
const tmpImg = document.createElement('img');
tmpImg.style = SCREENSHOT_PLACEHOLDER;
tmpImg.src = imageURL;
model._screenshot.placeHolder = model.el.appendChild(tmpImg);

// hide the main canvas
model.canvas.style.display = 'none';

// remember the main canvas original size, then resize it
model._screenshot.originalSize = model.size;
model.size = model._screenshot.size;
model._screenshot.size = null;

// process the resize
publicAPI.modified();

if (resetCamera) {
// If resetCamera was requested, we first save camera parameters
// from all the renderers, so we can restore them later
model._screenshot.cameras = model.renderable
.getRenderers()
.map((renderer) => {
const camera = renderer.getActiveCamera();
const params = camera.get(
'focalPoint',
'position',
'parallelScale'
);

return {
resetCameraFn: renderer.resetCamera,
restoreParamsFn: camera.set,
// "clone" the params so we don't keep refs to properties
arg: JSON.parse(JSON.stringify(params)),
};
});

// Perform the resetCamera() on each renderer only after capturing
// the params from all active cameras, in case there happen to be
// linked cameras among the renderers.
model._screenshot.cameras.forEach(({ resetCameraFn }) =>
resetCameraFn()
);
}

// Trigger a render at the custom size
publicAPI.traverseAllPasses();
}
});
});
};
Expand Down

0 comments on commit 12f97e6

Please sign in to comment.