Skip to content

Commit

Permalink
Merge pull request #50 from MetaCell/feature/CELE-93
Browse files Browse the repository at this point in the history
Feature/cele 93
  • Loading branch information
ddelpiano authored Oct 2, 2024
2 parents b3607c5 + 81138da commit 550ddd8
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 7 deletions.
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");
}

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 @@ -6,12 +6,26 @@ import Tooltip from "@mui/material/Tooltip";
import { useRef, useState } from "react";
import { vars } from "../../../theme/variables.ts";
import CustomFormControlLabel from "./CustomFormControlLabel.tsx";
import { Recorder } from "./Recorder.ts";
import { downloadScreenshot } from "./Screenshoter.ts";

const { gray500 } = vars;

function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
function SceneControls({ cameraControlRef, isWireframe, setIsWireframe, recorderRef }) {
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 @@ -43,6 +57,31 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
setIsRotating(!isRotating);
};

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 (
<Box
sx={{
Expand Down Expand Up @@ -143,13 +182,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,6 +17,7 @@ import {
import DatasetPicker from "./DatasetPicker.tsx";
import Gizmo from "./Gizmo.tsx";
import Loader from "./Loader.tsx";
import type { Recorder } from "./Recorder";
import STLViewer from "./STLViewer.tsx";
import SceneControls from "./SceneControls.tsx";

Expand All @@ -37,6 +38,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 @@ -67,7 +70,8 @@ function ThreeDViewer() {
return (
<>
<DatasetPicker datasets={dataSets} selectedDataset={selectedDataset} onDatasetChange={setSelectedDataset} />
<Canvas style={{ backgroundColor: SCENE_BACKGROUND }} frameloop={"demand"}>
<Canvas style={{ backgroundColor: SCENE_BACKGROUND }} frameloop={"demand"} gl={{ preserveDrawingBuffer: true }}>
<color attach="background" args={["#F6F5F4"]} />
<Suspense fallback={<Loader />}>
<PerspectiveCamera
makeDefault
Expand All @@ -87,7 +91,7 @@ function ThreeDViewer() {
<STLViewer instances={instances} isWireframe={isWireframe} />
</Suspense>
</Canvas>
<SceneControls cameraControlRef={cameraControlRef} isWireframe={isWireframe} setIsWireframe={setIsWireframe} />
<SceneControls cameraControlRef={cameraControlRef} isWireframe={isWireframe} setIsWireframe={setIsWireframe} recorderRef={recorderRef} />
</>
);
}
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;
}

0 comments on commit 550ddd8

Please sign in to comment.