How can I add a screenshot button to the Three.js Editor?

I would like to modify the three.js editor to add a button to take screenshots of the scene.

Basically I want to emulate what has been implemented here, where you can see “Capture Image” option at the bottom of the “File” menu: http://editor.xtra-d.co.uk/standalone-editor/

I also found a tool named “Online 3D Viewer” live here: (https://3dviewer.net)
Source code here: GitHub - kovacsv/Online3DViewer: A solution to visualize and explore 3D models in your browser. which is based on three.js an has functionality for taking a screenshot/snapshot.

What I have managed to do so far is to overlay a button with an image on top of the three.js scene. And this button listens to click events:

threejs_screenshot_button

By googling I found out that the canvas element can be used to take a screenshot of a three.js scene. In the bellow example this works fine since the renderer object is in the same file where the function for taking the screenshot is defined:

Original question here: javascript - Three.js: How can I make a 2D SnapShot of a Scene as a JPG Image? - Stack Overflow
Full working example code here: https://codepen.io/shivasaxena/pen/QEzrrv

[code shortened]
renderer = new THREE.WebGLRenderer({
            preserveDrawingBuffer: true
        });
[code shortened]

function saveAsImage() {
        var imgData, imgNode;

        try {
            var strMime = "image/jpeg";
            imgData = renderer.domElement.toDataURL(strMime);

            saveFile(imgData.replace(strMime, strDownloadMime), "test.jpg");

        } catch (e) {
            console.log(e);
            return;
        }

    }

    var saveFile = function (strData, filename) {
        var link = document.createElement('a');
        if (typeof link.download === 'string') {
            document.body.appendChild(link); //Firefox requires the link to be in the body
            link.download = filename;
            link.href = strData;
            link.click();
            document.body.removeChild(link); //remove the link when done
        } else {
            location.replace(uri);
        }
    }

The problem I am facing is that I don’t know how to access the canvas or the renderer object from inside my file (which I named Screenshot.js) where I have the code to take the screenshot.

What I have done so far is create a Screenshot.js file:


import { UIPanel, UIButton } from './libs/ui.js';

function Screenshot( editor ) {

	const strings = editor.strings;

	const container = new UIPanel();
	container.setId( 'screenshot' );
    container.setPosition( 'absolute' );
	container.setRight( '620px' );
	container.setTop( '40px' );

	// screenshot

	const screenshotIcon = document.createElement( 'img' );
	screenshotIcon.title = strings.getKey( 'screenshot' );
	screenshotIcon.src = 'images/screenshot.svg';

	const screenshotButton = new UIButton();
	screenshotButton.dom.appendChild( screenshotIcon );
	screenshotButton.onClick( function () {

		console.log("Screenshot from Screenshot,js");

        const canvas = document.getElementById("myCanvas");

        if (canvas) {
            console.log('Debug click screenshot');
        
            canvas.toBlob(function (blob) {
                // Create a link for downloading the Blob
                const a = document.createElement('a');
                a.href = URL.createObjectURL(blob);
                a.download = 'image.png';
        
                // Programmatically trigger a click event to initiate the download
                a.click();
            }, 'image/png');
        } else {
            console.error("Canvas element not found.");
        }

    }

);

	container.add( screenshotButton );

	return container;

}

export { Screenshot };

And I modified the original index.html file to assign an id to the canvas element:

[code shortened]
<body ontouchstart="">
		<script type="module">

			import * as THREE from './js/three.module.js';
			import { APP } from './js/app.js';
			import { VRButton } from './js/VRButton.js';

			window.THREE = THREE; // Used by APP Scripts.
			window.VRButton = VRButton; // Used by APP Scripts.

			var loader = new THREE.FileLoader();
			loader.load( 'app.json', function ( text ) {

				var player = new APP.Player();
				player.load( JSON.parse( text ) );
				player.setSize( window.innerWidth, window.innerHeight );
				player.play();

                // Here I assign an id to the canvas element
                player.dom.id = "myCanvas";
                
				document.body.appendChild( player.dom );

				window.addEventListener( 'resize', function () {

					player.setSize( window.innerWidth, window.innerHeight );

				} );

			} );

			/* edit button */

		</script>
	</body>
[code shortened]

Unfortunetely the above method for assigning the “myCanvas” ID to the player.dom canvas element results in the “Canvas element not found.” message displayed to the console.

Can anyone help? Greatly appreciate any hints or insights you can provide. Thanks in advance!

I would try to implement it like so:

  • Create a new signal in Editor.js called makeScreenshot.
  • Dispatch this signal in Screenshot.js when the button is clicked.
  • Add an event handler to Viewport.js. This is the only component that has access to the editor’s renderer. The handler will the use the renderer.domElement canvas reference to make a screenshot.

But to make it clear, making screenshots like this would only work in the editor, not in exported apps.

3 Likes

Hi @Mugen87,

By following your pointers, I was able to make it work!
Thank you, thank you so much for taking the time to answer!

For others to reference, these are the changes I made:

Editor.js

        // screenshot
       makeScreenshot: new Signal(),

Screenshot.js

import { UIPanel, UIButton } from './libs/ui.js';

function Screenshot( editor ) {

	const strings = editor.strings;
	const container = new UIPanel();
	container.setId( 'screenshot' );
    container.setPosition( 'absolute' );
	container.setRight( '620px' );
	container.setTop( '40px' );

	const screenshotIcon = document.createElement( 'img' );
	screenshotIcon.title = strings.getKey( 'screenshot' );
	screenshotIcon.src = 'images/screenshot.svg';

	const screenshotButton = new UIButton();
	screenshotButton.dom.appendChild( screenshotIcon );
	screenshotButton.onClick( function () {
        editor.signals.makeScreenshot.dispatch();

    }

);

	container.add( screenshotButton );

	return container;

}

export { Screenshot };

Viewport.js

signals.makeScreenshot.add(function () {

    var screenshotDataUrl;
    try {
        var strMime = "image/jpeg";
        render();
        screenshotDataUrl = renderer.domElement.toDataURL(strMime);

        var link = document.createElement('a');
        if (typeof link.download === 'string') {
            // Firefox requires the link to be in the body
            document.body.appendChild(link);
            const filename = "screenshot" + ".jpg";
            link.download = filename;
            link.href = screenshotDataUrl.replace(strMime, "image/octet-stream");
            link.click();
            document.body.removeChild(link);
        } else {
            location.replace(uri);
        }
    } catch (e) {
        console.log(e);
        return;
    }
});

index.html

import { Screenshot } from './js/Screenshot.js';

// code shortened
 const screenshot = new Screenshot( editor );
 document.body.appendChild( screenshot.dom );

Also added an entry to Strings.js, main.css and the screenshot.svg file.

3 Likes