I wrote a function (a bit of vibe coding too) that exports the users current canvas as an image.
Reference Images
User’s View:
Please ignore the toolbar in the user’s view image that is not a part of canvas its just an overlay.
Exported Image:
Issue:
If you see users view the object is centered while inside the exported image the object is not in the center and is bit more towards the top-right corner side.
I’ve trying to fix this for quite sometime now,
I think it’s got something to do with how the viewport is being set but I’m not an expert.
Here’s the function to export the canvas:
exportImageWithBg(bgType, resolution = '8K') {
const resolutions = {
'1K': { width: 1280, height: 720 },
'2K': { width: 2048, height: 1080 },
'4K': { width: 3840, height: 2160 },
'8K': { width: 7680, height: 4320 },
};
const { width, height } = resolutions[resolution];
const fileName = `export_${resolution}_${Date.now()}.png`;
const renderer = this.renderer;
const scene = this.scene;
const originalCamera = this.camera;
// Preserve original settings
const originalBg = scene.background;
const originalRenderTarget = renderer.getRenderTarget();
const originalViewport = renderer.getViewport(new THREE.Vector4());
// Set background
switch (bgType) {
case 'white':
scene.background = new THREE.Color(0xffffff);
break;
case 'dark':
scene.background = new THREE.Color(0x000000);
break;
case 'transparent':
scene.background = null;
renderer.setClearColor(0x000000, 0);
break;
}
// Clone and configure camera for correct aspect ratio
const exportCamera = originalCamera.clone();
exportCamera.aspect = width / height;
exportCamera.updateProjectionMatrix();
exportCamera.position.copy(originalCamera.position);
exportCamera.quaternion.copy(originalCamera.quaternion);
exportCamera.updateMatrixWorld(true);
// Render target
const renderTarget = new THREE.WebGLRenderTarget(width, height, {
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
colorSpace: THREE.SRGBColorSpace,
});
// Render offscreen
renderer.setRenderTarget(renderTarget);
console.log(renderTarget);
// renderer.setViewport(0, 0, width, height);
// renderer.setViewport(0, 0, width * 2, height * 2);
renderer.setViewport(0, 0, width, height);
renderer.render(scene, exportCamera);
// Extract pixels
const buffer = new Uint8Array(width * height * 4);
renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer);
// Copy to canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const srcIdx = (y * width + x) * 4;
const dstIdx = ((height - y - 1) * width + x) * 4;
imageData.data[dstIdx] = buffer[srcIdx];
imageData.data[dstIdx + 1] = buffer[srcIdx + 1];
imageData.data[dstIdx + 2] = buffer[srcIdx + 2];
imageData.data[dstIdx + 3] = buffer[srcIdx + 3];
}
}
ctx.putImageData(imageData, 0, 0);
// Trigger download
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = fileName;
link.click();
// Cleanup and restore
renderTarget.dispose();
scene.background = originalBg;
renderer.setRenderTarget(originalRenderTarget);
renderer.setViewport(originalViewport);
}