跳到主要内容

状态录制

上一章回顾: 改变视角

  • 你了解什么是 State, 以及如何获取和修改。
  • 通过 State 完成了自动环视的功能。
本章你可以学习到
  • 通过 State 完成用户操作的录制。
  • 通过 State 还原用户操作画面。

准备工作

和上一章节一样,我们新建一个目录(src/3.recording-state)以及对应的 html 文件 以及 jsxtsx 文件。

jsts 文件可以先拷贝上一章节的内容。

<!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>
import { Five, parseWork } from "@realsee/five";

const workURL =
  "https://vr-public.realsee-cdn.cn/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 {};

启动服务 npm run dev。 并跳转到当前页面 "http://localhost:3000/src/3.recording-state/index.html"。

提示

请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。
如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。

录制 / 回放

这章我们继续通过 State 来完成一个有意思的应用。 本章我们会完成这样的一个应用,记录用户在页面上发生的 State,并且可以回放这些操作。

构建 录制功能 UI

我们在页面左上角添加 UI 按钮,我们设计了:

  • 开始录制按钮
  • 停止录制按钮
  • 回放按钮

以及两个状态:

  • 录制中
  • 回放中
<!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>
    <!-- highlight-start -->
    <!-- 录制 -->
    <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>
    <!-- highlight-end -->
    <script type="module" src="./index"></script>
  </body>
</html>

编写 Recorder

首先,我们需要编写 Recorder 类,来支持记录和回放。 Recorder 类 并非 Five 的内容,只是为了达成本章的效果编写的。

  1. 实现 startRecording / endRecording 方法,用于开始录制和结束录制。
  2. 实现 record(state: State) 方法,记录录制内容。记录 startRecording / endRecording 之间的内容。
  3. 实现 play(callback) 方法,用于回放,调用 play 之后,会安装 record 的内容,依次调用 callback 方法,回放 State
/**
 * 录制类
 */
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 };

编写录制逻辑

在上一章的 环视 的代码后追加:

{
  //=== 录制 ===
  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 的手势交互系统。
  • 获取到点的三维位置。