Recap of the previous chapter: Changing the Viewpoint
- You learned what State is, as well as how to read and modify it.
- You used State to implement an automatic look-around feature.
In this chapter you will learn
- How to record user interactions through State.
- How to replay user interactions through State.
Getting Started
Just like in the previous chapter, create a new directory (src/3.recording-state) along with its corresponding html file and js or ts file.
You can start by copying the contents of the previous chapter into the js or ts file.
<!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>import { ref, onBeforeUnmount } from "vue";
function useWindowDimensions() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const listener = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
window.addEventListener("resize", listener, false);
onBeforeUnmount(() => {
window.removeEventListener("resize", listener, false);
});
return { width, height };
}
export { useWindowDimensions };<template>
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary active'
: 'btn btn-primary'
"
@click="() => setState({ mode: Five.Mode.Panorama })"
>
Panorama Walkthrough
</button>
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary'
: 'btn btn-primary active'
"
@click="() => setState({ mode: Five.Mode.Floorplan })"
>
Space Overview
</button>
</div>
</div>
</nav>
</template>
<script setup>
import { useFiveCurrentState } from "@realsee/five/vue";
import { Five } from "@realsee/five";
const [state, setState] = useFiveCurrentState();
</script><template>
<div class="card position-fixed m-2 top-0 end-0">
<button class="btn btn-light" v-show="isShow" @click="startFunc">
<i class="bi bi-arrow-repeat"></i>
</button>
<button class="btn btn-light" v-show="!isShow" @click="stopFunc">
<i class="bi bi-pause"></i>
</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useFiveCurrentState } from "@realsee/five/vue";
const [currentState, setState] = useFiveCurrentState();
const isShow = ref(true);
let timer;
const startFunc = () => {
window.clearInterval(timer);
isShow.value = false;
timer = window.setInterval(() => {
setState({ longitude: currentState.value.longitude + Math.PI / 360 });
}, 16);
};
const stopFunc = () => {
window.clearInterval(timer);
isShow.value = true;
};
</script><template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
<LookAroundController />
</FiveProvider>
</template>
<script setup lang="ts">
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
import LookAroundController from "./LookAroundController.vue";
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");import { ref, onBeforeUnmount } from "vue";
function useWindowDimensions() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const listener = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
window.addEventListener("resize", listener, false);
onBeforeUnmount(() => {
window.removeEventListener("resize", listener, false);
});
return { width, height };
}
export { useWindowDimensions };<template>
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary active'
: 'btn btn-primary'
"
@click="() => setState({ mode: Five.Mode.Panorama })"
>
Panorama Walkthrough
</button>
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary'
: 'btn btn-primary active'
"
@click="() => setState({ mode: Five.Mode.Floorplan })"
>
Space Overview
</button>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { useFiveCurrentState } from "@realsee/five/vue";
import { Five } from "@realsee/five";
const [state, setState] = useFiveCurrentState();
</script><template>
<div class="card position-fixed m-2 top-0 end-0">
<button class="btn btn-light" v-show="isShow" @click="startFunc">
<i class="bi bi-arrow-repeat"></i>
</button>
<button class="btn btn-light" v-show="!isShow" @click="stopFunc">
<i class="bi bi-pause"></i>
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useFiveCurrentState } from "@realsee/five/vue";
const [currentState, setState] = useFiveCurrentState();
const isShow = ref(true);
let timer: number | undefined;
const startFunc = () => {
window.clearInterval(timer);
isShow.value = false;
timer = window.setInterval(() => {
setState({ longitude: currentState.value.longitude + Math.PI / 360 });
}, 16);
};
const stopFunc = () => {
window.clearInterval(timer);
isShow.value = true;
};
</script><template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
<LookAroundController />
</FiveProvider>
</template>
<script setup lang="ts">
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
import LookAroundController from "./LookAroundController.vue";
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");Start the dev server with npm run dev, then navigate to the current page at "http://localhost:3000/src/3.recording-state/index.html".
Info
Please check your console. The port number may change depending on your configuration and which ports are currently in use, so always rely on the value printed in the console. If you use a different build tool, start the server according to that tool's requirements.
Recording / Playback
In this chapter we will continue using State to build an interesting application.
We will create an application that records the State changes the user produces on the page, and that can replay those interactions.
Writing the Recorder Class
First, we need to write a Recorder class to support recording and playback. The Recorder class is not part of Five SDK; it is written purely to achieve the effect of this chapter.
- Implement the startRecording / endRecording methods, used to start and stop recording.
- Implement the record(state: State) method to capture the recorded content. It records what happens between startRecording and endRecording.
- Implement the play(callback) method, used for playback. After calling
play, it walks through the recorded content and invokes the callback method in sequence to replay the State.
/**
* Recorder class
*/
class Recorder {
constructor() {
this.startTime = 0;
this.records = null;
}
/**
* Whether anything has been recorded
*/
hasRecords() {
return this.records !== null;
}
/**
* Record a keyframe
* @param state the Five 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 = [];
}
/**
* Stop recording
*/
endRecording() {
this.startTime = 0;
}
/**
* Replay the recording
* @param callback keyframe callback
* @returns whether there is a recording at present
*/
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";
/**
* Recorder class
*/
class Recorder {
private records: { state: State; time: number }[] | null = null;
private startTime: number;
constructor() {
this.startTime = 0;
this.records = null;
}
/**
* Whether anything has been recorded
*/
hasRecords() {
return this.records !== null;
}
/**
* Record a keyframe
* @param state the Five 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 = [];
}
/**
* Stop recording
*/
endRecording() {
this.startTime = 0;
}
/**
* Replay the recording
* @param callback keyframe callback
* @returns whether there is a recording at present
*/
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 };Writing the Recorder Component
Wrap the Recorder class into a Vue component.
- Add a RecorderController file in which to write the component.
- The component has two states, recording and playing, representing the recording-in-progress and playback-in-progress states respectively.
- `useFiveEventCallback` lets you hook into Five SDK's built-in event callbacks.
Here we listen to the stateChange event, which fires whenever the state changes, and then record that State by calling the recorder.record(state) method.
For more event details, see Five SDK's event list
- When the playback button is pressed, calling the
recorder.play(callback)method invokes the callback for each previously recorded state one by one, replaying the records. - Apply each record by calling Five SDK's setState method, so that the playback content takes effect and the view changes accordingly.
<template>
<div class="card position-fixed m-2 top-0 start-0">
<div class="btn-group align-items-center">
<button class="btn btn-light " v-show="recordStart" @click="startRecord">
<i class="bi bi-record-fill"></i>
</button>
<button class="btn btn-light " v-show="recordEnd" @click="stopRecord">
<i class="bi bi-stop-fill"></i>
</button>
<button class="btn btn-light " v-show="playingStart" @click="playRecord">
<i class="bi bi-play-fill"></i>
</button>
<p class="badge bg-primary m-2 " v-show="recording" @click="">Recording</p>
<p class="badge bg-primary m-2 " v-show="playing" @click="">Playing</p>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { Recorder } from "./recorder";
import { useFiveState, useFiveEventCallback } from "@realsee/five/vue";
const recorder = new Recorder();
const [state, setState] = useFiveState();
const recordStart = ref(true);
const recordEnd = ref(false);
const playingStart = ref(true);
const recording = ref(false);
const playing = ref(false);
useFiveEventCallback("stateChange", (state) => {
if (recording.value === true) {
recorder.record(state);
}
});
const startRecord = () => {
recorder.startRecording();
recordStart.value = false;
recordEnd.value = true;
recording.value = true;
playingStart.value = false;
};
const stopRecord = () => {
recorder.endRecording();
recordStart.value = true;
recordEnd.value = false;
recording.value = false;
playingStart.value = true;
};
const playRecord = () => {
recordStart.value = false;
recordEnd.value = false;
recording.value = false;
playingStart.value = false;
playing.value = true;
const hasRecrod = recorder.play((state, isFinal) => {
setState(state);
if (isFinal) {
recordStart.value = true;
recordEnd.value = false;
recording.value = false;
playingStart.value = true;
playing.value = false;
}
});
if (!hasRecrod) {
recordStart.value = true;
recordEnd.value = false;
recording.value = false;
playingStart.value = true;
playing.value = false;
}
};
</script><template>
<div class="card position-fixed m-2 top-0 start-0">
<div class="btn-group align-items-center">
<button class="btn btn-light " v-show="recordStart" @click="startRecord">
<i class="bi bi-record-fill"></i>
</button>
<button class="btn btn-light " v-show="recordEnd" @click="stopRecord">
<i class="bi bi-stop-fill"></i>
</button>
<button class="btn btn-light " v-show="playingStart" @click="playRecord">
<i class="bi bi-play-fill"></i>
</button>
<p class="badge bg-primary m-2 " v-show="recording" @click="">Recording</p>
<p class="badge bg-primary m-2 " v-show="playing" @click="">Playing</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Recorder } from "./recorder";
import { useFiveState, useFiveEventCallback } from "@realsee/five/vue";
const recorder = new Recorder();
const [state, setState] = useFiveState();
const recordStart = ref(true);
const recordEnd = ref(false);
const playingStart = ref(true);
const recording = ref(false);
const playing = ref(false);
useFiveEventCallback("stateChange", (state) => {
if (recording.value === true) {
recorder.record(state);
}
});
const startRecord = () => {
recorder.startRecording();
recordStart.value = false;
recordEnd.value = true;
recording.value = true;
playingStart.value = false;
};
const stopRecord = () => {
recorder.endRecording();
recordStart.value = true;
recordEnd.value = false;
recording.value = false;
playingStart.value = true;
};
const playRecord = () => {
recordStart.value = false;
recordEnd.value = false;
recording.value = false;
playingStart.value = false;
playing.value = true;
const hasRecrod = recorder.play((state, isFinal) => {
setState(state);
if (isFinal) {
recordStart.value = true;
recordEnd.value = false;
recording.value = false;
playingStart.value = true;
playing.value = false;
}
});
if (!hasRecrod) {
recordStart.value = true;
recordEnd.value = false;
recording.value = false;
playingStart.value = true;
playing.value = false;
}
};
</script>Using the State Recording Component
Insert it into the FiveProvider in the App file.
<template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
<LookAroundController />
// highlight-start
<RecorderController />
// highlight-end
</FiveProvider>
</template>
<script setup>
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
import LookAroundController from "./LookAroundController.vue";
// highlight-start
import RecorderController from "./RecorderController.vue";
// highlight-end
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script><template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
<LookAroundController />
// highlight-start
<RecorderController />
// highlight-end
</FiveProvider>
</template>
<script setup lang="ts">
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
import LookAroundController from "./LookAroundController.vue";
// highlight-start
import RecorderController from "./RecorderController.vue";
// highlight-end
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>Go back to your browser and take a look. You will see a record button and a play button appear in the top-left corner of the page. Give it a try and see whether the feature behaves as expected.
Nicely done — you can already build programs this sophisticated 🥳.
What You Will Learn in the Next Chapter
In the next chapter we will start working with models in 3D space.
- Learn about the Five SDK's gesture interaction system.
- Obtain the 3D position of a point.
