Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor screenshot code #54

Merged
merged 18 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions applications/visualizer/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"cytoscape-dagre": "^2.5.0",
"cytoscape-fcose": "^2.2.0",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"ol": "^9.1.0",
"pako": "^2.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { formatDate } from "../../../helpers/utils.ts";
import { GlobalError } from "../../../models/Error.ts";
export class Recorder {
private mediaRecorder: MediaRecorder | null = null;
private recordedBlobs: Blob[] = [];
private stream: MediaStream;
private ctx: WebGLRenderingContext;
// @ts-ignore
private options: { mediaRecorderOptions?: MediaRecorderOptions; blobOptions?: BlobPropertyBag } = {
mediaRecorderOptions: { mimeType: "video/webm" },
blobOptions: { type: "video/webm" },
};
private blobOptions: BlobPropertyBag = { type: "video/webm" };

constructor(canvas: HTMLCanvasElement, recorderOptions: { mediaRecorderOptions?: MediaRecorderOptions; blobOptions?: BlobPropertyBag }) {
this.stream = canvas.captureStream();
const { mediaRecorderOptions, blobOptions } = recorderOptions;
this.setupMediaRecorder(mediaRecorderOptions);
this.recordedBlobs = [];
this.blobOptions = blobOptions;
this.ctx = canvas.getContext("webgl");
}

handleDataAvailable(event) {
if (event.data && event.data.size > 0) {
this.recordedBlobs.push(event.data);
}
}

setupMediaRecorder(options) {
let error = "";

if (options == null) {
options = { mimeType: "video/webm" };
}
let mediaRecorder;
try {
mediaRecorder = new MediaRecorder(this.stream, options);
} catch (e0) {
error = `Unable to create MediaRecorder with options Object: ${e0}`;
try {
options = { mimeType: "video/webm,codecs=vp9" };
mediaRecorder = new MediaRecorder(this.stream, options);
} catch (e1) {
error = `Unable to create MediaRecorder with options Object: ${e1}`;
try {
options = { mimeType: "video/webm,codecs=vp8" }; // Chrome 47
mediaRecorder = new MediaRecorder(this.stream, options);
} catch (e2) {
error =
"MediaRecorder is not supported by this browser.\n\n" +
"Try Firefox 29 or later, or Chrome 47 or later, " +
"with Enable experimental Web Platform features enabled from chrome://flags." +
`Exception while creating MediaRecorder: ${e2}`;
}
}
}

if (!mediaRecorder) {
throw new GlobalError(error);
}

mediaRecorder.ondataavailable = (evt) => this.handleDataAvailable(evt);
mediaRecorder.onstart = () => this.animationLoop();

this.mediaRecorder = mediaRecorder;
this.options = options;
if (!this.blobOptions) {
const { mimeType } = options;
this.blobOptions = { type: mimeType };
}
}

startRecording() {
this.recordedBlobs = [];
this.mediaRecorder.start(100);
}

stopRecording(options) {
this.mediaRecorder.stop();
return this.getRecordingBlob(options);
}

download(filename, options) {
if (!filename) {
filename = `CanvasRecording_${formatDate(new Date())}.webm`;
}
const blob = this.getRecordingBlob(options);
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
return blob;
}

getRecordingBlob(options) {
if (!options) {
options = this.blobOptions;
}
return new Blob(this.recordedBlobs, options);
}

animationLoop() {
this.ctx.drawArrays(this.ctx.POINTS, 0, 0);
if (this.mediaRecorder.state !== "inactive") {
requestAnimationFrame(this.animationLoop);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
import { GetAppOutlined, HomeOutlined, PlayArrowOutlined, RadioButtonCheckedOutlined, SettingsOutlined, TonalityOutlined } from "@mui/icons-material";
import {
DarkModeOutlined,
GetAppOutlined,
HomeOutlined,
PlayArrowOutlined,
RadioButtonCheckedOutlined,
SettingsOutlined,
TonalityOutlined,
WbSunnyOutlined,
} from "@mui/icons-material";
import ZoomInIcon from "@mui/icons-material/ZoomIn";
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
import { Box, Divider, IconButton, Popover, Typography } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { useEffect, useRef, useState } from "react";
import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts";
import { DARK_SCENE_BACKGROUND, LIGHT_SCENE_BACKGROUND } from "../../../settings/threeDSettings.ts";
import { vars } from "../../../theme/variables.ts";
import CustomFormControlLabel from "./CustomFormControlLabel.tsx";
import { Recorder } from "./Recorder.ts";

import { useGlobalContext } from "../../../contexts/GlobalContext.tsx";

const { gray500 } = vars;

function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
function SceneControls({ cameraControlRef, isWireframe, setIsWireframe, recorderRef, handleScreenshot, sceneColor, setSceneColor }) {
const { isGlobalRotating } = useGlobalContext();
const workspace = useSelectedWorkspace();
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const rotateAnimationRef = useRef<number | null>(null);
const [isRotating, setIsRotating] = useState(false);
const [isRecording, setIsRecording] = useState(false);

const handleRecordClick = () => {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
setIsRecording(!isRecording);
};

const open = Boolean(anchorEl);
const id = open ? "settings-popover" : undefined;

Expand Down Expand Up @@ -60,6 +85,33 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
setIsRotating(isGlobalRotating);
}, [isGlobalRotating]);

const startRecording = () => {
if (recorderRef.current === null) {
const canvas = document.getElementsByTagName("canvas")[0];
recorderRef.current = new Recorder(canvas, {
mediaRecorderOptions: { mimeType: "video/webm" },
blobOptions: { type: "video/webm" },
});
recorderRef.current.startRecording();
}
};

const stopRecording = async () => {
if (recorderRef.current) {
recorderRef.current.stopRecording({ type: "video/webm" });
recorderRef.current.download(`${workspace.name}.webm`, { type: "video/webm" });
recorderRef.current = null;
}
};

const handleSwichMode = () => {
if (sceneColor === LIGHT_SCENE_BACKGROUND) {
setSceneColor(DARK_SCENE_BACKGROUND);
} else {
setSceneColor(LIGHT_SCENE_BACKGROUND);
}
};

return (
<Box
sx={{
Expand All @@ -69,11 +121,24 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
position: "absolute",
top: ".5rem",
left: ".5rem",
backgroundColor: "#fff",
backgroundColor: sceneColor === LIGHT_SCENE_BACKGROUND ? "white" : "#393937",
borderRadius: "0.5rem",
border: "1px solid #ECECE9",
border: `1px solid ${sceneColor === LIGHT_SCENE_BACKGROUND ? "#ECECE9" : "#393937"}`,
boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)",
padding: "0.25rem",

"& .MuiDivider-root": {
borderColor: sceneColor === LIGHT_SCENE_BACKGROUND ? "#ECECE9" : "#535350",
},

"& .MuiButtonBase-root": {
"&:hover": {
backgroundColor: sceneColor === LIGHT_SCENE_BACKGROUND ? "#F6F5F4" : "#535350",
},
"& .MuiSvgIcon-root": {
color: sceneColor === LIGHT_SCENE_BACKGROUND ? "#757570" : "#ECECE9",
},
},
}}
>
<Tooltip title="Change settings" placement="right-start">
Expand Down Expand Up @@ -126,6 +191,9 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
<TonalityOutlined />
</IconButton>
</Tooltip>
<Tooltip title={sceneColor === LIGHT_SCENE_BACKGROUND ? "Switch to dark mode" : "Switch to light mode"} placement="right-start">
<IconButton onClick={handleSwichMode}>{sceneColor === LIGHT_SCENE_BACKGROUND ? <DarkModeOutlined /> : <WbSunnyOutlined />}</IconButton>
</Tooltip>
<Divider />
<Tooltip title="Zoom in" placement="right-start">
<IconButton
Expand Down Expand Up @@ -160,13 +228,17 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
<PlayArrowOutlined />
</IconButton>
</Tooltip>
<Tooltip title="Record viewer" placement="right-start">
<IconButton>
<RadioButtonCheckedOutlined />
<Tooltip title={isRecording ? "Stop recording" : "Record viewer"} placement="right-start">
<IconButton onClick={handleRecordClick}>
<RadioButtonCheckedOutlined
sx={{
color: isRecording ? "red !important" : "inherit",
}}
/>
</IconButton>
</Tooltip>
<Tooltip title="Download graph" placement="right-start">
<IconButton>
<IconButton onClick={handleScreenshot}>
<GetAppOutlined />
</IconButton>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as THREE from "three";
import { GlobalError } from "../../../models/Error.ts";

function getResolutionFixedRatio(htmlElement: HTMLElement, target: { width: number; height: number }) {
const current = {
height: htmlElement.clientHeight,
width: htmlElement.clientWidth,
};

if ((Math.abs(target.width - current.width) * 9) / 16 > Math.abs(target.height - current.height)) {
return {
height: target.height,
width: Math.round((current.width * target.height) / current.height),
};
}
return {
height: Math.round((current.height * target.width) / current.width),
width: target.width,
};
}

function getOptions(htmlElement: HTMLCanvasElement, targetResolution: { width: number; height: number }, pixelRatio: number) {
const resolution = getResolutionFixedRatio(htmlElement, targetResolution);
return {
canvasWidth: resolution.width,
canvasHeight: resolution.height,
pixelRatio: pixelRatio,
};
}

export function downloadScreenshot(
canvasRef: React.RefObject<HTMLCanvasElement>,
sceneRef: React.RefObject<THREE.Scene>,
cameraRef: React.RefObject<THREE.PerspectiveCamera>,
filename?: string,
) {
if (!sceneRef.current || !cameraRef.current || !canvasRef.current) return;

const options = getOptions(canvasRef.current, { width: 3840, height: 2160 }, 1);

try {
const tempRenderer = new THREE.WebGLRenderer({ preserveDrawingBuffer: true });
tempRenderer.setSize(options.canvasWidth, options.canvasHeight);
tempRenderer.setPixelRatio(options.pixelRatio); // Set the resolution scaling

cameraRef.current.aspect = options.canvasWidth / options.canvasHeight;
cameraRef.current.updateProjectionMatrix();

tempRenderer.render(sceneRef.current, cameraRef.current);

tempRenderer.domElement.toBlob((blob) => {
if (blob) {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename || "screenshot.png";
link.click();
URL.revokeObjectURL(link.href);
}
}, "image/png");

tempRenderer.dispose();
} catch (e) {
throw new GlobalError(`Error saving image: ${e}`);
}
}
Loading