Status Recording
Previous chapter recalls: Change the perspective
You know what is State, and how to get and modify auto-ring features via State.
- Record user operations via State.
- Restore the user action picture with State.
Preparatory work
Like the previous section, we create a new directory (src/3.recording-state
and corresponding html and jsx or tsx files.
jsx or tsx files can first copy the previous chapter.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>state recording | Recording state</title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body #app {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index"></script>
</body>
</html>
- JavaScript
- TypeScript
import React, { Component } from "react";
import { parseWork } from "@realsee/five";
/**
* React HOC fetch work
* @param url address of work.json
*/
function withFetchWork(url) {
return function (Compnent) {
return class extends Component {
state = { work: null };
componentDidMount() {
fetch(url)
.then((res) => res.json())
.then((json) => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props} />;
}
};
};
}
export { withFetchWork };
import React, { Component } from "react";
/**
* React HOC: Get the size of the current window
*/
function withWindowDimensions() {
return function (Compnent) {
return class extends Component {
state = this.getWindowDimensions();
resizeListener = () => {
this.setState(this.getWindowDimensions());
};
getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
componentDidMount() {
window.addEventListener("resize", this.resizeListener, false);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeListener, false);
}
render() {
const dimensions = {
width: this.state.width,
height: this.state.height,
};
return <Compnent windowDimensions={dimensions} {...this.props} />;
}
};
};
}
export { withWindowDimensions };
import React, { Component } from "react";
import { Five } from "@realsee/five";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";
const FEATURES = createFiveFeature("currentState", "setState");
/**
* React Component: Modal Control
*/
const ModeController = compose(withFive(FEATURES))(
class extends Component {
render() {
return (
<Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={this.props.$five.currentState.mode}
onChange={(_, newValue) => {
this.props.$five.setState({ mode: newValue });
}}
>
<BottomNavigationAction
label="Panorama roaming"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="Space overview"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
}
}
);
export { ModeController };
import React, { Component } from "react";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FlipCameraAndroidIcon from "@mui/icons-material/FlipCameraAndroid";
import PauseIcon from "@mui/icons-material/Pause";
const FEATURES = createFiveFeature("currentState", "setState");
/**
* ReactComponent: Automatic Ring Button
*/
const LookAroundController = compose(withFive(FEATURES))(
class extends Component {
timer;
state = { active: false };
toggleActive(active) {
window.clearInterval(this.timer);
this.setState({ active });
if (active === true) {
this.timer = window.setInterval(() => {
this.props.$five.setState({
longitude: this.props.$five.currentState.longitude + Math.PI / 360,
});
}, 16);
} else {
delete this.timer;
}
}
render() {
return (
<Paper sx={{ position: "fixed", top: 10, right: 10 }}>
{this.state.active ? (
<IconButton onClick={() => this.toggleActive(false)}>
<PauseIcon />
</IconButton>
) : (
<IconButton onClick={() => this.toggleActive(true)}>
<FlipCameraAndroidIcon />
</IconButton>
)}
</Paper>
);
}
}
);
export { LookAroundController };
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";
/** Data URL of work.json */
const workURL =
"https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(
class extends Component {
render() {
const { work, windowDimensions } = this.props;
return (
<FiveProvider initialWork={work}>
<FiveCanvas
width={windowDimensions.width}
height={windowDimensions.height}
/>
<ModeController />;
<LookAroundController />;
</FiveProvider>
);
}
}
);
export { App };
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App />, document.querySelector("#app"));
export {};
import React, { Component, ComponentClass } from "react";
import { Work, parseWork } from "@realsee/five";
/**
* React HOC for work
* @param url work. The address of the son
*/
function withFetchWork<P extends Record<string, any>>(url: string) {
return function (
Compnent: ComponentClass<P & { work: Work }>
): ComponentClass<P> {
return class extends Component<P, { work: Work | null }> {
state = { work: null };
componentDidMount() {
fetch(url)
.then((res) => res.json())
.then((json) => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props} />;
}
};
};
}
export { withFetchWork };
import React, { Component, ComponentClass } from "react";
/**
* React HOC: Get the current window size
*/
function withWindowDimensions<P extends Record<string, any>>() {
return function (
Compnent: ComponentClass<
P & { windowDimensions: { width: number; height: number } }
>
): ComponentClass<P> {
return class extends Component<P, { width: number; height: number }> {
state = this.getWindowDimensions();
resizeListener = () => {
this.setState(this.getWindowDimensions());
};
getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
componentDidMount() {
window.addEventListener("resize", this.resizeListener, false);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeListener, false);
}
render() {
const dimensions = {
width: this.state.width,
height: this.state.height,
};
return <Compnent windowDimensions={dimensions} {...this.props} />;
}
};
};
}
export { withWindowDimensions };
import React, { Component } from "react";
import { Five, Mode } from "@realsee/five";
import {
withFive,
createFiveFeature,
PropTypeOfFiveFeatures,
} from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";
const FEATURES = createFiveFeature("currentState", "setState");
type Props = PropTypeOfFiveFeatures<typeof FEATURES>;
/**
* React Component: Model Control
*/
const ModeController = compose(withFive(FEATURES))(
class extends Component<Props> {
render() {
return (
<Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={this.props.$five.currentState.mode}
onChange={(_, newValue: Mode) => {
this.props.$five.setState({ mode: newValue });
}}
>
<BottomNavigationAction
label="Panorama roaming"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="Space overview"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
}
}
);
export { ModeController };
import React, { Component } from "react";
import {
withFive,
createFiveFeature,
PropTypeOfFiveFeatures,
} from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FlipCameraAndroidIcon from "@mui/icons-material/FlipCameraAndroid";
import PauseIcon from "@mui/icons-material/Pause";
const FEATURES = createFiveFeature("currentState", "setState");
type Props = PropTypeOfFiveFeatures<typeof FEATURES>;
type State = { active: boolean };
/**
* ReactComponent: Automatic lookaround button
*/
const LookAroundController = compose(withFive(FEATURES))(
class extends Component<Props, State> {
timer?: number;
state = { active: false };
toggleActive(active: boolean) {
window.clearInterval(this.timer);
this.setState({ active });
if (active === true) {
this.timer = window.setInterval(() => {
this.props.$five.setState({
longitude: this.props.$five.currentState.longitude + Math.PI / 360,
});
}, 16);
} else {
delete this.timer;
}
}
render() {
return (
<Paper sx={{ position: "fixed", top: 10, right: 10 }}>
{this.state.active ? (
<IconButton onClick={() => this.toggleActive(false)}>
<PauseIcon />
</IconButton>
) : (
<IconButton onClick={() => this.toggleActive(true)}>
<FlipCameraAndroidIcon />
</IconButton>
)}
</Paper>
);
}
}
);
export { LookAroundController };
import React, { Component } from "react";
import { Work } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";
/** work.json data URL */
const workURL =
"https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(
class extends Component<{
work: Work;
windowDimensions: { width: number; height: number };
}> {
render() {
const { work, windowDimensions } = this.props;
return (
<FiveProvider initialWork={work}>
<FiveCanvas
width={windowDimensions.width}
height={windowDimensions.height}
/>
<ModeController />
<LookAroundController />
</FiveProvider>
);
}
}
);
export { App };
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App />, document.querySelector("#app"));
export {};
Start service npm run dev
and jump to the current page "http://localhost:3000/src/3.recording-state/index.html".
Please see your console. The port number will change due to your configuration and current port occupancy, please check the console output. If you use another development build tool, please start the service as required by your own development build tool.
Recording / Playback
This chapter continues to complete an interesting app with State. This chapter will complete such an app, log users into Stateon the page and can replay these actions.
Write Recorder
First, we need to write Recorder to support records and playbacks. Recorder category is not the content of Five , it is only written to achieve the effect of this chapter.
- Implement the startRecording / endRecording method to start recording and end recording.
- Implementing record(state: State) method, recording content.Record between startRecording / endRecording.
- Implement play(callback) method, for playbacks, after calling play, install record content, call callback method,play State.
- JavaScript
- TypeScript
/**
* Recording class
*/
class Recorder {
constructor() {
this.startTime = 0;
this.records = null;
}
/**
* Whether
has been recorded */
hasRecords() {
return this.records !== null;
}
/**
* Record keyframe
* @param state five of state
* @returns
*/
record(state) {
if (this.records === null) return;
this.records.push({
state: Object.assign({}, state),
time: Date.now() - this.startTime,
});
}
/**
* start recording
*/
startRecording() {
this.startTime = Date.now();
this.records = [];
}
/**
* End recording
*/
endRecording() {
this.startTime = 0;
}
/**
* Playback recording
* @param callback key frame callback
* @returns whether the current There are records
*/
play(callback) {
if (this.records === null || this.records.length === 0) return false;
const records = this.records.slice();
const keyframe = (keyIndex) => {
const current = records[keyIndex];
const next = records[keyIndex + 1];
callback(current.state, next === undefined);
if (next) {
const delay = next.time - current.time;
setTimeout(() => keyframe(keyIndex + 1), delay);
}
};
keyframe(0);
return true;
}
}
export { Recorder };
import { State } from "@realsee/five";
/**
* Recording class
*/
class Recorder {
private records: { state: State; time: number }[] | null = null;
private startTime: number;
constructor() {
this.startTime = 0;
this.records = null;
}
/**
* Whether
has been recorded */
hasRecords() {
return this.records !== null;
}
/**
* record key frame
* @param state five's state
* @returns
*/
record(state: State) {
if (this.records === null) return;
this.records.push({
state: Object.assign({}, state),
time: Date.now() - this.startTime,
});
}
/* *
* start recording
*/
startRecording() {
this.startTime = Date.now();
this.records = [];
}
/**
* end recording
*/
endRecording() {
this.startTime = 0;
}
/**
* Playback recording
* @param callback Keyframe callback
* @returns Whether there is a recording currently
*/
play(callback: (state: State, isFinal: boolean) => void) {
if (this.records === null || this.records.length === 0) return false;
const records = this.records.slice();
const keyframe = (keyIndex: number) => {
const current = records[keyIndex];
const next = records[keyIndex + 1];
callback(current.state, next === undefined);
if (next) {
const delay = next.time - current.time;
setTimeout(() => keyframe(keyIndex + 1), delay);
}
};
keyframe(0);
return true;
}
}
export { Recorder };
Write Recording Component
Put Recorder into React components.
Add a RecorderController file to write components.
Two React state states in the component recording and playing.Separate status in recording/playback.
useFiveEventCallback
Gets the Built-in method callback.
here we call stateChange event, when state changes, this event will be triggered, and then State callsrecorder.record(state)
method to record.More event descriptions see Five events list
When the playback button is pressed, call the
recorder.play(callback)
method, then call the previously recorded state arc call callback method.The setState method to call the record and apply the replay content to change the picture.
- JavaScript
- TypeScript
import React, { Component } from "react";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
import StopIcon from "@mui/icons-material/Stop";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { Recorder } from "./recorder";
const FEATURES = createFiveFeature("state", "setState", "on", "off");
/**
* ReactComponent: State Recorder
*/
const RecorderController = compose(withFive(FEATURES))(
class extends Component {
recorder = new Recorder();
state = { recording: false, playing: false };
startRecording = () => {
this.recorder.startRecording();
this.setState({ recording: true });
};
endRecording = () => {
this.recorder.endRecording();
this.setState({ recording: false });
};
play = () => {
const hasRecord = this.recorder.play((state, isFinal) => {
this.props.$five.setState(state);
this.setState({ playing: !isFinal });
});
this.setState({ playing: hasRecord });
};
record = (state) => {
this.recorder.record(state);
};
componentDidMount() {
this.props.$five.on("stateChange", this.record);
}
componentWillUnmount() {
this.props.$five.off("stateChange", this.record);
}
render() {
if (this.state.recording) {
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<IconButton onClick={this.endRecording}>
<StopIcon />
</IconButton>
<Button disabled>Recording</Button>
</Paper>
);
}
if (this.state.playing) {
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<Button disabled>Playing</Button>
</Paper>
);
}
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<IconButton onClick={this.startRecording}>
<FiberManualRecordIcon />
</IconButton>
<IconButton onClick={this.play}>
<PlayArrowIcon />
</IconButton>
</Paper>
);
}
}
);
export { RecorderController };
import React, { Component } from "react";
import { State } from "@realsee/five";
import {
withFive,
createFiveFeature,
PropTypeOfFiveFeatures,
} from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
import StopIcon from "@mui/icons-material/Stop";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { Recorder } from "./recorder";
const FEATURES = createFiveFeature("state", "setState", "on", "off");
type Props = PropTypeOfFiveFeatures<typeof FEATURES>;
/**
* ReactComponent: state recording
*/
const RecorderController = compose(withFive(FEATURES))(
class extends Component<Props, { recording: boolean; playing: boolean }> {
recorder = new Recorder();
state = { recording: false, playing: false };
startRecording = () => {
this.recorder.startRecording();
this.setState({ recording: true });
};
endRecording = () => {
this.recorder.endRecording();
this.setState({ recording: false });
};
play = () => {
const hasRecord = this.recorder.play((state, isFinal) => {
this.props.$five.setState(state);
this.setState({ playing: !isFinal });
});
this.setState({ playing: hasRecord });
};
record = (state: State) => {
this.recorder.record(state);
};
componentDidMount() {
this.props.$five.on("stateChange", this.record);
}
componentWillUnmount() {
this.props.$five.off("stateChange", this.record);
}
render() {
if (this.state.recording) {
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<IconButton onClick={this.endRecording}>
<StopIcon />
</IconButton>
<Button disabled>Recording</Button>
</Paper>
);
}
if (this.state.playing) {
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<Button disabled>Playing</Button>
</Paper>
);
}
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<IconButton onClick={this.startRecording}>
<FiberManualRecordIcon />
</IconButton>
<IconButton onClick={this.play}>
<PlayArrowIcon />
</IconButton>
</Paper>
);
}
}
);
export { RecorderController };
Use Status Recording Component
Insert into App file GiveProvider
- JavaScript
- TypeScript
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";
import { RecorderController } from "./RecorderController";
/** Data URL of work.json */
const workURL =
"https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(
class extends Component {
render() {
const { work, windowDimensions } = this.props;
return (
<FiveProvider initialWork={work}>
<FiveCanvas
width={windowDimensions.width}
height={windowDimensions.height}
/>
<ModeController />;
<LookAroundController />; // highlight-start
<RecorderController />; // highlight-end
</FiveProvider>
);
}
}
);
export { App };
import React, { Component } from "react";
import { Work } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";
import { RecorderController } from "./RecorderController";
/** data URL of work.json */
const workURL =
"https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(
class extends Component<{
work: Work;
windowDimensions: { width: number; height: number };
}> {
render() {
const { work, windowDimensions } = this.props;
return (
<FiveProvider initialWork={work}>
<FiveCanvas
width={windowDimensions.width}
height={windowDimensions.height}
/>
<ModeController />
<LookAroundController />
<RecorderController />
</FiveProvider>
);
}
}
);
export { App };
Back to your browser to view will find a record and play button in the upper left corner of your page.Try whether the trial function meets expectations.
Yes, you’re and can write what complex programs are🥳
The next section will be completed by you
- Learn about the Live SDK gesture interaction.
- Gets a three-dimensional position to a point.