状态录制
上一章回顾: 改变视角
- 你了解什么是 State, 以及如何获取和修改。
- 通过 State 完成了自动环视的功能。
本章你可以学习到
- 通过 State 完成用户操作的录制。
- 通过 State 还原用户操作画面。
准备工作
和上一章节一样,我们新建一个目录(src/3.recording-state
)以及对应的 html 文件 以及 jsx 或 tsx 文件。
js 或 ts 文件可以先拷贝上一章节的内容。
src/3.recording-state/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>改变视角 | Knowing state</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"
rel="stylesheet"
/>
<style>
html,
body,
#app {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- 模式切换 -->
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button class="btn btn-primary active js-Panorama">全景漫游</button>
<button class="btn btn-primary js-Floorplan">空间总览</button>
</div>
</div>
</nav>
<!-- 环视 -->
<div class="card position-fixed m-2 top-0 end-0">
<button class="btn btn-light js-lookAround-start">
<i class="bi bi-arrow-repeat"></i>
</button>
<button class="btn btn-light js-lookAround-stop d-none">
<i class="bi bi-pause"></i>
</button>
</div>
<script type="module" src="./index"></script>
</body>
</html>
- JavaScript
- TypeScript
src/3.recording-state/index.js
import { Five, parseWork } from "@realsee/five";
const workURL =
"https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const five = new Five();
five.appendTo(document.querySelector("#app"));
fetch(workURL)
.then((res) => res.json())
.then((json) => {
const work = parseWork(json);
five.load(work);
});
window.addEventListener("resize", () => five.refresh(), false);
{
// === 模式切换 ===
const buttons = {
Panorama: document.querySelector(".js-Panorama"),
Floorplan: document.querySelector(".js-Floorplan"),
};
for (const [modeName, element] of Object.entries(buttons)) {
element.addEventListener(
"click",
() => {
five.setState({ mode: modeName });
},
false
);
}
five.on("stateChange", (state) => {
for (const [modeName, element] of Object.entries(buttons)) {
if (modeName === state.mode) {
element.classList.add("active");
} else {
element.classList.remove("active");
}
}
});
}
{
// === 环视 ===
let timer;
const startButton = document.querySelector(".js-lookAround-start");
const stopButton = document.querySelector(".js-lookAround-stop");
startButton.addEventListener(
"click",
() => {
window.clearInterval(timer);
timer = window.setInterval(() => {
five.setState({ longitude: five.state.longitude + Math.PI / 360 });
}, 16);
startButton.classList.add("d-none");
stopButton.classList.remove("d-none");
},
false
);
stopButton.addEventListener(
"click",
() => {
window.clearInterval(timer);
startButton.classList.remove("d-none");
stopButton.classList.add("d-none");
},
false
);
}
export {};
src/3.recording-state/index.ts
import { Five, Mode, parseWork } from "@realsee/five";
const workURL =
"https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const five = new Five();
five.appendTo(document.querySelector("#app")!);
fetch(workURL)
.then((res) => res.json())
.then((json) => {
const work = parseWork(json);
five.load(work);
});
window.addEventListener("resize", () => five.refresh(), false);
{
// === 模式切换 ===
const buttons: Partial<Record<Mode, Element>> = {
Panorama: document.querySelector(".js-Panorama")!,
Floorplan: document.querySelector(".js-Floorplan")!,
};
for (const [modeName, element] of Object.entries(buttons)) {
element.addEventListener(
"click",
() => {
five.setState({ mode: modeName as Mode });
},
false
);
}
five.on("stateChange", (state) => {
for (const [modeName, element] of Object.entries(buttons)) {
if (modeName === state.mode) {
element.classList.add("active");
} else {
element.classList.remove("active");
}
}
});
}
{
// === 环视 ===
let timer: number | undefined;
const startButton = document.querySelector(".js-lookAround-start")!;
const stopButton = document.querySelector(".js-lookAround-stop")!;
startButton.addEventListener(
"click",
() => {
window.clearInterval(timer);
timer = window.setInterval(() => {
five.setState({ longitude: five.state.longitude + Math.PI / 360 });
}, 16);
startButton.classList.add("d-none");
stopButton.classList.remove("d-none");
},
false
);
stopButton.addEventListener(
"click",
() => {
window.clearInterval(timer);
startButton.classList.remove("d-none");
stopButton.classList.add("d-none");
},
false
);
}
export {};
启动服务 npm run dev
。 并跳转到当前页面 "http://localhost:3000/src/3.recording-state/index.html"。
提示
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。
如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
录制 / 回放
这章我们继续通过 State 来完成一个有意思的应用。 本章我们会完成这样的一个应用,记录用户在页面上发生的 State,并且可以回放这些操作。
构建 录制功能 UI
我们在页面左上角添加 UI 按钮,我们设计了:
- 开始录制按钮
- 停止录制按钮
- 回放按钮
以及两个状态:
- 录制中
- 回放中
src/3.recording-state/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>状态录制 | Recording state</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"
rel="stylesheet"
/>
<style>
html,
body,
#app {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- 模式切换 -->
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button class="btn btn-primary active js-Panorama">全景漫游</button>
<button class="btn btn-primary js-Floorplan">空间总览</button>
</div>
</div>
</nav>
<!-- 环视 -->
<div class="card position-fixed m-2 top-0 end-0">
<button class="btn btn-light js-lookAround-start">
<i class="bi bi-arrow-repeat"></i>
</button>
<button class="btn btn-light js-lookAround-stop d-none">
<i class="bi bi-pause"></i>
</button>
</div>
<!-- 录制 -->
<div class="card position-fixed m-2 top-0 start-0">
<div class="btn-group align-items-center">
<button class="btn btn-light js-recording-start">
<i class="bi bi-record-fill"></i>
</button>
<button class="btn btn-light js-recording-stop d-none">
<i class="bi bi-stop-fill"></i>
</button>
<button class="btn btn-light js-recording-play">
<i class="bi bi-play-fill"></i>
</button>
<p class="badge bg-primary m-2 js-state-recording d-none">录制中</p>
<p class="badge bg-primary m-2 js-state-playing d-none">播放中</p>
</div>
</div>
<script type="module" src="./index"></script>
</body>
</html>
编写 Recorder 类
首先,我们需要编写 Recorder 类,来支持记录和回放。 Recorder 类 并非 Five 的内容,只是为了达成本章的效果编写的。
- 实现 startRecording / endRecording 方法,用于开始录制和结束录制。
- 实现 record(state: State) 方法,记录录制内容。记录 startRecording / endRecording 之间的内容。
- 实现 play(callback) 方法,用于回放,调用 play 之后,会安装 record 的内容,依次调用 callback 方法,回放 State。
- JavaScript
- TypeScript
src/3.recording-state/recorder.js
/**
* 录制类
*/
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 };
src/3.recording-state/recorder.ts
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 };
编写录制逻辑
在上一章的 环视 的代码后追加:
- JavaScript
- TypeScript
src/3.recording-state/index.js
{
//=== 录制 ===
const recorder = new Recorder();
const startRecordingButton = document.querySelector(".js-recording-start");
const stopRecordingButton = document.querySelector(".js-recording-stop");
const playRecordingButton = document.querySelector(".js-recording-play");
const recordingState = document.querySelector(".js-state-recording");
const playingState = document.querySelector(".js-state-playing");
five.on("stateChange", (state) => {
if (recordingState.classList.contains("d-none")) return;
recorder.record(state);
});
startRecordingButton.addEventListener(
"click",
() => {
recorder.startRecording();
startRecordingButton.classList.add("d-none");
stopRecordingButton.classList.remove("d-none");
playRecordingButton.classList.add("d-none");
recordingState.classList.remove("d-none");
playingState.classList.add("d-none");
},
false
);
stopRecordingButton.addEventListener(
"click",
() => {
recorder.endRecording();
startRecordingButton.classList.remove("d-none");
stopRecordingButton.classList.add("d-none");
playRecordingButton.classList.remove("d-none");
recordingState.classList.add("d-none");
playingState.classList.add("d-none");
},
false
);
playRecordingButton.addEventListener(
"click",
() => {
const hasReocrd = recorder.play((state, isFinal) => {
five.setState(state);
if (isFinal) {
startRecordingButton.classList.remove("d-none");
stopRecordingButton.classList.add("d-none");
playRecordingButton.classList.remove("d-none");
recordingState.classList.add("d-none");
playingState.classList.add("d-none");
}
});
if (hasReocrd) {
startRecordingButton.classList.add("d-none");
stopRecordingButton.classList.add("d-none");
playRecordingButton.classList.add("d-none");
recordingState.classList.add("d-none");
playingState.classList.remove("d-none");
}
},
false
);
}
src/3.recording-state/index.ts
{
//=== 录制 ===
const recorder = new Recorder();
const startRecordingButton = document.querySelector(".js-recording-start")!;
const stopRecordingButton = document.querySelector(".js-recording-stop")!;
const playRecordingButton = document.querySelector(".js-recording-play")!;
const recordingState = document.querySelector(".js-state-recording")!;
const playingState = document.querySelector(".js-state-playing")!;
five.on("stateChange", (state) => {
if (recordingState.classList.contains("d-none")) return;
recorder.record(state);
});
startRecordingButton.addEventListener(
"click",
() => {
recorder.startRecording();
startRecordingButton.classList.add("d-none");
stopRecordingButton.classList.remove("d-none");
playRecordingButton.classList.add("d-none");
recordingState.classList.remove("d-none");
playingState.classList.add("d-none");
},
false
);
stopRecordingButton.addEventListener(
"click",
() => {
recorder.endRecording();
startRecordingButton.classList.remove("d-none");
stopRecordingButton.classList.add("d-none");
playRecordingButton.classList.remove("d-none");
recordingState.classList.add("d-none");
playingState.classList.add("d-none");
},
false
);
playRecordingButton.addEventListener(
"click",
() => {
const hasReocrd = recorder.play((state, isFinal) => {
five.setState(state);
if (isFinal) {
startRecordingButton.classList.remove("d-none");
stopRecordingButton.classList.add("d-none");
playRecordingButton.classList.remove("d-none");
recordingState.classList.add("d-none");
playingState.classList.add("d-none");
}
});
if (hasReocrd) {
startRecordingButton.classList.add("d-none");
stopRecordingButton.classList.add("d-none");
playRecordingButton.classList.add("d-none");
recordingState.classList.add("d-none");
playingState.classList.remove("d-none");
}
},
false
);
}
回到你的浏览器查看,会发现你的页面左上角出现一个录制和播放按钮。试试功能是不是符合预期。
真厉害,你以及可以编写那么复杂的程序了 🥳 。
下一章节你会学到
下一章我们需要和三维空间的模型打交道了
- 了解 Five SDK 的手势交互系统。
- 获取到点的三维位置。