状态录制
上一章回顾: 改变视角
你了解什么是 State, 以及如何获取和修改通过 State 完成了自动环视的功能。
本章你可以学习到
- 通过 State 完成用户操作的录制。
- 通过 State 还原用户操作画面。
准备工作
和上一章节一样,我们新建一个目录(src/3.recording-state
)以及对应的 html 文件 以及 jsx 或 tsx 文件。
jsx 或 tsx 文件可以先拷贝上一章节的内容。
<!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>状态录制 | 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 获取 work
* @param url 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 };
src/3.recording-state/withWindowDimensions.jsx
import React, { Component } from "react";
/**
* React HOC: 获取当前窗口的尺寸
*/
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 };
src/3.recording-state/ModeController.jsx
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: 模态控制
*/
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="全景漫游"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="空间总览"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
}
}
);
export { ModeController };
src/3.recording-state/LookAroundController.jsx
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: 自动环视按钮
*/
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 };
rc/3.recording-state/App.jsx
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";
/** work.json 的数据 URL */
const workURL =
"https://vr-public.realsee-cdn.cn/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 };
src/3.recording-state/index.jsx
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 获取 work
* @param url work.json 的地址
*/
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 };
src/3.recording-state/useWindowDimensions.tsx
import React, { Component, ComponentClass } from "react";
/**
* React HOC: 获取当前窗口的尺寸
*/
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 };
src/3.recording-state/ModeController.tsx
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: 模态控制
*/
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="全景漫游"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="空间总览"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
}
}
);
export { ModeController };
src/3.recording-state/LookAroundController.tsx
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: 自动环视按钮
*/
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 };
src/3.recording-state/App.tsx
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 的数据 URL */
const workURL =
"https://vr-public.realsee-cdn.cn/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 };
src/3.recording-state/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App />, document.querySelector("#app"));
export {};
启动服务 npm run dev
。 并跳转到当前页面 "http://localhost:3000/src/3.recording-state/index.html"。
信息
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
录制 / 回放
这章我们继续通过 State 来完成一个有意思的应用。 本章我们会完成这样的一个应用,记录用户在页面上发生的 State,并且可以回放这些操作。
编写 Recorder 类
首先, 我们需要编写 Recorder 类,来支持记录和回放。 Recorder 类 并非 Five 的内容,只是为了达成本章的效果编写的。
- 实现 startRecording / endRecording 方法,用于开始录制和结束录制。
- 实现 record(state: State) 方法,记录录制内容。记录 startRecording / endRecording 之间的内容。
- 实现 play(callback) 方法,用于回放,调用 play 之后,会安装 record 的内容,依次调用 callback 方法,回放 State。
- JavaScript
- TypeScript
/**
* 录制类
*/
class Recorder {
constructor() {
this.startTime = 0;
this.records = null;
}
/**
* 是否已录制
*/
hasRecords() {
return this.records !== null;
}
/**
* 录制关键帧
* @param state five 的 state
* @returns
*/
record(state) {
if (this.records === null) return;
this.records.push({
state: Object.assign({}, state),
time: Date.now() - this.startTime,
});
}
/**
* 开始录制
*/
startRecording() {
this.startTime = Date.now();
this.records = [];
}
/**
* 结束录制
*/
endRecording() {
this.startTime = 0;
}
/**
* 回放录制
* @param callback 关键帧回调
* @returns 当前是否有录制
*/
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";
/**
* 录制类
*/
class Recorder {
private records: { state: State; time: number }[] | null = null;
private startTime: number;
constructor() {
this.startTime = 0;
this.records = null;
}
/**
* 是否已录制
*/
hasRecords() {
return this.records !== null;
}
/**
* 录制关键帧
* @param state five 的 state
* @returns
*/
record(state: State) {
if (this.records === null) return;
this.records.push({
state: Object.assign({}, state),
time: Date.now() - this.startTime,
});
}
/**
* 开始录制
*/
startRecording() {
this.startTime = Date.now();
this.records = [];
}
/**
* 结束录制
*/
endRecording() {
this.startTime = 0;
}
/**
* 回放录制
* @param callback 关键帧回调
* @returns 当前是否有录制
*/
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 };
编写录制组件
将 Recorder 类,封装成 React 组件。
- 添加一个 RecorderController 文件,用于编写组件。
- 组件内有两个 React state 状态 recording 以及 playing。分别作为 录制中 / 回放中的状态。
useFiveEventCallback
可以获取到 Five 的内置方法回调。
这边我们调用 stateChange 事件,当 state 发生改变时,会触发该事件, 进而将 State 调用recorder.record(state)
方法记录下来。
更多的事件说明请看 Five 的事件列表
- 当回放按钮被按下后,调用
recorder.play(callback)
方法,会将之前录制的 state 逐条调用 callback 方法,回放记录。 - 将记录调用 five 的 setState 方法,将回放内容应用,使得画面变化。
- 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: 状态录制
*/
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>录制中</Button>
</Paper>
);
}
if (this.state.playing) {
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<Button disabled>回放中</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: 状态录制
*/
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>录制中</Button>
</Paper>
);
}
if (this.state.playing) {
return (
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<Button disabled>回放中</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 };
使用状态录制组件
插入到 App 文件的 FiveProvider 中
- 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";
// highlight-start
import { RecorderController } from "./RecorderController";
// highlight-end
/** work.json 的数据 URL */
const workURL =
"https://vr-public.realsee-cdn.cn/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";
// highlight-start
import { RecorderController } from "./RecorderController";
// highlight-end
/** work.json 的数据 URL */
const workURL =
"https://vr-public.realsee-cdn.cn/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 />
// highlight-start
<RecorderController />
// highlight-end
</FiveProvider>
);
}
}
);
export { App };
回到你的浏览器查看,会发现你的页面左上角出现一个录制和播放按钮。试试功能是不是符合预期。
真厉害,你以及可以编写那么复杂的程序了 🥳 。
下一章节你会学到
下一章我们需要和三维空间的模型打交道了
- 了解 Five SDK 的手势交互系统。
- 获取到点的三维位置。