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

Feature/cele 93 #50

Merged
merged 8 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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,115 @@
import { formatDate } from "../../../helpers/utils.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", { alpha: true, antialias: true, preserveDrawingBuffer: true });
}

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 Error(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
Expand Up @@ -9,8 +9,18 @@ import CustomFormControlLabel from "./CustomFormControlLabel.tsx";

const { gray500 } = vars;

function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
function SceneControls({ cameraControlRef, isWireframe, setIsWireframe, handleScreenshot, startRecording, stopRecording }) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
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 @@ -123,13 +133,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" : "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,49 @@
import * as htmlToImage from "html-to-image";
import { formatDate } from "../../../helpers/utils.ts";

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

export function downloadScreenshot(
htmlElement,
quality = 0.95,
targetResolution = { width: 3840, height: 2160 },
pixelRatio = 1,
filter = () => true,
filename = `Canvas_${formatDate(new Date())}.png`,
) {
const options = getOptions(htmlElement, targetResolution, quality, pixelRatio, filter);

htmlToImage.toBlob(htmlElement, options).then((blob) => {
const link = document.createElement("a");
link.download = filename;
link.href = window.URL.createObjectURL(blob);
link.click();
});
}

function getResolutionFixedRatio(htmlElement, target) {
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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
import DatasetPicker from "./DatasetPicker.tsx";
import Gizmo from "./Gizmo.tsx";
import Loader from "./Loader.tsx";
import { Recorder } from "./Recorder";
import STLViewer from "./STLViewer.tsx";
import SceneControls from "./SceneControls.tsx";
import { downloadScreenshot } from "./Screenshoter.ts";

export interface Instance {
id: string;
Expand All @@ -37,6 +39,8 @@ function ThreeDViewer() {

const cameraControlRef = useRef<CameraControls | null>(null);

const recorderRef = useRef<Recorder | null>(null);

// @ts-expect-error 'setShowNeurons' is declared but its value is never read.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [showNeurons, setShowNeurons] = useState<boolean>(true);
Expand Down Expand Up @@ -64,10 +68,36 @@ function ThreeDViewer() {
setInstances(newInstances);
}, [selectedDataset, workspace.availableNeurons, workspace.visibilities]);

const handleScreenshot = () => {
if (cameraControlRef.current) {
downloadScreenshot(document.getElementsByTagName("canvas")[0], 0.95, { width: 3840, height: 2160 }, 1, () => true, "screenshot.png");
}
};

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("CanvasRecording.webm", { type: "video/webm" });
recorderRef.current = null;
}
};

return (
<>
<DatasetPicker datasets={dataSets} selectedDataset={selectedDataset} onDatasetChange={setSelectedDataset} />
<Canvas style={{ backgroundColor: SCENE_BACKGROUND }} frameloop={"demand"}>
<Canvas style={{ backgroundColor: SCENE_BACKGROUND }} gl={{ alpha: true, antialias: true, preserveDrawingBuffer: true }}>
afonsobspinto marked this conversation as resolved.
Show resolved Hide resolved
<color attach="background" args={["#F6F5F4"]} />
<Suspense fallback={<Loader />}>
<PerspectiveCamera
makeDefault
Expand All @@ -87,7 +117,14 @@ function ThreeDViewer() {
<STLViewer instances={instances} isWireframe={isWireframe} />
</Suspense>
</Canvas>
<SceneControls cameraControlRef={cameraControlRef} isWireframe={isWireframe} setIsWireframe={setIsWireframe} />
<SceneControls
cameraControlRef={cameraControlRef}
isWireframe={isWireframe}
setIsWireframe={setIsWireframe}
handleScreenshot={handleScreenshot}
startRecording={startRecording}
stopRecording={stopRecording}
/>
</>
);
}
Expand Down
12 changes: 12 additions & 0 deletions applications/visualizer/frontend/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,15 @@ export function areSetsEqual<T>(setA: Set<T>, setB: Set<T>): boolean {
}
return true;
}

export function formatDate(d) {
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}-${pad(d.getHours(), 2)}${pad(d.getMinutes(), 2)}${pad(d.getSeconds(), 2)}`;
}

function pad(num, size) {
let s = num + "";
while (s.length < size) {
s = "0" + s;
}
return s;
}