Skip to content

Commit adca684

Browse files
Merge pull request #154 from Exabyte-io/feature/SOF-7438
Feature/SOF-7438 GIF recording
2 parents 05f6b26 + 5f302a7 commit adca684

23 files changed

+344
-719
lines changed

dist/components/ThreeDEditor.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export class ThreeDEditor extends React.Component<any, any, any> {
5959
onMeasurementParam(param: any, offParam: any): void;
6060
addHotKeyListener(): void;
6161
removeHotKeyListener(): void;
62+
handleStartGifRecording(downloadPath: any, rotationSpeed?: number, frameDuration?: number): Promise<void>;
63+
doWaveFunc(funcStr: any): void;
6264
componentDidMount(): void;
6365
componentWillUnmount(): void;
6466
UNSAFE_componentWillReceiveProps(nextProps: any, nextContext: any): void;

dist/components/ThreeDEditor.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
33
// import "../MuiClassNameSetup";
44
import { DarkMaterialUITheme } from "@exabyte-io/cove.js/dist/theme";
55
import ThemeProvider from "@exabyte-io/cove.js/dist/theme/provider";
6+
import { exportToDisk } from "@exabyte-io/cove.js/dist/utils/downloader";
7+
import { AlertProvider } from "@exabyte-io/cove.js/src/theme/provider";
68
import { Made } from "@mat3ra/made";
79
import Article from "@mui/icons-material/Article";
810
import Autorenew from "@mui/icons-material/Autorenew";
@@ -30,7 +32,6 @@ import $ from "jquery";
3032
import PropTypes from "prop-types";
3133
import React from "react";
3234
import settings from "../settings";
33-
import { exportToDisk } from "../utils";
3435
import IconsToolbar from "./IconsToolbar";
3536
import ParametersMenu from "./ParametersMenu";
3637
import { ThreejsEditorModal } from "./ThreejsEditorModal";
@@ -234,6 +235,13 @@ export class ThreeDEditor extends React.Component {
234235
},
235236
];
236237
return [
238+
{
239+
id: "StartGif",
240+
title: "Auto Rotate GIF",
241+
content: "Auto Rotate GIF",
242+
leftIcon: _jsx(PictureInPicture, {}),
243+
onClick: () => this.handleStartGifRecording(),
244+
},
237245
{
238246
id: "Screenshot",
239247
title: "Screenshot",
@@ -318,6 +326,8 @@ export class ThreeDEditor extends React.Component {
318326
this.onMeasurementParam = this.onMeasurementParam.bind(this);
319327
this.addHotKeyListener = this.addHotKeyListener.bind(this);
320328
this.removeHotKeyListener = this.removeHotKeyListener.bind(this);
329+
this.handleStartGifRecording = this.handleStartGifRecording.bind(this);
330+
this.doWaveFunc = this.doWaveFunc.bind(this);
321331
}
322332
componentDidMount() {
323333
this.addHotKeyListener();
@@ -587,6 +597,14 @@ export class ThreeDEditor extends React.Component {
587597
}
588598
return toolbarConfig;
589599
}
600+
async handleStartGifRecording(downloadPath, rotationSpeed = 60, frameDuration = 0.05) {
601+
await this.WaveComponent.wave.takeGifScreenshot({
602+
downloadPath,
603+
rotationSpeed,
604+
frameDuration,
605+
});
606+
console.log("Recorded gif");
607+
}
590608
onThreejsEditorModalHide(material) {
591609
let { isThreejsEditorModalShown } = this.state;
592610
isThreejsEditorModalShown = !isThreejsEditorModalShown;
@@ -619,14 +637,29 @@ export class ThreeDEditor extends React.Component {
619637
return (_jsxs("div", { className: "wave-component-holder", style: { position: "relative", height: "100%" }, children: [this.renderCoverDiv(), _jsx(IconsToolbar, { toolbarConfig: this.getToolbarConfig(), isInteractive: isInteractive, handleToggleInteractive: this.handleToggleInteractive }), this.renderWaveComponent()] }));
620638
}
621639
render() {
622-
return (_jsx(ThemeProvider, { theme: DarkMaterialUITheme, children: _jsx(ScopedCssBaseline, { enableColorScheme: true, style: { height: "100%" }, children: this.renderWaveOrThreejsEditorModal() }) }));
640+
return (_jsx(ThemeProvider, { theme: DarkMaterialUITheme, children: _jsx(ScopedCssBaseline, { enableColorScheme: true, style: { height: "100%" }, children: _jsx(AlertProvider, { children: this.renderWaveOrThreejsEditorModal() }) }) }));
641+
}
642+
doWaveFunc(funcStr) {
643+
if (!this.WaveComponent || !this.WaveComponent.wave) {
644+
console.error("Wave component not initialized");
645+
return;
646+
}
647+
const { wave } = this.WaveComponent;
648+
try {
649+
// eslint-disable-next-line no-new-func
650+
const func = new Function("wave", `return wave.${funcStr}`);
651+
func(wave);
652+
this.WaveComponent.wave.rebuildScene();
653+
}
654+
catch (error) {
655+
console.error("Error executing wave function:", error);
656+
}
623657
}
624658
}
625659
ThreeDEditor.propTypes = {
626660
material: PropTypes.instanceOf(Made.Material).isRequired,
627661
editable: PropTypes.bool,
628-
isConventionalCellShown: PropTypes.bool,
629-
// eslint-disable-next-line react/forbid-prop-types
662+
isConventionalCellShown: PropTypes.bool, // eslint-disable-next-line react/forbid-prop-types
630663
boundaryConditions: PropTypes.object,
631664
onUpdate: PropTypes.func,
632665
};

dist/mixins/controls.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const OrbitControlsMixin = (superclass) => class extends superclass {
1919
this.disableOrbitControlsAnimation = this.disableOrbitControlsAnimation.bind(this);
2020
this.initSecondAxes = this.initSecondAxes.bind(this);
2121
this.updateSecondAxes = this.updateSecondAxes.bind(this);
22+
// Bind methods to context to avoid losing `this` reference in requestAnimationFrame
23+
this.performOrbitControlsAnimation = this.performOrbitControlsAnimation.bind(this);
2224
}
2325
initOrbitControls(enabled = false) {
2426
this.initSecondAxes();
@@ -65,14 +67,22 @@ const OrbitControlsMixin = (superclass) => class extends superclass {
6567
if (!this.orbitControls)
6668
return;
6769
this.orbitControls.autoRotate = true;
68-
this.animationFrameId = window.requestAnimationFrame(this.enableOrbitControlsAnimation);
70+
this.performOrbitControlsAnimation();
71+
}
72+
// eslint-disable-next-line class-methods-use-this
73+
performOrbitControlsAnimation(action = () => { }) {
74+
this.animationFrameId = window.requestAnimationFrame(this.performOrbitControlsAnimation);
6975
// required if controls.enableDamping or controls.autoRotate are set to true
7076
this.orbitControls.update();
7177
this.render();
78+
if (typeof action === "function") {
79+
action();
80+
}
7281
}
7382
disableOrbitControlsAnimation() {
7483
if (!this.orbitControls)
7584
return;
85+
this.orbitControls.autoRotate = false;
7686
window.cancelAnimationFrame(this.animationFrameId);
7787
this.animationFrameId = null;
7888
}

dist/mixins/image.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function ImageMixin(superclass: any): {
2+
new (): {
3+
[x: string]: any;
4+
takeScreenshot(): void;
5+
getScreenshotImage(): any;
6+
updateScene(): Promise<any>;
7+
createRotatingGifData(options?: {}): Promise<any>;
8+
takeGifScreenshot(options?: {}): Promise<void>;
9+
};
10+
[x: string]: any;
11+
};

dist/mixins/image.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { showInfoAlert, showSuccessAlert, showWarningAlert, } from "@exabyte-io/cove.js/dist/other/alerts";
2+
import { saveImageDataToFile } from "@exabyte-io/cove.js/dist/utils/downloader";
3+
import { createGIFAsync } from "./utils";
4+
export const ImageMixin = (superclass) => class extends superclass {
5+
takeScreenshot() {
6+
saveImageDataToFile(this.getScreenshotImage());
7+
}
8+
getScreenshotImage() {
9+
const canvas = this.renderer.domElement;
10+
canvas.getContext("2d", { willReadFrequently: true });
11+
return canvas.toDataURL("image/png");
12+
}
13+
async updateScene() {
14+
return new Promise((resolve) => {
15+
const checkRender = () => {
16+
this.renderer.render(this.scene, this.camera); // Ensure scene updates
17+
requestAnimationFrame(() => resolve()); // Wait for the next frame
18+
};
19+
checkRender();
20+
});
21+
}
22+
async createRotatingGifData(options = {}) {
23+
const sampleInterval = options.sampleInterval || 20; // Parts of image in pixels
24+
const totalGifDuration = options.totalDuration || 3; // Seconds
25+
const animationDuration = options.animationDuration || 1; // Seconds
26+
const totalFrames = options.totalFrames || 60; // Number of frames in GIF
27+
const autoRotateSpeed = 60 / animationDuration; // RPM
28+
const frameDuration = totalGifDuration / totalFrames;
29+
const canvas = this.renderer.domElement;
30+
canvas.willReadFrequently = true;
31+
const { width, height } = canvas;
32+
if (this.orbitControls.autoRotate) {
33+
showWarningAlert("Please disable auto-rotation before creating a GIF.");
34+
return null;
35+
}
36+
// Store original auto-rotate settings
37+
const originalSpeed = this.orbitControls.autoRotateSpeed;
38+
this.orbitControls.autoRotateSpeed = autoRotateSpeed;
39+
this.orbitControls.autoRotate = true;
40+
const frames = [];
41+
for (let i = 0; i < totalFrames; i += 1) {
42+
this.orbitControls.update(); // Move scene to new position
43+
// eslint-disable-next-line no-await-in-loop
44+
await this.updateScene(); // Wait for rendering to finish
45+
frames.push(this.getScreenshotImage()); // Capture screenshot
46+
}
47+
showInfoAlert("GIF is being created. Please wait...");
48+
const gifData = await createGIFAsync({
49+
images: frames,
50+
gifWidth: width,
51+
gifHeight: height,
52+
sampleInterval,
53+
frameDuration,
54+
});
55+
// Restore original rotation settings
56+
this.orbitControls.autoRotateSpeed = originalSpeed;
57+
this.orbitControls.autoRotate = false;
58+
canvas.willReadFrequently = false;
59+
return gifData;
60+
}
61+
async takeGifScreenshot(options = {}) {
62+
const gifDataUrl = await this.createRotatingGifData(options);
63+
if (!gifDataUrl)
64+
return;
65+
const fileName = (this._structure.name || this._structure.formula || "wave-visualization") + ".gif";
66+
showSuccessAlert("GIF is created. Proceeding to download.");
67+
saveImageDataToFile(gifDataUrl, fileName);
68+
}
69+
};

dist/mixins/labels.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
66
import * as THREE from "three";
77
import { ATOM_GROUP_NAME, LABELS_GROUP_NAME } from "../enums";
88
// eslint-disable-next-line import/no-cycle
9-
import { setParameters } from "../utils";
109
/*
1110
* Mixin containing the logic for dealing with atom labels.
1211
* Dynamically draws labels over atoms.
@@ -35,9 +34,9 @@ export const LabelsMixin = (superclass) => { var _texturesCache, _a; return _a =
3534
const basicTextSize = textWidth > fontSize ? textWidth : fontSize;
3635
const textSizePowOf2 = 2 ** Math.floor(Math.log2(basicTextSize));
3736
const scaledFontSize = (fontSize * textSizePowOf2) / basicTextSize;
38-
setParameters(canvas, { width: textSizePowOf2, height: textSizePowOf2 });
37+
Object.assign(canvas, { width: textSizePowOf2, height: textSizePowOf2 });
3938
const scaledFont = `${fontWeight} ${scaledFontSize}px ${fontFace}`;
40-
setParameters(context, { font: scaledFont, ...textParams });
39+
Object.assign(context, { font: scaledFont, ...textParams });
4140
context.fillText(text, context.canvas.width / 2, (context.canvas.height / 2) * 1.15);
4241
context.strokeText(text, context.canvas.width / 2, (context.canvas.height / 2) * 1.15);
4342
const texture = new THREE.Texture(canvas);

dist/mixins/utils.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
export function createGIFAsync({ images, gifWidth, gifHeight, numFrames, frameDuration, sampleInterval, }: {
2+
images: any;
3+
gifWidth: any;
4+
gifHeight: any;
5+
numFrames: any;
6+
frameDuration: any;
7+
sampleInterval: any;
8+
}): Promise<any>;
19
export function UtilsMixin(superclass: any): {
210
new (): {
311
[x: string]: any;

dist/mixins/utils.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import gifshot from "gifshot";
12
import * as THREE from "three";
23
export const UtilsMixin = (superclass) => class extends superclass {
34
// toggles a boolean variable and optionally sets all variables in the antagonists array to the opposite value
@@ -58,3 +59,15 @@ export const ApplyGlow = (meshObjet, baseColor, offset = 0) => {
5859
meshObjet.material.emissive.setHSL(hue, saturation, atomHSL.l);
5960
}
6061
};
62+
export function createGIFAsync({ images, gifWidth, gifHeight, numFrames, frameDuration, sampleInterval, }) {
63+
return new Promise((resolve, reject) => {
64+
gifshot.createGIF({ images, gifWidth, gifHeight, numFrames, frameDuration, sampleInterval }, (obj) => {
65+
if (!obj.error) {
66+
resolve(obj.image); // Resolve with the GIF data URL
67+
}
68+
else {
69+
reject(obj.error); // Reject with the error
70+
}
71+
});
72+
});
73+
}

dist/react-app-env.d.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)