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/SOF-7438 GIF recording #154

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
67 changes: 67 additions & 0 deletions dist/components/ThreeDEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ class ThreeDEditor extends _react.default.Component {
onClick: () => this.handleDownloadClick("poscar")
}];
return [{
id: "StartGif",
title: "Auto Rotate GIF",
content: "Auto Rotate GIF",
leftIcon: /*#__PURE__*/_react.default.createElement(_PictureInPicture.default, null),
onClick: this.handleStartGifRecording
}, {
id: "Screenshot",
title: "Screenshot",
content: "Screenshot",
Expand All @@ -262,6 +268,39 @@ class ThreeDEditor extends _react.default.Component {
handleChemicalConnectivityFactorChange: this.handleChemicalConnectivityFactorChange
});
});
_defineProperty(this, "handleStartGifRecording", (downloadPath, rotationSpeed = 60, frameDuration = 0.05) => {
this.WaveComponent.wave.takeGifScreenshot({
rotationSpeed,
frameDuration,
downloadPath
}).then(result => {
console.log("Recorded gif", result);
});
});
_defineProperty(this, "handleMessage", event => {
if (event.data && event.data.material) {
try {
const newMaterial = new _made.Made.Material(event.data.material);
this.setState({
originalMaterial: newMaterial,
material: newMaterial.clone()
}, () => {
// Force Wave component to update after state change
if (this.WaveComponent) {
this.WaveComponent.wave.rebuildScene();
}
});
} catch (error) {
alert("Error creating material: " + error.message);
}
} else if (event.data && event.data.action && this[event.data.action]) {
const {
action,
parameters
} = event.data;
this[action](...parameters);
}
});
const {
boundaryConditions,
isConventionalCellShown: _isConventionalCellShown,
Expand Down Expand Up @@ -327,14 +366,20 @@ class ThreeDEditor extends _react.default.Component {
this.onMeasurementParam = this.onMeasurementParam.bind(this);
this.addHotKeyListener = this.addHotKeyListener.bind(this);
this.removeHotKeyListener = this.removeHotKeyListener.bind(this);
this.handleStartGifRecording = this.handleStartGifRecording.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.doWaveFunc = this.doWaveFunc.bind(this);
this.handleSetCameraToFitCell = this.handleSetCameraToFitCell.bind(this);
}
componentDidMount() {
this.addHotKeyListener();
window.addEventListener("message", this.handleMessage);
}
componentWillUnmount() {
this.handleResetMeasurements();
this.WaveComponent.wave.destroyListeners();
this.removeHotKeyListener();
window.removeEventListener("message", this.handleMessage);
}

// TODO: update component to fully controlled or fully uncontrolled with a key?
Expand Down Expand Up @@ -746,6 +791,28 @@ class ThreeDEditor extends _react.default.Component {
enableColorScheme: true
}, this.renderWaveOrThreejsEditorModal()));
}
doWaveFunc(funcStr) {
if (!this.WaveComponent || !this.WaveComponent.wave) {
console.error("Wave component not initialized");
return;
}
const {
wave
} = this.WaveComponent;
try {
// eslint-disable-next-line no-new-func
const func = new Function("wave", `return wave.${funcStr}`);
func(wave);
wave.render();
} catch (error) {
alert("Error executing wave function: " + error.message);
console.error("Error executing wave function:", error);
}
}
handleSetCameraToFitCell() {
this.WaveComponent.wave.adjustCamerasAndOrbitControlsToCell();
this.WaveComponent.wave.rebuildScene();
}
}
exports.ThreeDEditor = ThreeDEditor;
ThreeDEditor.propTypes = {
Expand Down
1 change: 1 addition & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var _ThreeDEditor = require("./components/ThreeDEditor");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const domElement = document.getElementById("root");
const material = new _made.Made.Material(_made.Made.defaultMaterialConfig);

// eslint-disable-next-line react/no-render-return-value
window.threeDEditor = _reactDom.default.render(/*#__PURE__*/_react.default.createElement(_ThreeDEditor.ThreeDEditor, {
editable: true,
Expand Down
66 changes: 65 additions & 1 deletion dist/mixins/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ Object.defineProperty(exports, "__esModule", {
value: true
});
exports.UtilsMixin = exports.ApplyGlow = void 0;
exports.createRotatingGif = createRotatingGif;
var _gifshot = _interopRequireDefault(require("gifshot"));
var THREE = _interopRequireWildcard(require("three"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const UtilsMixin = superclass => class extends superclass {
// toggles a boolean variable and optionally sets all variables in the antagonists array to the opposite value
toggleBoolean(name, antagonistNames = []) {
Expand Down Expand Up @@ -68,4 +71,65 @@ const ApplyGlow = (meshObjet, baseColor, offset = 0) => {
meshObjet.material.emissive.setHSL(hue, saturation, atomHSL.l);
}
};
exports.ApplyGlow = ApplyGlow;
exports.ApplyGlow = ApplyGlow;
function createRotatingGif(wave, options = {}) {
const ROTATION_SPEED = options.rotationSpeed || 60; // RPM
const frameDuration = options.frameDuration || 0.05; // seconds
const sampleInterval = ROTATION_SPEED * frameDuration;
const canvas = wave.renderer.domElement;
const frames = [];
let frameCount = 0;
const totalFrames = ROTATION_SPEED;
const {
width
} = canvas;
const {
height
} = canvas;

// Store original auto-rotate settings
const wasAutoRotating = wave.orbitControls.autoRotate;
const originalSpeed = wave.orbitControls.autoRotateSpeed;

// Enable rotation
wave.orbitControls.autoRotate = true;
wave.orbitControls.autoRotateSpeed = ROTATION_SPEED;
return new Promise((resolve, reject) => {
const captureFrame = () => {
wave.render();
frames.push(canvas.toDataURL("image/png"));
};
const createGif = () => {
console.log("Creating GIF from frames...");
// Restore original rotation settings
wave.orbitControls.autoRotateSpeed = originalSpeed;
wave.orbitControls.autoRotate = wasAutoRotating;
_gifshot.default.createGIF({
images: frames,
gifWidth: width,
gifHeight: height,
numFrames: totalFrames,
frameDuration,
sampleInterval
}, result => {
frames.length = 0; // Clear frames array
if (!result.error) {
resolve(result.image);
} else {
reject(new Error(result.error));
}
});
};
const animate = () => {
if (frameCount < totalFrames) {
wave.orbitControls.update();
captureFrame();
frameCount += 1;
requestAnimationFrame(animate);
} else {
createGif();
}
};
animate();
});
}
28 changes: 26 additions & 2 deletions dist/wave.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ var _controls = require("./mixins/controls");
var _labels = require("./mixins/labels");
var _measurement = require("./mixins/measurement");
var _repetition = require("./mixins/repetition");
var _utils = require("./mixins/utils");
var _settings = _interopRequireDefault(require("./settings"));
var _utils = require("./utils");
var _utils2 = require("./utils");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
Expand Down Expand Up @@ -67,6 +68,7 @@ class WaveBase {
this.setupLights();
this.handleResize = this.handleResize.bind(this);
this.setBackground = this.setBackground.bind(this);
this.doFunc = this.doFunc.bind(this);
}
updateSettings(settings) {
this.settings = {
Expand Down Expand Up @@ -243,7 +245,7 @@ class Wave extends (0, _mixwith.mix)(WaveBase).with(_atoms.AtomsMixin, _bonds.Bo
this.doFunc = this.doFunc.bind(this);
}
takeScreenshot() {
(0, _utils.saveImageDataToFile)(this.renderer.domElement.toDataURL("image/png"));
(0, _utils2.saveImageDataToFile)(this.renderer.domElement.toDataURL("image/png"));
}
clearView() {
while (this.structureGroup.children.length) {
Expand Down Expand Up @@ -310,5 +312,27 @@ class Wave extends (0, _mixwith.mix)(WaveBase).with(_atoms.AtomsMixin, _bonds.Bo
doFunc(func) {
func(this);
} // for scripting

async takeGifScreenshot(options = {}) {
try {
const gifDataUrl = await (0, _utils.createRotatingGif)(this, options);

// Use custom filename from options or fall back to default
const fileName = options.downloadPath ? options.downloadPath.split("/").pop() // Extract filename from path
: (this._structure.name || this._structure.formula || "wave-visualization") + ".gif";

// Download the GIF
const a = document.createElement("a");
a.href = gifDataUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
return gifDataUrl;
} catch (error) {
console.error("Error creating GIF:", error);
throw error;
}
}
}
exports.Wave = Wave;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@mui/material": "^5.11.9",
"@mui/styles": "^5.11.7",
"classnames": "^2.3.1",
"gifshot": "^0.4.5",
"jquery": "3.6.0",
"mixwith": "^0.1.1",
"moment": "^2.29.4",
Expand Down
76 changes: 74 additions & 2 deletions src/components/ThreeDEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,22 @@ export class ThreeDEditor extends React.Component {
this.onMeasurementParam = this.onMeasurementParam.bind(this);
this.addHotKeyListener = this.addHotKeyListener.bind(this);
this.removeHotKeyListener = this.removeHotKeyListener.bind(this);
this.handleStartGifRecording = this.handleStartGifRecording.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.doWaveFunc = this.doWaveFunc.bind(this);
this.handleSetCameraToFitCell = this.handleSetCameraToFitCell.bind(this);
}

componentDidMount() {
this.addHotKeyListener();
window.addEventListener("message", this.handleMessage);
}

componentWillUnmount() {
this.handleResetMeasurements();
this.WaveComponent.wave.destroyListeners();
this.removeHotKeyListener();
window.removeEventListener("message", this.handleMessage);
}

// TODO: update component to fully controlled or fully uncontrolled with a key?
Expand Down Expand Up @@ -596,6 +602,13 @@ export class ThreeDEditor extends React.Component {
},
];
return [
{
id: "StartGif",
title: "Auto Rotate GIF",
content: "Auto Rotate GIF",
leftIcon: <PictureInPicture />,
onClick: this.handleStartGifRecording,
},
{
id: "Screenshot",
title: "Screenshot",
Expand Down Expand Up @@ -675,6 +688,18 @@ export class ThreeDEditor extends React.Component {
return toolbarConfig;
}

handleStartGifRecording = (downloadPath, rotationSpeed = 60, frameDuration = 0.05) => {
this.WaveComponent.wave
.takeGifScreenshot({
rotationSpeed,
frameDuration,
downloadPath,
})
.then((result) => {
console.log("Recorded gif", result);
});
};

onThreejsEditorModalHide(material) {
let { isThreejsEditorModalShown } = this.state;
isThreejsEditorModalShown = !isThreejsEditorModalShown;
Expand Down Expand Up @@ -734,13 +759,60 @@ export class ThreeDEditor extends React.Component {
</ThemeProvider>
);
}

handleMessage = (event) => {
if (event.data && event.data.material) {
try {
const newMaterial = new Made.Material(event.data.material);
this.setState(
{
originalMaterial: newMaterial,
material: newMaterial.clone(),
},
() => {
// Force Wave component to update after state change
if (this.WaveComponent) {
this.WaveComponent.wave.rebuildScene();
}
},
);
} catch (error) {
alert("Error creating material: " + error.message);
}
} else if (event.data && event.data.action && this[event.data.action]) {
const { action, parameters } = event.data;
this[action](...parameters);
}
};

doWaveFunc(funcStr) {
if (!this.WaveComponent || !this.WaveComponent.wave) {
console.error("Wave component not initialized");
return;
}

const { wave } = this.WaveComponent;
try {
// eslint-disable-next-line no-new-func
const func = new Function("wave", `return wave.${funcStr}`);
func(wave);
wave.render();
} catch (error) {
alert("Error executing wave function: " + error.message);
console.error("Error executing wave function:", error);
}
}

handleSetCameraToFitCell() {
this.WaveComponent.wave.adjustCamerasAndOrbitControlsToCell();
this.WaveComponent.wave.rebuildScene();
}
}

ThreeDEditor.propTypes = {
material: PropTypes.instanceOf(Made.Material).isRequired,
editable: PropTypes.bool,
isConventionalCellShown: PropTypes.bool,
// eslint-disable-next-line react/forbid-prop-types
isConventionalCellShown: PropTypes.bool, // eslint-disable-next-line react/forbid-prop-types
boundaryConditions: PropTypes.object,
onUpdate: PropTypes.func,
};
Expand Down
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ import { ThreeDEditor } from "./components/ThreeDEditor";

const domElement = document.getElementById("root");
const material = new Made.Material(Made.defaultMaterialConfig);

// eslint-disable-next-line react/no-render-return-value
window.threeDEditor = ReactDOM.render(<ThreeDEditor editable material={material} />, domElement);
Loading
Loading