Spaces:
Running
Running
boryzoo commited on
Commit ·
339e856
0
Parent(s):
Deploy Orbit Video Viewer to Hugging Face Spaces
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +6 -0
- .gitignore +8 -0
- ASSETS.md +31 -0
- LICENSE +21 -0
- README.huggingface.md +64 -0
- README.md +64 -0
- README.zh-CN.md +153 -0
- THIRD_PARTY_NOTICES.md +17 -0
- VALIDATION.md +82 -0
- docs/COMPATIBILITY.md +19 -0
- docs/INTEGRATION.md +143 -0
- docs/ORIGIN_AND_ATTRIBUTION.md +19 -0
- docs/PROJECT_AND_PUBLISHING.zh-CN.md +178 -0
- docs/PUBLISHING_RUNBOOK.zh-CN.md +94 -0
- docs/RELEASE_CHECKLIST.md +55 -0
- docs/SKILL_VALIDATION.md +38 -0
- docs/TECHNICAL_REPORT.md +61 -0
- docs/assets/concepts/PROMPTS.md +21 -0
- docs/assets/concepts/game-item-inspection.jpg +3 -0
- docs/assets/concepts/interactive-comic.jpg +3 -0
- docs/assets/concepts/product-viewer.jpg +3 -0
- examples/basic-integration/index.html +23 -0
- examples/basic-integration/package.json +17 -0
- examples/basic-integration/pnpm-lock.yaml +668 -0
- examples/basic-integration/src/main.ts +35 -0
- examples/basic-integration/src/style.css +27 -0
- examples/basic-integration/src/vite-env.d.ts +1 -0
- examples/basic-integration/tsconfig.json +15 -0
- examples/basic-integration/vite.config.ts +7 -0
- index.html +49 -0
- package.json +59 -0
- pnpm-lock.yaml +853 -0
- public/demo/poster.png +3 -0
- public/demo/video_01.mp4 +3 -0
- recording-comic/README.md +101 -0
- recording-comic/assets/impact.png +3 -0
- recording-comic/assets/shot.png +3 -0
- recording-comic/index.html +61 -0
- recording-comic/main.ts +114 -0
- recording-comic/output/orbit-comic-demo.gif +3 -0
- recording-comic/output/orbit-comic-demo.mp4 +3 -0
- recording-comic/style.css +261 -0
- skills/integrate-orbit-video/SKILL.md +62 -0
- skills/integrate-orbit-video/agents/openai.yaml +5 -0
- skills/integrate-orbit-video/references/sdk-api.md +80 -0
- src/core/FrameExtractor.ts +203 -0
- src/core/FrameSource.ts +21 -0
- src/core/FrameStore.ts +68 -0
- src/core/OrbitPanel.ts +214 -0
- src/core/index.ts +2 -0
.gitattributes
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* text=auto eol=lf
|
| 2 |
+
|
| 3 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
.DS_Store
|
| 4 |
+
*.log
|
| 5 |
+
*.tsbuildinfo
|
| 6 |
+
|
| 7 |
+
# Local publishing plans; not part of the public repository.
|
| 8 |
+
publishing/huggingface-article/
|
ASSETS.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Demo media
|
| 2 |
+
|
| 3 |
+
The sample image and video in `public/demo/` were created by the repository owner with Jimeng AI and are included so the interactive work can be experienced immediately.
|
| 4 |
+
|
| 5 |
+
They are made available under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/), to the extent the repository owner holds rights in the generated media:
|
| 6 |
+
|
| 7 |
+
- `public/demo/poster.png`
|
| 8 |
+
- `public/demo/video_01.mp4`
|
| 9 |
+
|
| 10 |
+
Suggested attribution: **“Orbit Comic demo artwork, created with Jimeng AI.”**
|
| 11 |
+
|
| 12 |
+
The MIT software license and the CC BY 4.0 media license are separate. You may replace both files with your own media; your replacement media keeps whatever license you choose for it.
|
| 13 |
+
|
| 14 |
+
## README media
|
| 15 |
+
|
| 16 |
+
The current README preview and its reproducible recording sources were created from Jimeng AI media supplied by the repository owner. They follow the same CC BY 4.0 terms, to the extent the repository owner holds rights in the generated media:
|
| 17 |
+
|
| 18 |
+
- `recording-comic/assets/shot.png`
|
| 19 |
+
- `recording-comic/assets/impact.png`
|
| 20 |
+
- `recording-comic/output/orbit-comic-demo.gif`
|
| 21 |
+
- `recording-comic/output/orbit-comic-demo.mp4`
|
| 22 |
+
|
| 23 |
+
The following concept images were generated for this repository with OpenAI ImageGen and are also offered under CC BY 4.0, to the extent the repository owner holds rights in them:
|
| 24 |
+
|
| 25 |
+
- `docs/assets/concepts/product-viewer.jpg`
|
| 26 |
+
- `docs/assets/concepts/game-item-inspection.jpg`
|
| 27 |
+
- `docs/assets/concepts/interactive-comic.jpg`
|
| 28 |
+
|
| 29 |
+
They illustrate possible uses only. They are not screenshots of completed bundled applications.
|
| 30 |
+
|
| 31 |
+
The exact generation prompts are recorded in [`docs/assets/concepts/PROMPTS.md`](docs/assets/concepts/PROMPTS.md).
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Orbit Panel Viewer contributors
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.huggingface.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Orbit Comic Demo
|
| 3 |
+
emoji: 🎞️
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: static
|
| 7 |
+
app_file: dist/index.html
|
| 8 |
+
app_build_command: npm run build:demo
|
| 9 |
+
fullWidth: true
|
| 10 |
+
header: mini
|
| 11 |
+
short_description: A live demo of the Orbit Video Viewer library.
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Orbit Video Viewer
|
| 15 |
+
|
| 16 |
+
**Turn pre-rendered orbit videos into interactive, drag-controlled views. No 3D models required.**
|
| 17 |
+
|
| 18 |
+
Orbit Comic is the live demonstration above. Drag horizontally across the artwork to browse the viewpoints encoded in the MP4. Use **Change video** to try your own local MP4; the file stays in your browser and is not uploaded by this app.
|
| 19 |
+
|
| 20 |
+
The reusable open-source project provides `OrbitViewer`, browser-side MP4 processing, a public control API, an integration Skill for AI coding agents, and a standalone consumer example.
|
| 21 |
+
|
| 22 |
+
> **Technique credit:** This project studies an interaction technique shared by [monoradio (@monoradio102)](https://x.com/monoradio102) through a Claude Artifact. The code was independently implemented with Codex and does not copy the Artifact's source.
|
| 23 |
+
|
| 24 |
+
## Source and local use
|
| 25 |
+
|
| 26 |
+
The canonical source is [boryzoo/orbit-video-viewer on GitHub](https://github.com/boryzoo/orbit-video-viewer). Use GitHub for the editable source, installation documentation, version history, and [issue tracking](https://github.com/boryzoo/orbit-video-viewer/issues). This Space is the live demonstration of the same project.
|
| 27 |
+
|
| 28 |
+
For a local source checkout:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
pnpm install
|
| 32 |
+
pnpm dev
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
The verified release target is Chromium with H.264 MP4. Safari and Firefox support remains pending the same manual interaction matrix. This is controlled viewpoint playback rather than real 3D; it cannot reveal angles absent from the source video.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
# Orbit Video Viewer(中文)
|
| 40 |
+
|
| 41 |
+
**把预先生成的环绕视频变成可拖动控制视角的网页交互,不需要 3D 模型。**
|
| 42 |
+
|
| 43 |
+
上方的 Orbit Comic 是在线演示。请在画面中水平拖动,浏览 MP4 已经包含的不同观察视角。使用 **Change video** 可以尝试自己的本地 MP4;文件只在浏览器内处理,不会由这个应用上传。
|
| 44 |
+
|
| 45 |
+
可复用的开源项目包含 `OrbitViewer`、浏览器端 MP4 处理、公开控制 API、供 AI 编程工具使用的接入 Skill,以及一个独立消费示例。
|
| 46 |
+
|
| 47 |
+
> **技术灵感署名:** 本项目研究了 [monoradio(@monoradio102)](https://x.com/monoradio102) 通过 Claude Artifact 分享的交互思路。代码由 Codex 独立实现,没有复制该 Artifact 的源码。
|
| 48 |
+
|
| 49 |
+
## 源码与本地运行
|
| 50 |
+
|
| 51 |
+
项目的唯一源码基准是 GitHub 上的 [boryzoo/orbit-video-viewer](https://github.com/boryzoo/orbit-video-viewer)。可编辑源码、安装说明和版本历史以 GitHub 为准;问题可提交到 [GitHub Issues](https://github.com/boryzoo/orbit-video-viewer/issues)。这个 Space 是同一项目的在线演示入口。
|
| 52 |
+
|
| 53 |
+
本地源码运行方式:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
pnpm install
|
| 57 |
+
pnpm dev
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
当前已经验证的发布目标是 Chromium + H.264 MP4;Safari 与 Firefox 尚待完成同等手动交互测试。这是受控的视角回放,不是真实 3D,不能生成源视频中不存在的角度。
|
| 61 |
+
|
| 62 |
+
## License / 许可
|
| 63 |
+
|
| 64 |
+
Software is MIT licensed. Demo media has separate terms documented in `ASSETS.md`. 技术代码使用 MIT License;演示媒体的独立条款见 `ASSETS.md`。
|
README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Orbit Comic Demo
|
| 3 |
+
emoji: 🎞️
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: static
|
| 7 |
+
app_file: dist/index.html
|
| 8 |
+
app_build_command: npm run build:demo
|
| 9 |
+
fullWidth: true
|
| 10 |
+
header: mini
|
| 11 |
+
short_description: A live demo of the Orbit Video Viewer library.
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Orbit Video Viewer
|
| 15 |
+
|
| 16 |
+
**Turn pre-rendered orbit videos into interactive, drag-controlled views. No 3D models required.**
|
| 17 |
+
|
| 18 |
+
Orbit Comic is the live demonstration above. Drag horizontally across the artwork to browse the viewpoints encoded in the MP4. Use **Change video** to try your own local MP4; the file stays in your browser and is not uploaded by this app.
|
| 19 |
+
|
| 20 |
+
The reusable open-source project provides `OrbitViewer`, browser-side MP4 processing, a public control API, an integration Skill for AI coding agents, and a standalone consumer example.
|
| 21 |
+
|
| 22 |
+
> **Technique credit:** This project studies an interaction technique shared by [monoradio (@monoradio102)](https://x.com/monoradio102) through a Claude Artifact. The code was independently implemented with Codex and does not copy the Artifact's source.
|
| 23 |
+
|
| 24 |
+
## Source and local use
|
| 25 |
+
|
| 26 |
+
The canonical source is [boryzoo/orbit-video-viewer on GitHub](https://github.com/boryzoo/orbit-video-viewer). Use GitHub for the editable source, installation documentation, version history, and [issue tracking](https://github.com/boryzoo/orbit-video-viewer/issues). This Space is the live demonstration of the same project.
|
| 27 |
+
|
| 28 |
+
For a local source checkout:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
pnpm install
|
| 32 |
+
pnpm dev
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
The verified release target is Chromium with H.264 MP4. Safari and Firefox support remains pending the same manual interaction matrix. This is controlled viewpoint playback rather than real 3D; it cannot reveal angles absent from the source video.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
# Orbit Video Viewer(中文)
|
| 40 |
+
|
| 41 |
+
**把预先生成的环绕视频变成可拖动控制视角的网页交互,不需要 3D 模型。**
|
| 42 |
+
|
| 43 |
+
上方的 Orbit Comic 是在线演示。请在画面中水平拖动,浏览 MP4 已经包含的不同观察视角。使用 **Change video** 可以尝试自己的本地 MP4;文件只在浏览器内处理,不会由这个应用上传。
|
| 44 |
+
|
| 45 |
+
可复用的开源项目包含 `OrbitViewer`、浏览器端 MP4 处理、公开控制 API、供 AI 编程工具使用的接入 Skill,以及一个独立消费示例。
|
| 46 |
+
|
| 47 |
+
> **技术灵感署名:** 本项目研究了 [monoradio(@monoradio102)](https://x.com/monoradio102) 通过 Claude Artifact 分享的交互思路。代码由 Codex 独立实现,没有复制该 Artifact 的源码。
|
| 48 |
+
|
| 49 |
+
## 源码与本地运行
|
| 50 |
+
|
| 51 |
+
项目的唯一源码基准是 GitHub 上的 [boryzoo/orbit-video-viewer](https://github.com/boryzoo/orbit-video-viewer)。可编辑源码、安装说明和版本历史以 GitHub 为准;问题可提交到 [GitHub Issues](https://github.com/boryzoo/orbit-video-viewer/issues)。这个 Space 是同一项目的在线演示入口。
|
| 52 |
+
|
| 53 |
+
本地源码运行方式:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
pnpm install
|
| 57 |
+
pnpm dev
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
当前已经验证的发布目标是 Chromium + H.264 MP4;Safari 与 Firefox 尚待完成同等手动交互测试。这是受控的视角回放,不是真实 3D,不能生成源视频中不存在的角度。
|
| 61 |
+
|
| 62 |
+
## License / 许可
|
| 63 |
+
|
| 64 |
+
Software is MIT licensed. Demo media has separate terms documented in `ASSETS.md`. 技术代码使用 MIT License;演示媒体的独立条款见 `ASSETS.md`。
|
README.zh-CN.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="README.md">English</a> · <strong>简体中文</strong></p>
|
| 2 |
+
|
| 3 |
+
<p align="center">
|
| 4 |
+
<img src="recording-comic/output/orbit-comic-demo.gif" alt="三格漫画演示:拖动指示器移动时,主画面的观察视角随之变化" width="720">
|
| 5 |
+
</p>
|
| 6 |
+
|
| 7 |
+
<h1 align="center">Orbit Video Viewer</h1>
|
| 8 |
+
|
| 9 |
+
<p align="center"><strong>把预先生成的环绕视频变成可拖动控制视角的网页交互,不需要 3D 模型。</strong></p>
|
| 10 |
+
|
| 11 |
+
<p align="center">
|
| 12 |
+
<a href="#体验-demo">体验 Demo</a> ·
|
| 13 |
+
<a href="#通过-ai-编程工具使用-skill">使用 Skill</a> ·
|
| 14 |
+
<a href="#直接使用-sdk">使用 SDK</a>
|
| 15 |
+
</p>
|
| 16 |
+
|
| 17 |
+
Orbit Comic 是仓库内附的演示作品;这个开源项目的主体是其下方可复用的交互能力:载入一段环绕 MP4,让用户通过拖动浏览不同视角,读取或设置当前角度,并把它嵌入自己的产品。
|
| 18 |
+
|
| 19 |
+
> **技术灵感署名:** 本项目研究了 [monoradio(@monoradio102)](https://x.com/monoradio102) 通过 Claude Artifact 分享的交互思路。代码由 Codex 独立实现,没有复制该 Artifact 的源码。
|
| 20 |
+
|
| 21 |
+
## 可以用它做什么?
|
| 22 |
+
|
| 23 |
+
<table>
|
| 24 |
+
<tr>
|
| 25 |
+
<td width="33%"><img src="docs/assets/concepts/product-viewer.jpg" alt="可拖动商品查看器概念图"></td>
|
| 26 |
+
<td width="33%"><img src="docs/assets/concepts/game-item-inspection.jpg" alt="游戏道具多视角检查概念图"></td>
|
| 27 |
+
<td width="33%"><img src="docs/assets/concepts/interactive-comic.jpg" alt="互动漫画视角探索概念图"></td>
|
| 28 |
+
</tr>
|
| 29 |
+
<tr>
|
| 30 |
+
<td align="center"><strong>商品查看器</strong></td>
|
| 31 |
+
<td align="center"><strong>游戏道具检查</strong></td>
|
| 32 |
+
<td align="center"><strong>互动漫画</strong></td>
|
| 33 |
+
</tr>
|
| 34 |
+
</table>
|
| 35 |
+
|
| 36 |
+
这些图片只用于说明可能的应用方向,不是仓库内已经完成的独立产品。同一种交互也可以用于展览、角色展示、教育、建筑和互动艺术。
|
| 37 |
+
|
| 38 |
+
## 开始使用
|
| 39 |
+
|
| 40 |
+
### 通过 AI 编程工具使用 Skill
|
| 41 |
+
|
| 42 |
+
[`integrate-orbit-video` Skill](skills/integrate-orbit-video/SKILL.md) 会指导 AI 编程工具把 SDK 和你的 MP4 接入现有网页项目,同时尽量保留原项目的视觉设计。
|
| 43 |
+
|
| 44 |
+
最简单的使用方式是下载本仓库,在 AI 编程工具中打开你自己的项目,并向工具提供三个路径:你的项目、你的 MP4 和这个 Skill 文件。然后输入:
|
| 45 |
+
|
| 46 |
+
```text
|
| 47 |
+
使用 integrate-orbit-video Skill,把我的环绕 MP4 接入这个项目。
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
如果需要反复使用,可以把整个 `skills/integrate-orbit-video` 文件夹安装到工具的 Skill 目录。Claude Code 项目级 Skill 可放在 `.claude/skills/integrate-orbit-video/`;兼容 Codex 的工具可以使用其本地或项目 Skill 目录,也可以直接读取仓库中的 `SKILL.md`。
|
| 51 |
+
|
| 52 |
+
这个 Skill 采用 OpenAI 与 Claude Code 所描述的开放 `SKILL.md` Agent Skills 格式。它已经在 Codex 工作流中完成真实接入测试;其他工具是否能自动发现和调用,取决于其具体版本与配置。详见 [Skill 验证记录](docs/SKILL_VALIDATION.md)。
|
| 53 |
+
|
| 54 |
+
### 直接使用 SDK
|
| 55 |
+
|
| 56 |
+
npm 包尚未公开发布。当前可先构建本仓库,再通过本地文件依赖安装:
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
pnpm install
|
| 60 |
+
pnpm build:library
|
| 61 |
+
pnpm add /absolute/path/to/orbit-panel-viewer
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
```html
|
| 65 |
+
<div id="orbit-view" tabindex="0"></div>
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
```css
|
| 69 |
+
#orbit-view {
|
| 70 |
+
aspect-ratio: 16 / 9;
|
| 71 |
+
overflow: hidden;
|
| 72 |
+
cursor: grab;
|
| 73 |
+
touch-action: pan-y;
|
| 74 |
+
user-select: none;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
#orbit-view canvas { display: block; width: 100%; height: 100%; }
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
```ts
|
| 81 |
+
import { OrbitViewer, createMp4Source } from 'orbit-video-viewer';
|
| 82 |
+
|
| 83 |
+
const element = document.querySelector<HTMLElement>('#orbit-view');
|
| 84 |
+
if (!element) throw new Error('Missing orbit view.');
|
| 85 |
+
|
| 86 |
+
const viewer = new OrbitViewer({ element, initialAngle: 0 });
|
| 87 |
+
await viewer.load(createMp4Source('/assets/orbit.mp4'));
|
| 88 |
+
|
| 89 |
+
viewer.setAngle(90, { animate: true });
|
| 90 |
+
viewer.reset();
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
更多内容见[接入指南](docs/INTEGRATION.md)、[公开 API](#公开-api)和独立的 [`basic-integration`](examples/basic-integration) 消费项目。
|
| 94 |
+
|
| 95 |
+
## 公开 API
|
| 96 |
+
|
| 97 |
+
| API | 用途 |
|
| 98 |
+
|---|---|
|
| 99 |
+
| `createMp4Source(urlOrFile)` | 载入 MP4 URL、`File` 或 `Blob` |
|
| 100 |
+
| `load(source, options?)` | 准备并显示素材 |
|
| 101 |
+
| `getAngle()` / `getState()` | 读取当前视图状态 |
|
| 102 |
+
| `setAngle(degrees, options?)` | 移动到指定视角 |
|
| 103 |
+
| `rotateBy(degrees, options?)` | 相对当前视角旋转 |
|
| 104 |
+
| `reset(options?)` | 回到当前视频的初始视角 |
|
| 105 |
+
| `setInteractionEnabled(boolean)` | 启用或禁用用户输入 |
|
| 106 |
+
| `destroy()` | 释放资源和事件监听 |
|
| 107 |
+
|
| 108 |
+
通用 `OrbitSource` 接口把 Viewer 与解码逻辑分开。当前版本只实现浏览器端 MP4 处理,不包含预处理工具。
|
| 109 |
+
|
| 110 |
+
## 源视频要求
|
| 111 |
+
|
| 112 |
+
建议使用 H.264 MP4:画面应是一段连续的水平环绕运动,主体大小和相机高度保持稳定,不要切镜;如果希望首尾循环,结束视角应尽量接近开场视角。
|
| 113 |
+
|
| 114 |
+
这是一种受控的视角回放,不是真实 3D。它不能生成视频中不存在的角度、改变几何结构、提供碰撞或重新打光。MP4 中的音频会被忽略。
|
| 115 |
+
|
| 116 |
+
## 体验 Demo
|
| 117 |
+
|
| 118 |
+
不要直接双击 `index.html`。请启动开发服务器:
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
pnpm install
|
| 122 |
+
pnpm dev
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
然后打开 Vite 输出的本地地址。使用 **Change video** 可以尝试自己的 MP4;**Reset view** 会回到当前已载入视频的初始视角。
|
| 126 |
+
|
| 127 |
+
## 项目结构
|
| 128 |
+
|
| 129 |
+
```text
|
| 130 |
+
src/core/ 内部运动、帧、缓存和素材契约
|
| 131 |
+
src/library/ 对外 OrbitViewer SDK
|
| 132 |
+
src/demo/ Orbit Comic 展示界面
|
| 133 |
+
skills/integrate-orbit-video AI 辅助接入流程
|
| 134 |
+
examples/basic-integration 独立 SDK 消费项目
|
| 135 |
+
public/demo/ 内置 Orbit Comic 媒体
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
## 文档
|
| 139 |
+
|
| 140 |
+
- [English README](README.md)
|
| 141 |
+
- [接入指南](docs/INTEGRATION.md)
|
| 142 |
+
- [技术报告](docs/TECHNICAL_REPORT.md)
|
| 143 |
+
- [浏览器兼容性](docs/COMPATIBILITY.md)
|
| 144 |
+
- [来源与署名](docs/ORIGIN_AND_ATTRIBUTION.md)
|
| 145 |
+
- [验证记录](VALIDATION.md)
|
| 146 |
+
- [发布清单](docs/RELEASE_CHECKLIST.md)
|
| 147 |
+
- [发布执行手册](docs/PUBLISHING_RUNBOOK.zh-CN.md)
|
| 148 |
+
- [产品与发布决策](docs/PROJECT_AND_PUBLISHING.zh-CN.md)
|
| 149 |
+
- [Hugging Face Space README 模板](README.huggingface.md)
|
| 150 |
+
|
| 151 |
+
## 许可
|
| 152 |
+
|
| 153 |
+
Library、Demo 源码和 Skill 使用 [MIT License](LICENSE)。Demo 与 README 媒体采用单独条款,见 [ASSETS.md](ASSETS.md);第三方软件见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party notices
|
| 2 |
+
|
| 3 |
+
The distributed Library has one runtime dependency:
|
| 4 |
+
|
| 5 |
+
| Package | Version checked | License | Purpose |
|
| 6 |
+
|---|---:|---|---|
|
| 7 |
+
| `mp4box` | 2.4.1 | BSD-3-Clause | MP4 parsing and encoded sample extraction |
|
| 8 |
+
|
| 9 |
+
Development and validation use:
|
| 10 |
+
|
| 11 |
+
| Package | Version checked | License |
|
| 12 |
+
|---|---:|---|
|
| 13 |
+
| `vite` | 8.3.4 | MIT |
|
| 14 |
+
| `vitest` | 5.0.3 | MIT |
|
| 15 |
+
| `typescript` | 7.0.2 | Apache-2.0 |
|
| 16 |
+
|
| 17 |
+
These entries were checked against the installed packages' own `package.json` metadata on 2026-10-12. Their licenses remain their authors' licenses; the repository's MIT License does not replace them.
|
VALIDATION.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# V0.3 验证记录
|
| 2 |
+
|
| 3 |
+
验证日期:2026-10-12(Asia/Shanghai)
|
| 4 |
+
|
| 5 |
+
## 本次验收范围
|
| 6 |
+
|
| 7 |
+
- Core、Library、Demo 已明确分层;
|
| 8 |
+
- Orbit Comic 改为通过公开 Library API 运行;
|
| 9 |
+
- Library 可以生成独立 ES Module 和 TypeScript 类型声明;
|
| 10 |
+
- 独立项目通过本地包依赖安装 Library,没有直接引用主项目 `src/`;
|
| 11 |
+
- 没有实现预处理工具或其他超出范围的扩展。
|
| 12 |
+
- 新增基于同一 Library 的 `integrate-orbit-video` Skill;
|
| 13 |
+
- 完成 Skill 驱动的全新临时项目接入验收;
|
| 14 |
+
- README 已按“作品效果 → 使用想象 → Skill / SDK”重新组织;
|
| 15 |
+
- 完成依赖许可、参考来源和浏览器支持范围检查。
|
| 16 |
+
|
| 17 |
+
## 测试素材
|
| 18 |
+
|
| 19 |
+
- `public/demo/video_01.mp4`;
|
| 20 |
+
- H.264,852 × 480,约 10.05 秒,约 14 MB;
|
| 21 |
+
- 视频包含 AAC 音轨,但 Library 只使用视频轨;
|
| 22 |
+
- 内容经过正面、侧面、背面、另一侧并返回正面。
|
| 23 |
+
|
| 24 |
+
## 自动检查
|
| 25 |
+
|
| 26 |
+
- TypeScript 严格检查:通过;
|
| 27 |
+
- Vitest:1 个文件、4 个用例通过;
|
| 28 |
+
- Orbit Comic Demo 生产构建:通过;
|
| 29 |
+
- Library ES Module 构建:通过;
|
| 30 |
+
- Library TypeScript 类型声明构建:通过;
|
| 31 |
+
- npm tarball 内容检查:通过;
|
| 32 |
+
- `examples/basic-integration` 独立项目 TypeScript 检查与 Vite 构建:通过。
|
| 33 |
+
- Skill 官方结构验证器:通过;
|
| 34 |
+
- Skill 临时消费项目安装、TypeScript 与 Vite 生产构建:通过。
|
| 35 |
+
|
| 36 |
+
Library 构建结果:
|
| 37 |
+
|
| 38 |
+
- `dist/library/orbit-viewer.js`:约 15.8 KB,gzip 后约 5.1 KB;
|
| 39 |
+
- `dist/types/`:公开 API 及其依赖类型声明。
|
| 40 |
+
|
| 41 |
+
## 独立项目浏览器实测
|
| 42 |
+
|
| 43 |
+
独立项目位于 `examples/basic-integration`,拥有自己的 `package.json`,通过 `file:../..` 安装构建后的 `orbit-video-viewer` 包。
|
| 44 |
+
|
| 45 |
+
实测结果:
|
| 46 |
+
|
| 47 |
+
- MP4 成功加载并显示第 0 帧;
|
| 48 |
+
- `onChange` 正确报告 `0° · frame 0`;
|
| 49 |
+
- `setAngle(90)` 到达 `90° · frame 31`;
|
| 50 |
+
- `setAngle(180)` 到达 `180° · frame 62`;
|
| 51 |
+
- `reset()` 返回 `0° · frame 0`;
|
| 52 |
+
- 鼠标拖动后角度与帧持续变化;
|
| 53 |
+
- 浏览器控制台无 warning、无 error。
|
| 54 |
+
|
| 55 |
+
## Orbit Comic 回归实测
|
| 56 |
+
|
| 57 |
+
- 默认沙漠作品成功加载;
|
| 58 |
+
- 原页面视觉、标题、署名、Change video 和 Reset view 保留;
|
| 59 |
+
- 拖动正常;
|
| 60 |
+
- Reset view 返回当前视频的初始视角;
|
| 61 |
+
- 浏览器控制台无 warning、无 error。
|
| 62 |
+
|
| 63 |
+
## Skill 独立接入实测
|
| 64 |
+
|
| 65 |
+
- 在系统临时目录新建了独立 Vite / TypeScript 项目;
|
| 66 |
+
- 项目从 npm tarball 安装 `orbit-video-viewer@0.1.0`,没有引用主仓库 `src/`;
|
| 67 |
+
- 页面采用与 Orbit Comic 不同的“Relic Study”视觉,不复制 Demo UI;
|
| 68 |
+
- MP4 加载成功,拖动后角度发生变化;
|
| 69 |
+
- Reset view 返回当前视频的 `0°` 初始视角;
|
| 70 |
+
- 控制台无 warning、无 error;
|
| 71 |
+
- 详细步骤见 `docs/SKILL_VALIDATION.md`。
|
| 72 |
+
|
| 73 |
+
## 已知边界
|
| 74 |
+
|
| 75 |
+
- 已验证的快速路径是 H.264 MP4,不代表所有 MP4 编码都已验证;
|
| 76 |
+
- `video fallback` 已保留,但本轮没有强制关闭 WebCodecs 单独覆盖该路径;
|
| 77 |
+
- 当前只实现浏览器即时处理 MP4;
|
| 78 |
+
- 已添加通用 `OrbitSource` 接口,但没有实现预处理工具或图片清单加载;
|
| 79 |
+
- 首尾连续性和角色一致性仍由源视频决定;
|
| 80 |
+
- Library 尚未发布到 npm,本轮验证使用本地构建包。
|
| 81 |
+
- Skill 使用开放的 `SKILL.md` 格式并通过结构验证,但目前只在 Codex 执行流程中完成真实接入测试;Claude Code 的目录兼容性有官方依据,尚未在本机 Claude Code 中运行同一验收。
|
| 82 |
+
- Safari 和 Firefox 尚未完成与 Chromium 相同的手动交互矩阵,因此 README 不宣称它们已经完整验证。
|
docs/COMPATIBILITY.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Browser compatibility
|
| 2 |
+
|
| 3 |
+
## What has been verified
|
| 4 |
+
|
| 5 |
+
- The Demo and independent integration project were exercised in a Chromium-based browser.
|
| 6 |
+
- The WebCodecs H.264 path loaded the bundled 852 × 480 MP4, supported dragging, angle controls, reset, and cleanup without console errors.
|
| 7 |
+
- The package build and TypeScript declarations were consumed by a separate Vite project.
|
| 8 |
+
|
| 9 |
+
## Runtime requirements
|
| 10 |
+
|
| 11 |
+
The Viewer needs JavaScript modules, Canvas, Pointer Events, `fetch`, `Blob`, and normal browser media support. The fast path also needs WebCodecs and H.264 decoding.
|
| 12 |
+
|
| 13 |
+
The code feature-detects the fast path and includes an offscreen `<video>` fallback. That fallback still depends on the browser being able to play the source codec.
|
| 14 |
+
|
| 15 |
+
## Honest support statement
|
| 16 |
+
|
| 17 |
+
Chromium with H.264 MP4 is the verified target for this release. Safari and Firefox are intended to use the feature-detected paths, but they have not yet completed the same manual browser matrix. Until that testing is done, they should be described as expected rather than guaranteed support.
|
| 18 |
+
|
| 19 |
+
The Skill does not change these runtime requirements. It integrates the same SDK into another browser project.
|
docs/INTEGRATION.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Library integration guide
|
| 2 |
+
|
| 3 |
+
Orbit Comic is the Demo. External projects should use the `orbit-video-viewer` Library instead of copying the Demo page or its internal files.
|
| 4 |
+
|
| 5 |
+
## Build the local package
|
| 6 |
+
|
| 7 |
+
The first release is not yet published to npm:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
pnpm install
|
| 11 |
+
pnpm build:library
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
In another project, add a local file dependency:
|
| 15 |
+
|
| 16 |
+
```json
|
| 17 |
+
{
|
| 18 |
+
"dependencies": {
|
| 19 |
+
"orbit-video-viewer": "file:../path/to/orbit-panel-viewer"
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
Run that project's normal package installation after the Library has been built.
|
| 25 |
+
|
| 26 |
+
## Minimal example
|
| 27 |
+
|
| 28 |
+
```html
|
| 29 |
+
<div id="viewer" tabindex="0"></div>
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
```css
|
| 33 |
+
#viewer {
|
| 34 |
+
aspect-ratio: 16 / 9;
|
| 35 |
+
overflow: hidden;
|
| 36 |
+
cursor: grab;
|
| 37 |
+
touch-action: pan-y;
|
| 38 |
+
user-select: none;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
#viewer canvas { width: 100%; height: 100%; display: block; }
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
```ts
|
| 45 |
+
import { OrbitViewer, createMp4Source } from 'orbit-video-viewer';
|
| 46 |
+
|
| 47 |
+
const element = document.querySelector<HTMLElement>('#viewer')!;
|
| 48 |
+
|
| 49 |
+
const viewer = new OrbitViewer({
|
| 50 |
+
element,
|
| 51 |
+
initialAngle: 0,
|
| 52 |
+
onProgress(progress) {
|
| 53 |
+
console.log(`Loading ${Math.round(progress * 100)}%`);
|
| 54 |
+
},
|
| 55 |
+
onChange(state) {
|
| 56 |
+
console.log(`${state.angle.toFixed(0)}°`, state.frame);
|
| 57 |
+
},
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
await viewer.load(createMp4Source('/assets/orbit.mp4'), {
|
| 61 |
+
frameCount: 124,
|
| 62 |
+
width: 852,
|
| 63 |
+
height: 480,
|
| 64 |
+
quality: 0.9,
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
viewer.setAngle(90, { animate: true });
|
| 68 |
+
viewer.rotateBy(-45);
|
| 69 |
+
viewer.reset({ animate: true });
|
| 70 |
+
|
| 71 |
+
window.addEventListener('beforeunload', () => viewer.destroy());
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
`OrbitViewer` creates a Canvas inside the supplied element when a Canvas is not provided. An existing Canvas can be passed with the `canvas` option.
|
| 75 |
+
|
| 76 |
+
## Loading a visitor's file
|
| 77 |
+
|
| 78 |
+
```ts
|
| 79 |
+
const file = fileInput.files?.[0];
|
| 80 |
+
if (file) await viewer.load(createMp4Source(file));
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
The file stays in that browser session and is not uploaded by the Library.
|
| 84 |
+
|
| 85 |
+
## Connecting product behavior
|
| 86 |
+
|
| 87 |
+
Use `onChange` to connect viewpoint changes to the surrounding product:
|
| 88 |
+
|
| 89 |
+
```ts
|
| 90 |
+
const viewer = new OrbitViewer({
|
| 91 |
+
element,
|
| 92 |
+
onChange({ angle }) {
|
| 93 |
+
if (angle >= 80 && angle <= 100) showDetails('left-side');
|
| 94 |
+
else hideDetails();
|
| 95 |
+
},
|
| 96 |
+
});
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
This can drive product labels, game events, sounds, subtitles, or educational annotations. Those features remain in the host product rather than the Library.
|
| 100 |
+
|
| 101 |
+
## Controls
|
| 102 |
+
|
| 103 |
+
```ts
|
| 104 |
+
viewer.getAngle();
|
| 105 |
+
viewer.getState();
|
| 106 |
+
viewer.setAngle(180);
|
| 107 |
+
viewer.setAngle(180, { animate: true, duration: 500 });
|
| 108 |
+
viewer.rotateBy(30);
|
| 109 |
+
viewer.reset();
|
| 110 |
+
viewer.setInteractionEnabled(false);
|
| 111 |
+
viewer.destroy();
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## Independent example
|
| 115 |
+
|
| 116 |
+
[`examples/basic-integration`](../examples/basic-integration) is a separate Vite project with its own `package.json`. It installs the built Library through a file dependency rather than importing `src/` directly.
|
| 117 |
+
|
| 118 |
+
It verifies:
|
| 119 |
+
|
| 120 |
+
- package exports and TypeScript declarations;
|
| 121 |
+
- MP4 loading from a normal consumer application;
|
| 122 |
+
- automatic Canvas creation;
|
| 123 |
+
- drag interaction;
|
| 124 |
+
- `setAngle()` for front, side, and back views;
|
| 125 |
+
- angle/frame reporting;
|
| 126 |
+
- `reset()` and `destroy()`.
|
| 127 |
+
|
| 128 |
+
Run it with:
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
pnpm --dir examples/basic-integration install
|
| 132 |
+
pnpm --dir examples/basic-integration dev
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
Or run the repeatable build check from the repository root:
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
pnpm test:integration
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Future source types
|
| 142 |
+
|
| 143 |
+
The public `OrbitSource` interface separates the Viewer from MP4 decoding. A future source may load preprocessed image frames, but no preprocessing command or manifest loader is implemented in this release.
|
docs/ORIGIN_AND_ATTRIBUTION.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Origin and attribution
|
| 2 |
+
|
| 3 |
+
## Technique inspiration
|
| 4 |
+
|
| 5 |
+
The project was inspired by an interaction technique shared publicly by [monoradio (@monoradio102)](https://x.com/monoradio102) through a Claude Artifact.
|
| 6 |
+
|
| 7 |
+
The repository does not include, translate, or adapt the Artifact's source code. The implementation was written independently with Codex after studying the visible behavior and underlying technique.
|
| 8 |
+
|
| 9 |
+
Because the reference Artifact's code license has not been established here, none of its code is treated as a dependency or redistributable source. Attribution is included for the technical inspiration, not as a claim that its code is licensed under this repository's MIT License.
|
| 10 |
+
|
| 11 |
+
## Code and media
|
| 12 |
+
|
| 13 |
+
- Repository software and the integration Skill: MIT License.
|
| 14 |
+
- Orbit Comic Demo image and video: created by the repository owner with Jimeng AI; terms are listed in `ASSETS.md`.
|
| 15 |
+
- README concept images: generated for this repository with OpenAI ImageGen; terms are listed in `ASSETS.md`.
|
| 16 |
+
|
| 17 |
+
## Tooling
|
| 18 |
+
|
| 19 |
+
Codex was used to implement, test, document, and package the project. Tool assistance does not change the repository's license boundaries.
|
docs/PROJECT_AND_PUBLISHING.zh-CN.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Orbit Video Viewer:产品与发布决策
|
| 2 |
+
|
| 3 |
+
这份文档记录当前已经确定的产品规则,以及 GitHub、Hugging Face 的发布分工。以后继续设计或发布时,以这里为共同基准,不必依赖聊天记录。
|
| 4 |
+
|
| 5 |
+
## 1. 产品定位
|
| 6 |
+
|
| 7 |
+
开源项目的主体是 **Orbit Video Viewer**:一种可以集成到游戏、商品展示、互动叙事、教育内容和其他网页中的浏览器 Library。
|
| 8 |
+
|
| 9 |
+
它把预生成的环绕视角视频转换成可实时操控的观察视图,不要求真实 3D 模型或 3D 引擎。
|
| 10 |
+
|
| 11 |
+
**Orbit Comic** 是面向普通访问者的演示作品,不是开源项目的全部。
|
| 12 |
+
|
| 13 |
+
- 页面首先服务普通访问者:打开、看见画面、拖动并发现视角变化。
|
| 14 |
+
- 技术人员看到同一件简洁的作品;架构、实现原理和部署说明放在 README 与技术文章中。
|
| 15 |
+
- 页面只保留作品身份、必要操作、克制的加载和错误反馈。
|
| 16 |
+
- 不在主界面解释 WebCodecs、抽帧、缓存、服务器架构等技术细节。
|
| 17 |
+
|
| 18 |
+
### 三个层次
|
| 19 |
+
|
| 20 |
+
- **Core**:角度、帧映射、惯性、缓存、Canvas 渲染和素材接口等内部能力;
|
| 21 |
+
- **Library**:开发者直接使用的 `OrbitViewer`、`createMp4Source()` 和公开控制方法;
|
| 22 |
+
- **Demo**:Orbit Comic 的标题、按钮、样式、加载界面和沙漠素材。
|
| 23 |
+
|
| 24 |
+
对外提供两个使用入口,但底层不是两套技术:
|
| 25 |
+
|
| 26 |
+
- **Skill**:给不熟悉开发的人使用。用户把项目文件夹和环绕视频交给 Codex、Claude Code 等兼容工具,Skill 指导 AI 调用 Library 完成接入;
|
| 27 |
+
- **SDK / Library**:给开发者直接安装和编程使用。
|
| 28 |
+
|
| 29 |
+
Skill 只是“如何使用 SDK”的操作说明,不复制、不替代 Core 或 Library。当前真实接入测试已在 Codex 工作流中通过;Claude Code 支持相同的 `SKILL.md` 开放格式和项目级 `.claude/skills/` 目录,但仍应如实区分“格式兼容”和“已经在该工具中实测”。
|
| 30 |
+
|
| 31 |
+
GitHub 使用英文 `README.md` 与中文 `README.zh-CN.md`,并在顶部互相切换。GitHub README 优先介绍 Library 的用途、集成方法、公开接口、素材要求和技术边界。Hugging Face Space 展示 Orbit Comic;技术报告解释具体实现原理。
|
| 32 |
+
|
| 33 |
+
### README 浏览顺序
|
| 34 |
+
|
| 35 |
+
README 应按以下顺序组织:
|
| 36 |
+
|
| 37 |
+
1. 首先看到 Orbit Comic 的拖动效果动图和一句核心说明;
|
| 38 |
+
2. 看到商品查看、游戏道具检查、互动漫画三种用途概念图;
|
| 39 |
+
3. 再选择通过 Skill 或 SDK 开始使用;
|
| 40 |
+
4. API、素材要求、技术边界和详细原理放在后面。
|
| 41 |
+
|
| 42 |
+
三张用途图只表达想象空间,必须写明不是已经完成的独立产品。
|
| 43 |
+
|
| 44 |
+
### 技术来源声明
|
| 45 |
+
|
| 46 |
+
本项目学习的交互技术思路来自 [monoradio(@monoradio102)](https://x.com/monoradio102) 发布在 Claude Artifact 上的作品。页面头部和 README 都必须保留这一来源声明。
|
| 47 |
+
|
| 48 |
+
当前项目没有沿用或复制原 Artifact 的源码、HTML 或 CSS,而是理解“环绕视频 → 浏览器抽帧 → 拖动控制帧序号 → Canvas 显示”这条路线后,使用 Codex 独立重新实现。若以后取得原 Artifact 的直接网址,应在现有作者主页链接之外补充该直达链接。
|
| 49 |
+
|
| 50 |
+
## 2. 当前界面与交互语义
|
| 51 |
+
|
| 52 |
+
### Reset view
|
| 53 |
+
|
| 54 |
+
界面使用 **Reset view**,其无障碍说明是 “Return to the original view”。
|
| 55 |
+
|
| 56 |
+
它只重置**当前已加载视频**的视角:
|
| 57 |
+
|
| 58 |
+
- 默认沙漠视频中,回到沙漠视频的初始视角;
|
| 59 |
+
- 用户换成自己的视频后,回到该视频的初始视角;
|
| 60 |
+
- 它不会重新加载默认沙漠视频,也不会撤销换视频操作。
|
| 61 |
+
|
| 62 |
+
如果将来需要恢复内置作品,应另设语义完全不同的操作,例如 **Restore original artwork**,不能让 Reset view 同时承担两种职责。
|
| 63 |
+
|
| 64 |
+
### Change video
|
| 65 |
+
|
| 66 |
+
**Change video** 是低频的“更换作品”操作,不与高频的视角重置并排。它位于画面下方的作品说明区域。
|
| 67 |
+
|
| 68 |
+
用户选择的文件:
|
| 69 |
+
|
| 70 |
+
- 只在本机浏览器内读取和抽帧;
|
| 71 |
+
- 不上传服务器、不写入项目,也不会永久保存;
|
| 72 |
+
- 刷新或关闭页面后消失;
|
| 73 |
+
- 更换成功后成为当前作品,Reset view 以它为准。
|
| 74 |
+
|
| 75 |
+
### 标题规则
|
| 76 |
+
|
| 77 |
+
- 内置演示作品保留专属标题 **Desert Standoff**。
|
| 78 |
+
- 用户载入本地视频后,标题改为中性的 **Untitled Orbit**。
|
| 79 |
+
- 原始文件名作为较小的作品信息显示,长文件名会被省略显示,但完整文件名保留在悬停提示中。
|
| 80 |
+
|
| 81 |
+
这样既保留沙漠作品的个性,也不会把它的名字错误套用到访问者自己的内容上。未来如果增加作品命名功能,可允许用户直接编辑 “Untitled Orbit”,但当前版本不需要增加这一层操作。
|
| 82 |
+
|
| 83 |
+
## 3. GitHub 发布什么
|
| 84 |
+
|
| 85 |
+
GitHub 是项目的**唯一源代码基准(source of truth)**。公开仓库包含:
|
| 86 |
+
|
| 87 |
+
- 可编辑的完整源代码;
|
| 88 |
+
- README、验证记录和技术文档;
|
| 89 |
+
- 测试、依赖清单与锁文件;
|
| 90 |
+
- 构建和本地运行方法;
|
| 91 |
+
- 授权文件;
|
| 92 |
+
- 当前演示媒��(前提是先确定媒体授权)。
|
| 93 |
+
|
| 94 |
+
不提交 `node_modules/`、临时缓存、日志等生成内容。访问者既可以 `git clone`,也可以直接使用 GitHub 的 **Download ZIP** 下载源码。正式公开时建议创建 `v0.1.0` 标签和 GitHub Release,后续版本按相同方式发布。
|
| 95 |
+
|
| 96 |
+
当前视频约 14 MB,低于 GitHub 单文件 100 MiB 的阻止限制,可以作为普通 Git 文件提交。若以后演示媒体显著变大,再考虑 Git LFS 或外部媒体托管。
|
| 97 |
+
|
| 98 |
+
官方资料:
|
| 99 |
+
|
| 100 |
+
- [About releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases)
|
| 101 |
+
- [Downloading source code archives](https://docs.github.com/en/repositories/working-with-files/using-files/downloading-source-code-archives)
|
| 102 |
+
- [About large files on GitHub](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github)
|
| 103 |
+
|
| 104 |
+
## 4. Hugging Face 发布什么
|
| 105 |
+
|
| 106 |
+
Hugging Face Space 是同一项目的**在线体验展览页**,不是另一套独立产品。
|
| 107 |
+
|
| 108 |
+
- 建立公开的 Static Space;
|
| 109 |
+
- 放入与 GitHub 某个正式版本相同的源码;
|
| 110 |
+
- 将仓库中的 `README.huggingface.md` 作为 Space 根目录的 `README.md`:保留英文在前、中文在后的单文件说明,以及最顶部的 Static Space YAML;
|
| 111 |
+
- Hugging Face 根据 README 顶部配置运行 `npm run build:demo`,发布 `dist/index.html`;
|
| 112 |
+
- 不需要 GPU、后端服务器或模型推理端点;
|
| 113 |
+
- 访问者可以直接体验,也可以查看和克隆公开 Space 的代码仓库。
|
| 114 |
+
|
| 115 |
+
推荐维护方式:先在 GitHub 完成修改、测试和版本标记,再把**同一个发布提交**同步到 Hugging Face。不要分别在两个网站修改代码,否则两个版本很快会漂移。
|
| 116 |
+
|
| 117 |
+
官方资料:
|
| 118 |
+
|
| 119 |
+
- [Spaces overview](https://huggingface.co/docs/hub/en/spaces-overview)
|
| 120 |
+
- [Static HTML Spaces](https://huggingface.co/docs/hub/main/spaces-sdks-static)
|
| 121 |
+
|
| 122 |
+
## 5. 技术文章扮演什么角色
|
| 123 |
+
|
| 124 |
+
技术文章是开源项目的解释与展示,不是一个不同的软件包。文章应同时链接:
|
| 125 |
+
|
| 126 |
+
- GitHub:源码、版本、安装、贡献与授权;
|
| 127 |
+
- Hugging Face Space:无需安装即可体验的在线作品。
|
| 128 |
+
|
| 129 |
+
文章可以采用以下结构:
|
| 130 |
+
|
| 131 |
+
1. 最终交互效果与创作目标;
|
| 132 |
+
2. 为什么采用“环绕视频 → 浏览器抽帧 → 拖动映射帧序号”;
|
| 133 |
+
3. 浏览器端内存、缓存和兼容性方案;
|
| 134 |
+
4. 如何替换成自己的环绕视频;
|
| 135 |
+
5. 局限、适合的源视频条件与后续方向;
|
| 136 |
+
6. GitHub 与在线 Space 链接。
|
| 137 |
+
|
| 138 |
+
Hugging Face 的站内 Blog Articles 当前要求个人账号具有有效 PRO,或由符合条件的 Team / Enterprise 组织发布。如果账号不满足条件,可以先把技术说明写进 Space README 和 GitHub 文档,再发布到其他博客平台;这不影响 Space 本身公开展示。
|
| 139 |
+
|
| 140 |
+
官方资料:[Hugging Face Blog Articles](https://huggingface.co/docs/hub/blog-articles)
|
| 141 |
+
|
| 142 |
+
## 6. 推荐发布流程
|
| 143 |
+
|
| 144 |
+
1. 确认演示视频和海报的公开授权;
|
| 145 |
+
2. 运行 `pnpm check:release`,完成测试、生产构建、独立消费项目和 npm 包内容检查;
|
| 146 |
+
3. 补入最终 GitHub、主页和问题反馈 URL;
|
| 147 |
+
4. 在 GitHub 建立公开仓库并推送源码;
|
| 148 |
+
5. 打 `v0.1.0` 标签并创建 GitHub Release;
|
| 149 |
+
6. 把同一发布提交同步到 Hugging Face Static Space,并把 `README.huggingface.md` 用作 Space 根 `README.md`;
|
| 150 |
+
7. 验证线上拖动、Reset view、Change video、移动端布局和错误提示;
|
| 151 |
+
8. 发布技术文章,并互相链接 GitHub 与 Space。
|
| 152 |
+
|
| 153 |
+
### 当前发布前状态
|
| 154 |
+
|
| 155 |
+
已经完成:Library 构建、类型声明、npm tarball、独立项目安装、Skill 结构验证、Skill 全新项目接入、Chromium 浏览器交互、第三方依赖许可清单、参考技术来源说明、README 视觉重组。
|
| 156 |
+
|
| 157 |
+
GitHub 源码仓库已经发布:<https://github.com/boryzoo/orbit-video-viewer>。仓库名为 `orbit-video-viewer`,公开项目名为 **Orbit Video Viewer**。
|
| 158 |
+
|
| 159 |
+
仍待完成:
|
| 160 |
+
|
| 161 |
+
- 决定是否同时发布 npm 包;
|
| 162 |
+
- 建立 Hugging Face Static Space 并填写线上 Demo 链接;
|
| 163 |
+
- 在 Safari、Firefox 做同等手动浏览器验收;
|
| 164 |
+
- 如即梦平台条款或画面中的第三方权利存在疑问,发布前再次确认或替换素材。
|
| 165 |
+
|
| 166 |
+
在用户最终确认前,不创建公开仓库、不推送、不发布 npm 包、不建立公开 Space。
|
| 167 |
+
|
| 168 |
+
## 7. 授权边界
|
| 169 |
+
|
| 170 |
+
代码采用 MIT License。演示视频和海报由项目所有者使用即梦 AI 制作,并在权利允许的范围内采用 CC BY 4.0 开放许可;具体说明记录在 `ASSETS.md`。
|
| 171 |
+
|
| 172 |
+
代码与媒体是两套许可:
|
| 173 |
+
|
| 174 |
+
- 源代码:MIT;
|
| 175 |
+
- `public/demo/poster.png` 与 `public/demo/video_01.mp4`:CC BY 4.0;
|
| 176 |
+
- 用户自行导入的文件:仍由用户自己决定授权,不会因为使用本项目而改变许可。
|
| 177 |
+
|
| 178 |
+
若即梦平台条款或素材中包含的第三方权利对公开再分发另有限制,应以这些限制为准,并在发布前替换有疑问的素材。
|
docs/PUBLISHING_RUNBOOK.zh-CN.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Orbit Video Viewer 发布执行手册
|
| 2 |
+
|
| 3 |
+
这份手册只描述正式发布时的顺序和核对点,不授权创建公开仓库、发布 npm 包或建立 Hugging Face Space。任何对外操作都必须等仓库所有者最终确认。
|
| 4 |
+
|
| 5 |
+
## 1. 当前本地产物
|
| 6 |
+
|
| 7 |
+
| 对外表面 | 准备使用的文件 | 发布角色 |
|
| 8 |
+
|---|---|---|
|
| 9 |
+
| GitHub 英文首页 | `README.md` | 英文项目入口、SDK 与 Skill 用法 |
|
| 10 |
+
| GitHub 中文首页 | `README.zh-CN.md` | 与英文首页对应的中文入口 |
|
| 11 |
+
| GitHub 首屏动图 | `recording-comic/output/orbit-comic-demo.gif` | 三格漫画交互预览 |
|
| 12 |
+
| 视频发布素材 | `recording-comic/output/orbit-comic-demo.mp4` | 支持视频的平台或文章嵌入 |
|
| 13 |
+
| Hugging Face Space 说明 | `README.huggingface.md` | 发布时改名为 Space 根 `README.md` |
|
| 14 |
+
| Static Space 应用 | 根项目源码,由 `npm run build:demo` 生成 `dist/index.html` | 在线可操作 Demo |
|
| 15 |
+
| npm 包(若决定发布) | `orbit-video-viewer@0.1.0` | Library、类型声明与许可文件 |
|
| 16 |
+
|
| 17 |
+
`recording-comic/` 只是可复现首屏素材的录制页,不应被描述成 SDK 功能或漫画编辑器。
|
| 18 |
+
|
| 19 |
+
## 2. 发布前必须由所有者确定
|
| 20 |
+
|
| 21 |
+
1. 对外项目名与 GitHub 仓库名;
|
| 22 |
+
2. GitHub 账号或组织;
|
| 23 |
+
3. Hugging Face Space 名称与账号或组织;
|
| 24 |
+
4. 是否同步发布 npm `0.1.0`,以及 npm 包名是否继续使用 `orbit-video-viewer`;
|
| 25 |
+
5. 即梦生成媒体及画面内可识别元素是否允许按 `ASSETS.md` 所述方式公开再分发;
|
| 26 |
+
6. Safari、Firefox 是在首发前补测,还是继续明确标记为未完整验证。
|
| 27 |
+
|
| 28 |
+
这些决定会产生最终 URL。在 URL 确定前,不要在 `package.json` 中编造 `repository`、`homepage` 或 `bugs` 字段。
|
| 29 |
+
|
| 30 |
+
## 3. 本地候选版本检查
|
| 31 |
+
|
| 32 |
+
在项目根目录执行:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
pnpm install --frozen-lockfile
|
| 36 |
+
pnpm check:release
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
检查输出应同时覆盖:
|
| 40 |
+
|
| 41 |
+
- Vitest 单元测试;
|
| 42 |
+
- Demo 与 Library 生产构建;
|
| 43 |
+
- TypeScript 类型声明;
|
| 44 |
+
- `examples/basic-integration` 独立消费构建;
|
| 45 |
+
- npm tarball 文件清单。
|
| 46 |
+
|
| 47 |
+
另外手动打开普通 Demo 和 `recording-comic/?capture=1`,确认控制台无报错,三格录制页可以在独立 clone 中载入自己的 `assets/shot.png` 与 `assets/impact.png`。
|
| 48 |
+
|
| 49 |
+
## 4. GitHub 发布顺序
|
| 50 |
+
|
| 51 |
+
1. 得到最终确认后再初始化 Git,并创建公开 GitHub 仓库;
|
| 52 |
+
2. 在 `package.json` 写入真实的 `repository`、`homepage` 与 `bugs` URL;
|
| 53 |
+
3. 把 Hugging Face 在线地址补入 `README.md` 与 `README.zh-CN.md`;如果 Space 尚未建立,先保持本地 Demo 说明,不放假链接;
|
| 54 |
+
4. 重新运行本地候选版本检查;
|
| 55 |
+
5. 推送同一个通过检查的提交;
|
| 56 |
+
6. 建立 `v0.1.0` 标签和 GitHub Release;
|
| 57 |
+
7. Release 说明至少列出 Library、Demo、Skill、浏览器验证范围和已知限制。
|
| 58 |
+
|
| 59 |
+
推荐仓库 Topics:`orbit-viewer`、`interactive-video`、`canvas`、`webcodecs`、`typescript`。Topics 是发布后仓库设置,不需要写进源码。
|
| 60 |
+
|
| 61 |
+
## 5. Hugging Face Static Space 顺序
|
| 62 |
+
|
| 63 |
+
1. 建立 **Static Space**,不要建立 Dataset;
|
| 64 |
+
2. 从 GitHub 的正式发布提交同步源码,不在两个平台分别维护功能修改;
|
| 65 |
+
3. 将 `README.huggingface.md` 的占位源码段落换成真实 GitHub、Release 和问题反馈链接;
|
| 66 |
+
4. 在 Space 仓库中让该文件成为根 `README.md`,并保留最顶部 YAML;
|
| 67 |
+
5. 确认构建命令为 `npm run build:demo`,入口为 `dist/index.html`;
|
| 68 |
+
6. 记录 Space 实际对应的 Git commit SHA;
|
| 69 |
+
7. 在线测试拖动、键盘方向键、Reset view、Change video、本地文件隐私说明、窄屏布局、加载与错误反馈。
|
| 70 |
+
|
| 71 |
+
[Hugging Face 官方文档](https://huggingface.co/docs/hub/main/spaces-sdks-static)确认 Static Space 通过根 `README.md` 顶部 YAML 中的 `sdk: static` 配置,并可使用 `app_build_command` 生成由 `app_file` 指向的静态入口。
|
| 72 |
+
|
| 73 |
+
## 6. npm 可选发布
|
| 74 |
+
|
| 75 |
+
只有在所有者明确决定同时发布 npm 后才执行:
|
| 76 |
+
|
| 77 |
+
1. 确认包名可用且最终包名与 import 示例一致;
|
| 78 |
+
2. 确认 `package.json` 的 URL 字段已经是真实公开地址;
|
| 79 |
+
3. 再次运行 `pnpm pack --dry-run`,确认只包含 Library、类型声明、双语 README 和许可文件;
|
| 80 |
+
4. 在临时消费项目中安装最终 tarball并构建;
|
| 81 |
+
5. 发布 `0.1.0`,然后用公开 registry 安装再做一次最小接入验证。
|
| 82 |
+
|
| 83 |
+
如果首发不发布 npm,README 继续保留本地文件依赖方式,不把未存在的 registry 安装命令写成可用路径。
|
| 84 |
+
|
| 85 |
+
## 7. 发布完成判定
|
| 86 |
+
|
| 87 |
+
只有以下条件同时满足,才把本次发布标记为完成:
|
| 88 |
+
|
| 89 |
+
- GitHub Release、Hugging Face Space(以及可选 npm 包)来自同一候选版本;
|
| 90 |
+
- GitHub 双语 README 的语言切换、图片和内部链接有效;
|
| 91 |
+
- Space 显示英文在前、中文在后的说明,并能实际操作 Demo;
|
| 92 |
+
- monoradio 技术灵感署名、Codex 独立实现说明、即梦素材来源和许可边界均保留;
|
| 93 |
+
- 线上浏览器检查结果与文档声明一致;
|
| 94 |
+
- 没有把 `recording-comic/` 误称为 SDK 核心能力,也没有宣称存在预处理工具。
|
docs/RELEASE_CHECKLIST.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Release checklist
|
| 2 |
+
|
| 3 |
+
Last updated: 2026-10-12
|
| 4 |
+
|
| 5 |
+
## Complete
|
| 6 |
+
|
| 7 |
+
- [x] Core, Library, Demo, and Skill responsibilities are separated.
|
| 8 |
+
- [x] Unit tests pass.
|
| 9 |
+
- [x] Demo production build passes.
|
| 10 |
+
- [x] Library ES module build passes.
|
| 11 |
+
- [x] TypeScript declarations build and work in a consumer project.
|
| 12 |
+
- [x] Public exports include the Viewer, MP4 source, controls, state, and source contracts.
|
| 13 |
+
- [x] `examples/basic-integration` builds without importing repository source files.
|
| 14 |
+
- [x] npm tarball contains JavaScript, declarations, README, MIT License, and third-party notices.
|
| 15 |
+
- [x] Skill manifest passes the Skill creator validator.
|
| 16 |
+
- [x] Skill workflow was followed in a fresh project using the packed SDK.
|
| 17 |
+
- [x] Fresh Skill integration loads, drags, resets, and reports no Chromium console errors.
|
| 18 |
+
- [x] Chromium H.264 MP4 behavior is documented as the verified browser target.
|
| 19 |
+
- [x] Runtime and development dependency licenses are recorded.
|
| 20 |
+
- [x] Reference technique credit and independent-implementation boundary are recorded.
|
| 21 |
+
- [x] Demo and README media licensing is recorded.
|
| 22 |
+
- [x] README leads with the interaction, then use cases, then Skill and SDK entry points.
|
| 23 |
+
- [x] GitHub README uses the three-panel comic recording and links to a complete Chinese README.
|
| 24 |
+
- [x] Hugging Face has a separate bilingual Static Space README template with build metadata.
|
| 25 |
+
- [x] The recording page is self-contained inside the repository and its media is listed in `ASSETS.md`.
|
| 26 |
+
- [x] Package metadata declares Node.js 20+, public npm access, and both README languages in the tarball.
|
| 27 |
+
- [x] A single release-check command covers tests, both production builds, the consumer build, and package contents.
|
| 28 |
+
- [x] Technical report describes the same engine, Skill, runtime processing, and limitations.
|
| 29 |
+
|
| 30 |
+
## Before public release
|
| 31 |
+
|
| 32 |
+
- [ ] Repository owner gives final approval to publish.
|
| 33 |
+
- [x] Confirm the public project name **Orbit Video Viewer** and repository name `orbit-video-viewer`.
|
| 34 |
+
- [x] Create the local Git repository and public GitHub remote at `boryzoo/orbit-video-viewer`.
|
| 35 |
+
- [ ] Decide whether version `0.1.0` will also be published to npm.
|
| 36 |
+
- [x] Add the final `repository`, `homepage`, and issue-tracker fields to `package.json`.
|
| 37 |
+
- [ ] Replace the placeholder source paragraph in `README.huggingface.md`, then use that file as the Space repository's root `README.md`.
|
| 38 |
+
- [ ] Create the Hugging Face Static Space and add its public URL to both GitHub READMEs.
|
| 39 |
+
- [ ] Run the manual interaction matrix in Safari and Firefox, or keep their support explicitly unverified.
|
| 40 |
+
- [ ] Reconfirm that Jimeng's applicable terms and all visible media elements permit the intended CC BY 4.0 redistribution; replace the Demo media if uncertain.
|
| 41 |
+
- [ ] Add the direct Claude Artifact URL if it becomes available.
|
| 42 |
+
- [ ] Verify the deployed GitHub and Hugging Face versions come from the same release commit.
|
| 43 |
+
|
| 44 |
+
## Final local gate
|
| 45 |
+
|
| 46 |
+
Run from the repository root:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
pnpm install --frozen-lockfile
|
| 50 |
+
pnpm check:release
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Then inspect the package file list printed by `pnpm pack --dry-run`. Do not publish it yet. The owner approval, final public URLs, browser matrix, and media-rights confirmation remain human release gates.
|
| 54 |
+
|
| 55 |
+
No public repository, npm package, or Hugging Face Space should be created until the owner approves release.
|
docs/SKILL_VALIDATION.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Skill integration validation
|
| 2 |
+
|
| 3 |
+
Validation date: 2026-10-12 (Asia/Shanghai)
|
| 4 |
+
|
| 5 |
+
## Scope
|
| 6 |
+
|
| 7 |
+
This test checked whether the `integrate-orbit-video` Skill can guide a clean consumer integration rather than merely describing the existing Demo.
|
| 8 |
+
|
| 9 |
+
The test project was created in a new temporary directory. It did not import repository source files or copy the Orbit Comic Demo interface.
|
| 10 |
+
|
| 11 |
+
## Procedure
|
| 12 |
+
|
| 13 |
+
1. Built and packed `orbit-video-viewer@0.1.0` as an npm tarball.
|
| 14 |
+
2. Created a new Vite and TypeScript project with its own visual design.
|
| 15 |
+
3. Installed the tarball as a normal dependency.
|
| 16 |
+
4. Copied an orbit MP4 into the consumer project's public assets.
|
| 17 |
+
5. Added a viewer container, minimal integration CSS, `OrbitViewer`, `createMp4Source()`, progress feedback, a Reset control, and cleanup.
|
| 18 |
+
6. Ran the consumer TypeScript check and production build.
|
| 19 |
+
7. Opened the consumer project in a Chromium-based browser and tested loading, dragging, reset, and console output.
|
| 20 |
+
|
| 21 |
+
## Result
|
| 22 |
+
|
| 23 |
+
- Package installation: passed.
|
| 24 |
+
- Public package exports: passed.
|
| 25 |
+
- TypeScript declarations: passed.
|
| 26 |
+
- Consumer production build: passed.
|
| 27 |
+
- Initial MP4 render: passed.
|
| 28 |
+
- Horizontal drag changed the reported angle: passed.
|
| 29 |
+
- Reset returned to `0°`: passed.
|
| 30 |
+
- Browser warnings and errors: none.
|
| 31 |
+
|
| 32 |
+
The initial consumer build reported a missing declaration for the CSS side-effect import under TypeScript 7. The host project was corrected by adding Vite's client type reference; no SDK source change was needed.
|
| 33 |
+
|
| 34 |
+
## What this proves
|
| 35 |
+
|
| 36 |
+
The Skill instructions are sufficient for a coding agent to integrate the built SDK into a separate browser project and handle a normal host-project issue. This is one real validation on a Vite/TypeScript/Chromium stack, not a guarantee that every framework, browser, codec, or AI coding tool has been tested.
|
| 37 |
+
|
| 38 |
+
The Skill manifest also passes the `skill-creator` structural validator.
|
docs/TECHNICAL_REPORT.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Orbit Video Viewer: turning an authored orbit video into a controllable view
|
| 2 |
+
|
| 3 |
+
## Summary
|
| 4 |
+
|
| 5 |
+
Orbit Video Viewer is a browser Library that maps user input to viewpoints sampled from a pre-generated orbit video. It creates an art-directed, 3D-like viewing interaction without shipping geometry, materials, lighting, or a 3D runtime.
|
| 6 |
+
|
| 7 |
+
Orbit Comic is the demonstration application. The open-source capability is the reusable Viewer API beneath it. The optional `integrate-orbit-video` Skill is a workflow layer that teaches compatible AI coding agents how to add the same SDK to an existing browser project; it is not a second engine.
|
| 8 |
+
|
| 9 |
+
## Input and processing
|
| 10 |
+
|
| 11 |
+
The current source adapter accepts an MP4 URL or a browser `Blob`. MP4Box reads the H.264 track and supplies encoded samples to WebCodecs. The decoder walks the timeline once and selects evenly spaced target timestamps. Each selected frame is resized and encoded to an in-memory JPEG blob.
|
| 12 |
+
|
| 13 |
+
When WebCodecs or the H.264 path is unavailable, an offscreen `<video>` element seeks to the same target positions and copies them through Canvas.
|
| 14 |
+
|
| 15 |
+
The current Demo requests 124 frames at up to 852 × 480 pixels. These are application choices, not protocol requirements.
|
| 16 |
+
|
| 17 |
+
## Runtime interaction
|
| 18 |
+
|
| 19 |
+
Horizontal pointer distance is converted into a continuous turn value. The turn wraps cyclically and maps to a frame index. The same mapping supports both directions and any number of complete rotations.
|
| 20 |
+
|
| 21 |
+
Recent pointer samples estimate release velocity. Exponential decay creates a short inertial glide. Reset and programmatic angle changes can use an eased animation.
|
| 22 |
+
|
| 23 |
+
The public Library exposes the current angle and frame, allowing a host product to attach labels, game events, sound, educational annotations, or other state to a viewpoint.
|
| 24 |
+
|
| 25 |
+
## Frame storage and memory
|
| 26 |
+
|
| 27 |
+
JPEG blobs hold the sampled sequence. Rendering uses decoded `ImageBitmap` objects. A least-recently-used cache retains a small number of bitmaps, while directional prefetch prepares nearby frames.
|
| 28 |
+
|
| 29 |
+
Destroying a Viewer aborts active work, removes input listeners, closes cached bitmaps, and releases the prepared source.
|
| 30 |
+
|
| 31 |
+
All extracted frames remain in the visitor's browser memory. The Library does not upload them.
|
| 32 |
+
|
| 33 |
+
## Architecture
|
| 34 |
+
|
| 35 |
+
The repository separates three responsibilities:
|
| 36 |
+
|
| 37 |
+
1. **Core:** angle math, motion, frame storage, rendering, and the general source contract.
|
| 38 |
+
2. **Library:** the stable `OrbitViewer` API and the current `Mp4Source` adapter.
|
| 39 |
+
3. **Demo:** Orbit Comic's artwork, layout, title, buttons, loading UI, and local-file control.
|
| 40 |
+
|
| 41 |
+
The Demo uses the public Library API instead of assembling Core classes directly. A separate consumer project installs the built package to verify that the Library boundary is real.
|
| 42 |
+
|
| 43 |
+
Distribution adds a fourth, non-runtime layer: the integration Skill. Its `SKILL.md` inspects a host project, installs the Library, follows that project's component lifecycle, and verifies the resulting build. Applications created with the Skill still import the public Library API and do not depend on the Demo.
|
| 44 |
+
|
| 45 |
+
## Runtime MP4 versus preprocessed frames
|
| 46 |
+
|
| 47 |
+
Runtime MP4 processing keeps authoring simple and enables local visitor uploads. Its trade-offs are startup decoding work, memory use, and browser codec differences.
|
| 48 |
+
|
| 49 |
+
A production deployment may eventually preprocess frames and load them from a manifest or CDN. The `OrbitSource` contract was added so that such a source can be introduced later without changing the Viewer API. This release deliberately does not include that preprocessing tool.
|
| 50 |
+
|
| 51 |
+
## Limits
|
| 52 |
+
|
| 53 |
+
The Library can only show viewpoints present in its source. It cannot move freely through 3D space, repair discontinuities, change geometry, calculate collisions, or relight a scene.
|
| 54 |
+
|
| 55 |
+
The verified fast path uses H.264 MP4. Browser fallbacks depend on codecs supported by each browser. Source continuity remains an authoring responsibility.
|
| 56 |
+
|
| 57 |
+
The current manual browser verification covers a Chromium-based browser. Other modern browsers are feature-detected but should not be described as fully verified until the same interaction matrix has been run there.
|
| 58 |
+
|
| 59 |
+
## Credits
|
| 60 |
+
|
| 61 |
+
The interaction direction was learned from a Claude Artifact shared by [monoradio (@monoradio102)](https://x.com/monoradio102). This repository is an independent implementation written with Codex and does not copy the reference Artifact's source code.
|
docs/assets/concepts/PROMPTS.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# README concept image prompts
|
| 2 |
+
|
| 3 |
+
These images were generated with OpenAI ImageGen on 2026-10-12 and resized for the README. The full-resolution originals remain in the local ImageGen output directory used during development.
|
| 4 |
+
|
| 5 |
+
## Product Viewer
|
| 6 |
+
|
| 7 |
+
> Create a high-end landscape concept image for an open-source README, 16:9. Scene: an elegant unbranded contemporary sneaker on a low matte stone plinth in a restrained warm-gray studio. The product is the only subject, shown with subtle ghosted side-angle silhouettes and a delicate curved orbit cue suggesting that a viewer can drag to rotate it. Visual style: mature editorial product photography blended with refined digital-art direction; quiet, international, premium, believable materials, soft directional light, generous negative space. Constraints: no people, no logos, no words, no readable UI text, no watermark, no brand marks, no busy dashboard, no split screen. The image must read clearly at a small README width. Output intent: inspiration card titled externally as “Product Viewer”; do not put the title inside the image.
|
| 8 |
+
|
| 9 |
+
Saved as `product-viewer.jpg`.
|
| 10 |
+
|
| 11 |
+
## Game Item Inspection
|
| 12 |
+
|
| 13 |
+
> Create a high-end landscape concept image for an open-source README, 16:9. Scene: a single mysterious fantasy game artifact—a weathered dark-metal compass-like relic with a luminous amber core—floating above a minimal inspection pedestal in a deep charcoal environment. Show a few restrained technical tick marks and a subtle circular orbit cue around the item, communicating that a player can drag and inspect it from different angles. Visual style: sophisticated game item inspection screen, cinematic but minimal, polished, art-directed, strong object silhouette, controlled amber accent light. Constraints: no character, no logos, no words, no readable UI text, no watermark, no inventory grid, no clutter, no split screen. The image must read clearly at a small README width. Output intent: inspiration card titled externally as “Game Item Inspection”; do not put the title inside the image.
|
| 14 |
+
|
| 15 |
+
Saved as `game-item-inspection.jpg`.
|
| 16 |
+
|
| 17 |
+
## Interactive Comic
|
| 18 |
+
|
| 19 |
+
> Create a high-end landscape concept image for an open-source README, 16:9. Scene: an original interactive comic moment in a windswept coastal train platform at dusk. One illustrated traveler in a long red coat stands beside a stationary vintage train. Suggest viewpoint exploration with two softly layered alternate camera-angle echoes of the same scene and a restrained curved drag cue, while keeping one main composition dominant. Visual style: refined contemporary graphic novel, inked shapes, limited navy, warm cream, and vermilion palette, subtle paper texture, cinematic framing, mature independent-comic presentation. Constraints: original generic character, no existing franchise, no speech balloons, no words, no logos, no watermark, no multi-panel page, no split screen. The image must read clearly at a small README width. Output intent: inspiration card titled externally as “Interactive Comic”; do not put the title inside the image.
|
| 20 |
+
|
| 21 |
+
Saved as `interactive-comic.jpg`.
|
docs/assets/concepts/game-item-inspection.jpg
ADDED
|
Git LFS Details
|
docs/assets/concepts/interactive-comic.jpg
ADDED
|
Git LFS Details
|
docs/assets/concepts/product-viewer.jpg
ADDED
|
Git LFS Details
|
examples/basic-integration/index.html
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Orbit Viewer integration test</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<main>
|
| 10 |
+
<p class="eyebrow">Independent integration</p>
|
| 11 |
+
<h1>Product inspector</h1>
|
| 12 |
+
<div id="viewer" tabindex="0" aria-label="Rotatable product view"></div>
|
| 13 |
+
<div class="controls">
|
| 14 |
+
<button data-angle="0">Front</button>
|
| 15 |
+
<button data-angle="90">Side</button>
|
| 16 |
+
<button data-angle="180">Back</button>
|
| 17 |
+
<button id="reset">Reset</button>
|
| 18 |
+
<output id="angle">Loading…</output>
|
| 19 |
+
</div>
|
| 20 |
+
</main>
|
| 21 |
+
<script type="module" src="/src/main.ts"></script>
|
| 22 |
+
</body>
|
| 23 |
+
</html>
|
examples/basic-integration/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "orbit-viewer-basic-integration",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite --host 127.0.0.1 --port 5174",
|
| 8 |
+
"build": "tsc -b && vite build"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"orbit-video-viewer": "file:../.."
|
| 12 |
+
},
|
| 13 |
+
"devDependencies": {
|
| 14 |
+
"typescript": "^7.0.2",
|
| 15 |
+
"vite": "^8.3.4"
|
| 16 |
+
}
|
| 17 |
+
}
|
examples/basic-integration/pnpm-lock.yaml
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
lockfileVersion: '9.0'
|
| 2 |
+
|
| 3 |
+
settings:
|
| 4 |
+
autoInstallPeers: true
|
| 5 |
+
excludeLinksFromLockfile: false
|
| 6 |
+
|
| 7 |
+
importers:
|
| 8 |
+
|
| 9 |
+
.:
|
| 10 |
+
dependencies:
|
| 11 |
+
orbit-video-viewer:
|
| 12 |
+
specifier: file:../..
|
| 13 |
+
version: file:../..
|
| 14 |
+
devDependencies:
|
| 15 |
+
typescript:
|
| 16 |
+
specifier: ^7.0.2
|
| 17 |
+
version: 7.0.2
|
| 18 |
+
vite:
|
| 19 |
+
specifier: ^8.3.4
|
| 20 |
+
version: 8.3.4
|
| 21 |
+
|
| 22 |
+
packages:
|
| 23 |
+
|
| 24 |
+
'@oxc-project/types@0.153.0':
|
| 25 |
+
resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==}
|
| 26 |
+
|
| 27 |
+
'@rolldown/binding-android-arm-eabi@1.2.13':
|
| 28 |
+
resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==}
|
| 29 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 30 |
+
cpu: [arm]
|
| 31 |
+
os: [android]
|
| 32 |
+
|
| 33 |
+
'@rolldown/binding-android-arm64@1.2.13':
|
| 34 |
+
resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==}
|
| 35 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 36 |
+
cpu: [arm64]
|
| 37 |
+
os: [android]
|
| 38 |
+
|
| 39 |
+
'@rolldown/binding-darwin-arm64@1.2.13':
|
| 40 |
+
resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==}
|
| 41 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 42 |
+
cpu: [arm64]
|
| 43 |
+
os: [darwin]
|
| 44 |
+
|
| 45 |
+
'@rolldown/binding-darwin-x64@1.2.13':
|
| 46 |
+
resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==}
|
| 47 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 48 |
+
cpu: [x64]
|
| 49 |
+
os: [darwin]
|
| 50 |
+
|
| 51 |
+
'@rolldown/binding-freebsd-x64@1.2.13':
|
| 52 |
+
resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==}
|
| 53 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 54 |
+
cpu: [x64]
|
| 55 |
+
os: [freebsd]
|
| 56 |
+
|
| 57 |
+
'@rolldown/binding-linux-arm-gnueabihf@1.2.13':
|
| 58 |
+
resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==}
|
| 59 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 60 |
+
cpu: [arm]
|
| 61 |
+
os: [linux]
|
| 62 |
+
|
| 63 |
+
'@rolldown/binding-linux-arm64-gnu@1.2.13':
|
| 64 |
+
resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==}
|
| 65 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 66 |
+
cpu: [arm64]
|
| 67 |
+
os: [linux]
|
| 68 |
+
libc: [glibc]
|
| 69 |
+
|
| 70 |
+
'@rolldown/binding-linux-arm64-musl@1.2.13':
|
| 71 |
+
resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==}
|
| 72 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 73 |
+
cpu: [arm64]
|
| 74 |
+
os: [linux]
|
| 75 |
+
libc: [musl]
|
| 76 |
+
|
| 77 |
+
'@rolldown/binding-linux-ppc64-gnu@1.2.13':
|
| 78 |
+
resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==}
|
| 79 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 80 |
+
cpu: [ppc64]
|
| 81 |
+
os: [linux]
|
| 82 |
+
libc: [glibc]
|
| 83 |
+
|
| 84 |
+
'@rolldown/binding-linux-s390x-gnu@1.2.13':
|
| 85 |
+
resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==}
|
| 86 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 87 |
+
cpu: [s390x]
|
| 88 |
+
os: [linux]
|
| 89 |
+
libc: [glibc]
|
| 90 |
+
|
| 91 |
+
'@rolldown/binding-linux-x64-gnu@1.2.13':
|
| 92 |
+
resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==}
|
| 93 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 94 |
+
cpu: [x64]
|
| 95 |
+
os: [linux]
|
| 96 |
+
libc: [glibc]
|
| 97 |
+
|
| 98 |
+
'@rolldown/binding-linux-x64-musl@1.2.13':
|
| 99 |
+
resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==}
|
| 100 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 101 |
+
cpu: [x64]
|
| 102 |
+
os: [linux]
|
| 103 |
+
libc: [musl]
|
| 104 |
+
|
| 105 |
+
'@rolldown/binding-openharmony-arm64@1.2.13':
|
| 106 |
+
resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==}
|
| 107 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 108 |
+
cpu: [arm64]
|
| 109 |
+
os: [openharmony]
|
| 110 |
+
|
| 111 |
+
'@rolldown/binding-win32-arm64-msvc@1.2.13':
|
| 112 |
+
resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==}
|
| 113 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 114 |
+
cpu: [arm64]
|
| 115 |
+
os: [win32]
|
| 116 |
+
|
| 117 |
+
'@rolldown/binding-win32-x64-msvc@1.2.13':
|
| 118 |
+
resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==}
|
| 119 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 120 |
+
cpu: [x64]
|
| 121 |
+
os: [win32]
|
| 122 |
+
|
| 123 |
+
'@rolldown/pluginutils@1.0.1':
|
| 124 |
+
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
| 125 |
+
|
| 126 |
+
'@typescript/typescript-aix-ppc64@7.0.2':
|
| 127 |
+
resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
|
| 128 |
+
engines: {node: '>=16.20.0'}
|
| 129 |
+
cpu: [ppc64]
|
| 130 |
+
os: [aix]
|
| 131 |
+
|
| 132 |
+
'@typescript/typescript-darwin-arm64@7.0.2':
|
| 133 |
+
resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
|
| 134 |
+
engines: {node: '>=16.20.0'}
|
| 135 |
+
cpu: [arm64]
|
| 136 |
+
os: [darwin]
|
| 137 |
+
|
| 138 |
+
'@typescript/typescript-darwin-x64@7.0.2':
|
| 139 |
+
resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
|
| 140 |
+
engines: {node: '>=16.20.0'}
|
| 141 |
+
cpu: [x64]
|
| 142 |
+
os: [darwin]
|
| 143 |
+
|
| 144 |
+
'@typescript/typescript-freebsd-arm64@7.0.2':
|
| 145 |
+
resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
|
| 146 |
+
engines: {node: '>=16.20.0'}
|
| 147 |
+
cpu: [arm64]
|
| 148 |
+
os: [freebsd]
|
| 149 |
+
|
| 150 |
+
'@typescript/typescript-freebsd-x64@7.0.2':
|
| 151 |
+
resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
|
| 152 |
+
engines: {node: '>=16.20.0'}
|
| 153 |
+
cpu: [x64]
|
| 154 |
+
os: [freebsd]
|
| 155 |
+
|
| 156 |
+
'@typescript/typescript-linux-arm64@7.0.2':
|
| 157 |
+
resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
|
| 158 |
+
engines: {node: '>=16.20.0'}
|
| 159 |
+
cpu: [arm64]
|
| 160 |
+
os: [linux]
|
| 161 |
+
|
| 162 |
+
'@typescript/typescript-linux-arm@7.0.2':
|
| 163 |
+
resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
|
| 164 |
+
engines: {node: '>=16.20.0'}
|
| 165 |
+
cpu: [arm]
|
| 166 |
+
os: [linux]
|
| 167 |
+
|
| 168 |
+
'@typescript/typescript-linux-loong64@7.0.2':
|
| 169 |
+
resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
|
| 170 |
+
engines: {node: '>=16.20.0'}
|
| 171 |
+
cpu: [loong64]
|
| 172 |
+
os: [linux]
|
| 173 |
+
|
| 174 |
+
'@typescript/typescript-linux-mips64el@7.0.2':
|
| 175 |
+
resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
|
| 176 |
+
engines: {node: '>=16.20.0'}
|
| 177 |
+
cpu: [mips64el]
|
| 178 |
+
os: [linux]
|
| 179 |
+
|
| 180 |
+
'@typescript/typescript-linux-ppc64@7.0.2':
|
| 181 |
+
resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
|
| 182 |
+
engines: {node: '>=16.20.0'}
|
| 183 |
+
cpu: [ppc64]
|
| 184 |
+
os: [linux]
|
| 185 |
+
|
| 186 |
+
'@typescript/typescript-linux-riscv64@7.0.2':
|
| 187 |
+
resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
|
| 188 |
+
engines: {node: '>=16.20.0'}
|
| 189 |
+
cpu: [riscv64]
|
| 190 |
+
os: [linux]
|
| 191 |
+
|
| 192 |
+
'@typescript/typescript-linux-s390x@7.0.2':
|
| 193 |
+
resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
|
| 194 |
+
engines: {node: '>=16.20.0'}
|
| 195 |
+
cpu: [s390x]
|
| 196 |
+
os: [linux]
|
| 197 |
+
|
| 198 |
+
'@typescript/typescript-linux-x64@7.0.2':
|
| 199 |
+
resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
|
| 200 |
+
engines: {node: '>=16.20.0'}
|
| 201 |
+
cpu: [x64]
|
| 202 |
+
os: [linux]
|
| 203 |
+
|
| 204 |
+
'@typescript/typescript-netbsd-arm64@7.0.2':
|
| 205 |
+
resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
|
| 206 |
+
engines: {node: '>=16.20.0'}
|
| 207 |
+
cpu: [arm64]
|
| 208 |
+
os: [netbsd]
|
| 209 |
+
|
| 210 |
+
'@typescript/typescript-netbsd-x64@7.0.2':
|
| 211 |
+
resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
|
| 212 |
+
engines: {node: '>=16.20.0'}
|
| 213 |
+
cpu: [x64]
|
| 214 |
+
os: [netbsd]
|
| 215 |
+
|
| 216 |
+
'@typescript/typescript-openbsd-arm64@7.0.2':
|
| 217 |
+
resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
|
| 218 |
+
engines: {node: '>=16.20.0'}
|
| 219 |
+
cpu: [arm64]
|
| 220 |
+
os: [openbsd]
|
| 221 |
+
|
| 222 |
+
'@typescript/typescript-openbsd-x64@7.0.2':
|
| 223 |
+
resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
|
| 224 |
+
engines: {node: '>=16.20.0'}
|
| 225 |
+
cpu: [x64]
|
| 226 |
+
os: [openbsd]
|
| 227 |
+
|
| 228 |
+
'@typescript/typescript-sunos-x64@7.0.2':
|
| 229 |
+
resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
|
| 230 |
+
engines: {node: '>=16.20.0'}
|
| 231 |
+
cpu: [x64]
|
| 232 |
+
os: [sunos]
|
| 233 |
+
|
| 234 |
+
'@typescript/typescript-win32-arm64@7.0.2':
|
| 235 |
+
resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
|
| 236 |
+
engines: {node: '>=16.20.0'}
|
| 237 |
+
cpu: [arm64]
|
| 238 |
+
os: [win32]
|
| 239 |
+
|
| 240 |
+
'@typescript/typescript-win32-x64@7.0.2':
|
| 241 |
+
resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
|
| 242 |
+
engines: {node: '>=16.20.0'}
|
| 243 |
+
cpu: [x64]
|
| 244 |
+
os: [win32]
|
| 245 |
+
|
| 246 |
+
detect-libc@2.1.2:
|
| 247 |
+
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
| 248 |
+
engines: {node: '>=8'}
|
| 249 |
+
|
| 250 |
+
fdir@6.5.0:
|
| 251 |
+
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
| 252 |
+
engines: {node: '>=12.0.0'}
|
| 253 |
+
peerDependencies:
|
| 254 |
+
picomatch: ^3 || ^4
|
| 255 |
+
peerDependenciesMeta:
|
| 256 |
+
picomatch:
|
| 257 |
+
optional: true
|
| 258 |
+
|
| 259 |
+
fsevents@2.3.3:
|
| 260 |
+
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
| 261 |
+
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
| 262 |
+
os: [darwin]
|
| 263 |
+
|
| 264 |
+
lightningcss-android-arm64@1.33.0:
|
| 265 |
+
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
|
| 266 |
+
engines: {node: '>= 12.0.0'}
|
| 267 |
+
cpu: [arm64]
|
| 268 |
+
os: [android]
|
| 269 |
+
|
| 270 |
+
lightningcss-darwin-arm64@1.33.0:
|
| 271 |
+
resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
|
| 272 |
+
engines: {node: '>= 12.0.0'}
|
| 273 |
+
cpu: [arm64]
|
| 274 |
+
os: [darwin]
|
| 275 |
+
|
| 276 |
+
lightningcss-darwin-x64@1.33.0:
|
| 277 |
+
resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
|
| 278 |
+
engines: {node: '>= 12.0.0'}
|
| 279 |
+
cpu: [x64]
|
| 280 |
+
os: [darwin]
|
| 281 |
+
|
| 282 |
+
lightningcss-freebsd-x64@1.33.0:
|
| 283 |
+
resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
|
| 284 |
+
engines: {node: '>= 12.0.0'}
|
| 285 |
+
cpu: [x64]
|
| 286 |
+
os: [freebsd]
|
| 287 |
+
|
| 288 |
+
lightningcss-linux-arm-gnueabihf@1.33.0:
|
| 289 |
+
resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
|
| 290 |
+
engines: {node: '>= 12.0.0'}
|
| 291 |
+
cpu: [arm]
|
| 292 |
+
os: [linux]
|
| 293 |
+
|
| 294 |
+
lightningcss-linux-arm64-gnu@1.33.0:
|
| 295 |
+
resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
|
| 296 |
+
engines: {node: '>= 12.0.0'}
|
| 297 |
+
cpu: [arm64]
|
| 298 |
+
os: [linux]
|
| 299 |
+
libc: [glibc]
|
| 300 |
+
|
| 301 |
+
lightningcss-linux-arm64-musl@1.33.0:
|
| 302 |
+
resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
|
| 303 |
+
engines: {node: '>= 12.0.0'}
|
| 304 |
+
cpu: [arm64]
|
| 305 |
+
os: [linux]
|
| 306 |
+
libc: [musl]
|
| 307 |
+
|
| 308 |
+
lightningcss-linux-x64-gnu@1.33.0:
|
| 309 |
+
resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
|
| 310 |
+
engines: {node: '>= 12.0.0'}
|
| 311 |
+
cpu: [x64]
|
| 312 |
+
os: [linux]
|
| 313 |
+
libc: [glibc]
|
| 314 |
+
|
| 315 |
+
lightningcss-linux-x64-musl@1.33.0:
|
| 316 |
+
resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
|
| 317 |
+
engines: {node: '>= 12.0.0'}
|
| 318 |
+
cpu: [x64]
|
| 319 |
+
os: [linux]
|
| 320 |
+
libc: [musl]
|
| 321 |
+
|
| 322 |
+
lightningcss-win32-arm64-msvc@1.33.0:
|
| 323 |
+
resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
|
| 324 |
+
engines: {node: '>= 12.0.0'}
|
| 325 |
+
cpu: [arm64]
|
| 326 |
+
os: [win32]
|
| 327 |
+
|
| 328 |
+
lightningcss-win32-x64-msvc@1.33.0:
|
| 329 |
+
resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
|
| 330 |
+
engines: {node: '>= 12.0.0'}
|
| 331 |
+
cpu: [x64]
|
| 332 |
+
os: [win32]
|
| 333 |
+
|
| 334 |
+
lightningcss@1.33.0:
|
| 335 |
+
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
| 336 |
+
engines: {node: '>= 12.0.0'}
|
| 337 |
+
|
| 338 |
+
mp4box@2.4.1:
|
| 339 |
+
resolution: {integrity: sha512-0HGX7nXoDIX6FKLVl4a3wtYjBlwqsN3xuQC3GXzNtKp98FXUOhDSq623azsz8DG5ptd9ZXcXodDkgbdMZOjWvw==}
|
| 340 |
+
engines: {node: '>=20.8.1'}
|
| 341 |
+
|
| 342 |
+
nanoid@3.3.20:
|
| 343 |
+
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
|
| 344 |
+
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
| 345 |
+
hasBin: true
|
| 346 |
+
|
| 347 |
+
orbit-video-viewer@file:../..:
|
| 348 |
+
resolution: {directory: ../.., type: directory}
|
| 349 |
+
|
| 350 |
+
picocolors@1.1.1:
|
| 351 |
+
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
| 352 |
+
|
| 353 |
+
picomatch@4.0.7:
|
| 354 |
+
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
| 355 |
+
engines: {node: '>=12'}
|
| 356 |
+
|
| 357 |
+
postcss@8.5.29:
|
| 358 |
+
resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
|
| 359 |
+
engines: {node: ^10 || ^12 || >=14}
|
| 360 |
+
|
| 361 |
+
rolldown@1.2.13:
|
| 362 |
+
resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==}
|
| 363 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 364 |
+
hasBin: true
|
| 365 |
+
|
| 366 |
+
source-map-js@1.2.2:
|
| 367 |
+
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
| 368 |
+
engines: {node: '>=0.10.0'}
|
| 369 |
+
|
| 370 |
+
tinyglobby@0.2.17:
|
| 371 |
+
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
| 372 |
+
engines: {node: '>=12.0.0'}
|
| 373 |
+
|
| 374 |
+
typescript@7.0.2:
|
| 375 |
+
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
|
| 376 |
+
engines: {node: '>=16.20.0'}
|
| 377 |
+
hasBin: true
|
| 378 |
+
|
| 379 |
+
vite@8.3.4:
|
| 380 |
+
resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==}
|
| 381 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 382 |
+
hasBin: true
|
| 383 |
+
peerDependencies:
|
| 384 |
+
'@types/node': ^20.19.0 || >=22.12.0
|
| 385 |
+
'@vitejs/devtools': ^0.7.1
|
| 386 |
+
esbuild: ^0.27.0 || ^0.28.0
|
| 387 |
+
jiti: '>=1.21.0'
|
| 388 |
+
less: ^4.0.0
|
| 389 |
+
sass: ^1.70.0
|
| 390 |
+
sass-embedded: ^1.70.0
|
| 391 |
+
stylus: '>=0.54.8'
|
| 392 |
+
sugarss: ^5.0.0
|
| 393 |
+
terser: ^5.16.0
|
| 394 |
+
tsx: ^4.8.1
|
| 395 |
+
yaml: ^2.4.2
|
| 396 |
+
peerDependenciesMeta:
|
| 397 |
+
'@types/node':
|
| 398 |
+
optional: true
|
| 399 |
+
'@vitejs/devtools':
|
| 400 |
+
optional: true
|
| 401 |
+
esbuild:
|
| 402 |
+
optional: true
|
| 403 |
+
jiti:
|
| 404 |
+
optional: true
|
| 405 |
+
less:
|
| 406 |
+
optional: true
|
| 407 |
+
sass:
|
| 408 |
+
optional: true
|
| 409 |
+
sass-embedded:
|
| 410 |
+
optional: true
|
| 411 |
+
stylus:
|
| 412 |
+
optional: true
|
| 413 |
+
sugarss:
|
| 414 |
+
optional: true
|
| 415 |
+
terser:
|
| 416 |
+
optional: true
|
| 417 |
+
tsx:
|
| 418 |
+
optional: true
|
| 419 |
+
yaml:
|
| 420 |
+
optional: true
|
| 421 |
+
|
| 422 |
+
snapshots:
|
| 423 |
+
|
| 424 |
+
'@oxc-project/types@0.153.0': {}
|
| 425 |
+
|
| 426 |
+
'@rolldown/binding-android-arm-eabi@1.2.13':
|
| 427 |
+
optional: true
|
| 428 |
+
|
| 429 |
+
'@rolldown/binding-android-arm64@1.2.13':
|
| 430 |
+
optional: true
|
| 431 |
+
|
| 432 |
+
'@rolldown/binding-darwin-arm64@1.2.13':
|
| 433 |
+
optional: true
|
| 434 |
+
|
| 435 |
+
'@rolldown/binding-darwin-x64@1.2.13':
|
| 436 |
+
optional: true
|
| 437 |
+
|
| 438 |
+
'@rolldown/binding-freebsd-x64@1.2.13':
|
| 439 |
+
optional: true
|
| 440 |
+
|
| 441 |
+
'@rolldown/binding-linux-arm-gnueabihf@1.2.13':
|
| 442 |
+
optional: true
|
| 443 |
+
|
| 444 |
+
'@rolldown/binding-linux-arm64-gnu@1.2.13':
|
| 445 |
+
optional: true
|
| 446 |
+
|
| 447 |
+
'@rolldown/binding-linux-arm64-musl@1.2.13':
|
| 448 |
+
optional: true
|
| 449 |
+
|
| 450 |
+
'@rolldown/binding-linux-ppc64-gnu@1.2.13':
|
| 451 |
+
optional: true
|
| 452 |
+
|
| 453 |
+
'@rolldown/binding-linux-s390x-gnu@1.2.13':
|
| 454 |
+
optional: true
|
| 455 |
+
|
| 456 |
+
'@rolldown/binding-linux-x64-gnu@1.2.13':
|
| 457 |
+
optional: true
|
| 458 |
+
|
| 459 |
+
'@rolldown/binding-linux-x64-musl@1.2.13':
|
| 460 |
+
optional: true
|
| 461 |
+
|
| 462 |
+
'@rolldown/binding-openharmony-arm64@1.2.13':
|
| 463 |
+
optional: true
|
| 464 |
+
|
| 465 |
+
'@rolldown/binding-win32-arm64-msvc@1.2.13':
|
| 466 |
+
optional: true
|
| 467 |
+
|
| 468 |
+
'@rolldown/binding-win32-x64-msvc@1.2.13':
|
| 469 |
+
optional: true
|
| 470 |
+
|
| 471 |
+
'@rolldown/pluginutils@1.0.1': {}
|
| 472 |
+
|
| 473 |
+
'@typescript/typescript-aix-ppc64@7.0.2':
|
| 474 |
+
optional: true
|
| 475 |
+
|
| 476 |
+
'@typescript/typescript-darwin-arm64@7.0.2':
|
| 477 |
+
optional: true
|
| 478 |
+
|
| 479 |
+
'@typescript/typescript-darwin-x64@7.0.2':
|
| 480 |
+
optional: true
|
| 481 |
+
|
| 482 |
+
'@typescript/typescript-freebsd-arm64@7.0.2':
|
| 483 |
+
optional: true
|
| 484 |
+
|
| 485 |
+
'@typescript/typescript-freebsd-x64@7.0.2':
|
| 486 |
+
optional: true
|
| 487 |
+
|
| 488 |
+
'@typescript/typescript-linux-arm64@7.0.2':
|
| 489 |
+
optional: true
|
| 490 |
+
|
| 491 |
+
'@typescript/typescript-linux-arm@7.0.2':
|
| 492 |
+
optional: true
|
| 493 |
+
|
| 494 |
+
'@typescript/typescript-linux-loong64@7.0.2':
|
| 495 |
+
optional: true
|
| 496 |
+
|
| 497 |
+
'@typescript/typescript-linux-mips64el@7.0.2':
|
| 498 |
+
optional: true
|
| 499 |
+
|
| 500 |
+
'@typescript/typescript-linux-ppc64@7.0.2':
|
| 501 |
+
optional: true
|
| 502 |
+
|
| 503 |
+
'@typescript/typescript-linux-riscv64@7.0.2':
|
| 504 |
+
optional: true
|
| 505 |
+
|
| 506 |
+
'@typescript/typescript-linux-s390x@7.0.2':
|
| 507 |
+
optional: true
|
| 508 |
+
|
| 509 |
+
'@typescript/typescript-linux-x64@7.0.2':
|
| 510 |
+
optional: true
|
| 511 |
+
|
| 512 |
+
'@typescript/typescript-netbsd-arm64@7.0.2':
|
| 513 |
+
optional: true
|
| 514 |
+
|
| 515 |
+
'@typescript/typescript-netbsd-x64@7.0.2':
|
| 516 |
+
optional: true
|
| 517 |
+
|
| 518 |
+
'@typescript/typescript-openbsd-arm64@7.0.2':
|
| 519 |
+
optional: true
|
| 520 |
+
|
| 521 |
+
'@typescript/typescript-openbsd-x64@7.0.2':
|
| 522 |
+
optional: true
|
| 523 |
+
|
| 524 |
+
'@typescript/typescript-sunos-x64@7.0.2':
|
| 525 |
+
optional: true
|
| 526 |
+
|
| 527 |
+
'@typescript/typescript-win32-arm64@7.0.2':
|
| 528 |
+
optional: true
|
| 529 |
+
|
| 530 |
+
'@typescript/typescript-win32-x64@7.0.2':
|
| 531 |
+
optional: true
|
| 532 |
+
|
| 533 |
+
detect-libc@2.1.2: {}
|
| 534 |
+
|
| 535 |
+
fdir@6.5.0(picomatch@4.0.7):
|
| 536 |
+
optionalDependencies:
|
| 537 |
+
picomatch: 4.0.7
|
| 538 |
+
|
| 539 |
+
fsevents@2.3.3:
|
| 540 |
+
optional: true
|
| 541 |
+
|
| 542 |
+
lightningcss-android-arm64@1.33.0:
|
| 543 |
+
optional: true
|
| 544 |
+
|
| 545 |
+
lightningcss-darwin-arm64@1.33.0:
|
| 546 |
+
optional: true
|
| 547 |
+
|
| 548 |
+
lightningcss-darwin-x64@1.33.0:
|
| 549 |
+
optional: true
|
| 550 |
+
|
| 551 |
+
lightningcss-freebsd-x64@1.33.0:
|
| 552 |
+
optional: true
|
| 553 |
+
|
| 554 |
+
lightningcss-linux-arm-gnueabihf@1.33.0:
|
| 555 |
+
optional: true
|
| 556 |
+
|
| 557 |
+
lightningcss-linux-arm64-gnu@1.33.0:
|
| 558 |
+
optional: true
|
| 559 |
+
|
| 560 |
+
lightningcss-linux-arm64-musl@1.33.0:
|
| 561 |
+
optional: true
|
| 562 |
+
|
| 563 |
+
lightningcss-linux-x64-gnu@1.33.0:
|
| 564 |
+
optional: true
|
| 565 |
+
|
| 566 |
+
lightningcss-linux-x64-musl@1.33.0:
|
| 567 |
+
optional: true
|
| 568 |
+
|
| 569 |
+
lightningcss-win32-arm64-msvc@1.33.0:
|
| 570 |
+
optional: true
|
| 571 |
+
|
| 572 |
+
lightningcss-win32-x64-msvc@1.33.0:
|
| 573 |
+
optional: true
|
| 574 |
+
|
| 575 |
+
lightningcss@1.33.0:
|
| 576 |
+
dependencies:
|
| 577 |
+
detect-libc: 2.1.2
|
| 578 |
+
optionalDependencies:
|
| 579 |
+
lightningcss-android-arm64: 1.33.0
|
| 580 |
+
lightningcss-darwin-arm64: 1.33.0
|
| 581 |
+
lightningcss-darwin-x64: 1.33.0
|
| 582 |
+
lightningcss-freebsd-x64: 1.33.0
|
| 583 |
+
lightningcss-linux-arm-gnueabihf: 1.33.0
|
| 584 |
+
lightningcss-linux-arm64-gnu: 1.33.0
|
| 585 |
+
lightningcss-linux-arm64-musl: 1.33.0
|
| 586 |
+
lightningcss-linux-x64-gnu: 1.33.0
|
| 587 |
+
lightningcss-linux-x64-musl: 1.33.0
|
| 588 |
+
lightningcss-win32-arm64-msvc: 1.33.0
|
| 589 |
+
lightningcss-win32-x64-msvc: 1.33.0
|
| 590 |
+
|
| 591 |
+
mp4box@2.4.1: {}
|
| 592 |
+
|
| 593 |
+
nanoid@3.3.20: {}
|
| 594 |
+
|
| 595 |
+
orbit-video-viewer@file:../..:
|
| 596 |
+
dependencies:
|
| 597 |
+
mp4box: 2.4.1
|
| 598 |
+
|
| 599 |
+
picocolors@1.1.1: {}
|
| 600 |
+
|
| 601 |
+
picomatch@4.0.7: {}
|
| 602 |
+
|
| 603 |
+
postcss@8.5.29:
|
| 604 |
+
dependencies:
|
| 605 |
+
nanoid: 3.3.20
|
| 606 |
+
picocolors: 1.1.1
|
| 607 |
+
source-map-js: 1.2.2
|
| 608 |
+
|
| 609 |
+
rolldown@1.2.13:
|
| 610 |
+
dependencies:
|
| 611 |
+
'@oxc-project/types': 0.153.0
|
| 612 |
+
'@rolldown/pluginutils': 1.0.1
|
| 613 |
+
optionalDependencies:
|
| 614 |
+
'@rolldown/binding-android-arm-eabi': 1.2.13
|
| 615 |
+
'@rolldown/binding-android-arm64': 1.2.13
|
| 616 |
+
'@rolldown/binding-darwin-arm64': 1.2.13
|
| 617 |
+
'@rolldown/binding-darwin-x64': 1.2.13
|
| 618 |
+
'@rolldown/binding-freebsd-x64': 1.2.13
|
| 619 |
+
'@rolldown/binding-linux-arm-gnueabihf': 1.2.13
|
| 620 |
+
'@rolldown/binding-linux-arm64-gnu': 1.2.13
|
| 621 |
+
'@rolldown/binding-linux-arm64-musl': 1.2.13
|
| 622 |
+
'@rolldown/binding-linux-ppc64-gnu': 1.2.13
|
| 623 |
+
'@rolldown/binding-linux-s390x-gnu': 1.2.13
|
| 624 |
+
'@rolldown/binding-linux-x64-gnu': 1.2.13
|
| 625 |
+
'@rolldown/binding-linux-x64-musl': 1.2.13
|
| 626 |
+
'@rolldown/binding-openharmony-arm64': 1.2.13
|
| 627 |
+
'@rolldown/binding-win32-arm64-msvc': 1.2.13
|
| 628 |
+
'@rolldown/binding-win32-x64-msvc': 1.2.13
|
| 629 |
+
|
| 630 |
+
source-map-js@1.2.2: {}
|
| 631 |
+
|
| 632 |
+
tinyglobby@0.2.17:
|
| 633 |
+
dependencies:
|
| 634 |
+
fdir: 6.5.0(picomatch@4.0.7)
|
| 635 |
+
picomatch: 4.0.7
|
| 636 |
+
|
| 637 |
+
typescript@7.0.2:
|
| 638 |
+
optionalDependencies:
|
| 639 |
+
'@typescript/typescript-aix-ppc64': 7.0.2
|
| 640 |
+
'@typescript/typescript-darwin-arm64': 7.0.2
|
| 641 |
+
'@typescript/typescript-darwin-x64': 7.0.2
|
| 642 |
+
'@typescript/typescript-freebsd-arm64': 7.0.2
|
| 643 |
+
'@typescript/typescript-freebsd-x64': 7.0.2
|
| 644 |
+
'@typescript/typescript-linux-arm': 7.0.2
|
| 645 |
+
'@typescript/typescript-linux-arm64': 7.0.2
|
| 646 |
+
'@typescript/typescript-linux-loong64': 7.0.2
|
| 647 |
+
'@typescript/typescript-linux-mips64el': 7.0.2
|
| 648 |
+
'@typescript/typescript-linux-ppc64': 7.0.2
|
| 649 |
+
'@typescript/typescript-linux-riscv64': 7.0.2
|
| 650 |
+
'@typescript/typescript-linux-s390x': 7.0.2
|
| 651 |
+
'@typescript/typescript-linux-x64': 7.0.2
|
| 652 |
+
'@typescript/typescript-netbsd-arm64': 7.0.2
|
| 653 |
+
'@typescript/typescript-netbsd-x64': 7.0.2
|
| 654 |
+
'@typescript/typescript-openbsd-arm64': 7.0.2
|
| 655 |
+
'@typescript/typescript-openbsd-x64': 7.0.2
|
| 656 |
+
'@typescript/typescript-sunos-x64': 7.0.2
|
| 657 |
+
'@typescript/typescript-win32-arm64': 7.0.2
|
| 658 |
+
'@typescript/typescript-win32-x64': 7.0.2
|
| 659 |
+
|
| 660 |
+
vite@8.3.4:
|
| 661 |
+
dependencies:
|
| 662 |
+
lightningcss: 1.33.0
|
| 663 |
+
picomatch: 4.0.7
|
| 664 |
+
postcss: 8.5.29
|
| 665 |
+
rolldown: 1.2.13
|
| 666 |
+
tinyglobby: 0.2.17
|
| 667 |
+
optionalDependencies:
|
| 668 |
+
fsevents: 2.3.3
|
examples/basic-integration/src/main.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { OrbitViewer, createMp4Source } from 'orbit-video-viewer';
|
| 2 |
+
import './style.css';
|
| 3 |
+
|
| 4 |
+
const element = document.querySelector<HTMLElement>('#viewer');
|
| 5 |
+
const angleOutput = document.querySelector<HTMLOutputElement>('#angle');
|
| 6 |
+
|
| 7 |
+
if (!element || !angleOutput) throw new Error('Integration test markup is incomplete.');
|
| 8 |
+
|
| 9 |
+
const viewer = new OrbitViewer({
|
| 10 |
+
element,
|
| 11 |
+
initialAngle: 0,
|
| 12 |
+
onProgress(progress) {
|
| 13 |
+
angleOutput.value = `Loading ${Math.round(progress * 100)}%`;
|
| 14 |
+
},
|
| 15 |
+
onChange(state) {
|
| 16 |
+
angleOutput.value = `${state.angle.toFixed(0)}° · frame ${state.frame}`;
|
| 17 |
+
},
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
await viewer.load(createMp4Source('/demo/video_01.mp4'), {
|
| 21 |
+
width: 852,
|
| 22 |
+
height: 480,
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
document.querySelectorAll<HTMLButtonElement>('[data-angle]').forEach((button) => {
|
| 26 |
+
button.addEventListener('click', () => {
|
| 27 |
+
viewer.setAngle(Number(button.dataset.angle), { animate: true });
|
| 28 |
+
});
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
document.querySelector<HTMLButtonElement>('#reset')?.addEventListener('click', () => {
|
| 32 |
+
viewer.reset({ animate: true });
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
window.addEventListener('beforeunload', () => viewer.destroy());
|
examples/basic-integration/src/style.css
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
background: #171717;
|
| 3 |
+
color: #f4f0e8;
|
| 4 |
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
body { margin: 0; min-width: 320px; min-height: 100vh; }
|
| 8 |
+
main { width: min(900px, calc(100% - 32px)); margin: 0 auto; padding: 36px 0; }
|
| 9 |
+
.eyebrow { margin: 0 0 8px; color: #aaa49b; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
|
| 10 |
+
h1 { margin: 0 0 22px; font-size: clamp(28px, 5vw, 52px); font-weight: 500; }
|
| 11 |
+
|
| 12 |
+
#viewer {
|
| 13 |
+
overflow: hidden;
|
| 14 |
+
aspect-ratio: 16 / 9;
|
| 15 |
+
background: #e9e7e1;
|
| 16 |
+
cursor: grab;
|
| 17 |
+
touch-action: pan-y;
|
| 18 |
+
user-select: none;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
#viewer.is-dragging { cursor: grabbing; }
|
| 22 |
+
#viewer canvas { display: block; width: 100%; height: 100%; }
|
| 23 |
+
|
| 24 |
+
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding-top: 16px; }
|
| 25 |
+
button { padding: 8px 13px; border: 1px solid #5e5a54; border-radius: 999px; background: transparent; color: inherit; cursor: pointer; }
|
| 26 |
+
button:hover { border-color: #f4f0e8; }
|
| 27 |
+
output { margin-left: auto; color: #aaa49b; font-variant-numeric: tabular-nums; }
|
examples/basic-integration/src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
examples/basic-integration/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
"moduleResolution": "Bundler",
|
| 9 |
+
"isolatedModules": true,
|
| 10 |
+
"moduleDetection": "force",
|
| 11 |
+
"noEmit": true,
|
| 12 |
+
"strict": true
|
| 13 |
+
},
|
| 14 |
+
"include": ["src"]
|
| 15 |
+
}
|
examples/basic-integration/vite.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { fileURLToPath, URL } from 'node:url';
|
| 2 |
+
import { defineConfig } from 'vite';
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
base: './',
|
| 6 |
+
publicDir: fileURLToPath(new URL('../../public', import.meta.url)),
|
| 7 |
+
});
|
index.html
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="description" content="An interactive comic panel you can rotate by hand." />
|
| 7 |
+
<meta name="theme-color" content="#efede8" />
|
| 8 |
+
<title>Orbit Comic</title>
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<main class="site-shell">
|
| 12 |
+
<header class="topbar">
|
| 13 |
+
<div class="wordmark" aria-label="Orbit Comic"><span>Orbit</span><span>Comic</span></div>
|
| 14 |
+
<p class="credit">
|
| 15 |
+
Technique learned from a Claude Artifact shared by
|
| 16 |
+
<a href="https://x.com/monoradio102" target="_blank" rel="noreferrer">monoradio</a>
|
| 17 |
+
<span aria-hidden="true">·</span> Built with Codex
|
| 18 |
+
</p>
|
| 19 |
+
<nav class="actions" aria-label="Viewer controls">
|
| 20 |
+
<button id="reset" class="text-action" type="button" aria-label="Return to the original view" disabled>Reset view</button>
|
| 21 |
+
</nav>
|
| 22 |
+
</header>
|
| 23 |
+
|
| 24 |
+
<section class="artwork" aria-labelledby="artwork-title">
|
| 25 |
+
<div id="stage" class="stage is-loading" tabindex="0" role="application" aria-label="Interactive rotating comic panel">
|
| 26 |
+
<img id="poster" class="poster" src="./demo/poster.png" alt="A figure and motorcycle in a desert landscape" />
|
| 27 |
+
<canvas id="canvas" aria-hidden="true"></canvas>
|
| 28 |
+
<div id="loading" class="loading-state" aria-live="polite">
|
| 29 |
+
<div class="loading-copy"><span id="phase">Loading</span><span id="progress-label">0%</span></div>
|
| 30 |
+
<div class="progress-track" aria-hidden="true"><span id="progress"></span></div>
|
| 31 |
+
</div>
|
| 32 |
+
<div id="drag-hint" class="drag-hint" hidden>
|
| 33 |
+
<span class="drag-line"></span><span>Drag to rotate</span><span class="drag-line"></span>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<footer class="caption">
|
| 38 |
+
<h1 id="artwork-title">Desert Standoff</h1>
|
| 39 |
+
<div class="caption-meta">
|
| 40 |
+
<span id="source-label" class="source-label">Panel 01</span>
|
| 41 |
+
<label class="text-action file-action">Change video<input id="video-input" type="file" accept="video/mp4,video/*" /></label>
|
| 42 |
+
</div>
|
| 43 |
+
</footer>
|
| 44 |
+
<p id="status" class="sr-only" aria-live="polite">Loading artwork.</p>
|
| 45 |
+
</section>
|
| 46 |
+
</main>
|
| 47 |
+
<script type="module" src="/src/demo/main.ts"></script>
|
| 48 |
+
</body>
|
| 49 |
+
</html>
|
package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "orbit-video-viewer",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"description": "A browser library for turning pre-generated orbit videos into controllable visual interactions.",
|
| 5 |
+
"homepage": "https://github.com/boryzoo/orbit-video-viewer#readme",
|
| 6 |
+
"repository": {
|
| 7 |
+
"type": "git",
|
| 8 |
+
"url": "git+https://github.com/boryzoo/orbit-video-viewer.git"
|
| 9 |
+
},
|
| 10 |
+
"bugs": {
|
| 11 |
+
"url": "https://github.com/boryzoo/orbit-video-viewer/issues"
|
| 12 |
+
},
|
| 13 |
+
"module": "./dist/library/orbit-viewer.js",
|
| 14 |
+
"types": "./dist/types/library/index.d.ts",
|
| 15 |
+
"exports": {
|
| 16 |
+
".": {
|
| 17 |
+
"types": "./dist/types/library/index.d.ts",
|
| 18 |
+
"import": "./dist/library/orbit-viewer.js"
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"files": [
|
| 22 |
+
"dist/library",
|
| 23 |
+
"dist/types",
|
| 24 |
+
"README.md",
|
| 25 |
+
"README.zh-CN.md",
|
| 26 |
+
"LICENSE",
|
| 27 |
+
"THIRD_PARTY_NOTICES.md"
|
| 28 |
+
],
|
| 29 |
+
"sideEffects": false,
|
| 30 |
+
"scripts": {
|
| 31 |
+
"dev": "vite --host 127.0.0.1",
|
| 32 |
+
"build": "pnpm run build:demo && pnpm run build:library",
|
| 33 |
+
"build:demo": "tsc -b && vite build",
|
| 34 |
+
"build:library": "vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json",
|
| 35 |
+
"prepack": "pnpm run build:library",
|
| 36 |
+
"preview": "vite preview --host 127.0.0.1",
|
| 37 |
+
"test": "vitest run",
|
| 38 |
+
"test:integration": "pnpm run build && pnpm --dir examples/basic-integration build",
|
| 39 |
+
"check:release": "pnpm test && pnpm run test:integration && pnpm pack --dry-run"
|
| 40 |
+
},
|
| 41 |
+
"keywords": ["orbit", "video", "viewer", "interaction", "canvas"],
|
| 42 |
+
"license": "MIT",
|
| 43 |
+
"engines": {
|
| 44 |
+
"node": ">=20"
|
| 45 |
+
},
|
| 46 |
+
"publishConfig": {
|
| 47 |
+
"access": "public"
|
| 48 |
+
},
|
| 49 |
+
"packageManager": "pnpm@11.16.0",
|
| 50 |
+
"type": "module",
|
| 51 |
+
"dependencies": {
|
| 52 |
+
"mp4box": "^2.4.1"
|
| 53 |
+
},
|
| 54 |
+
"devDependencies": {
|
| 55 |
+
"typescript": "^7.0.2",
|
| 56 |
+
"vite": "^8.3.4",
|
| 57 |
+
"vitest": "^5.0.3"
|
| 58 |
+
}
|
| 59 |
+
}
|
pnpm-lock.yaml
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
lockfileVersion: '9.0'
|
| 2 |
+
|
| 3 |
+
settings:
|
| 4 |
+
autoInstallPeers: true
|
| 5 |
+
excludeLinksFromLockfile: false
|
| 6 |
+
|
| 7 |
+
importers:
|
| 8 |
+
|
| 9 |
+
.:
|
| 10 |
+
dependencies:
|
| 11 |
+
mp4box:
|
| 12 |
+
specifier: ^2.4.1
|
| 13 |
+
version: 2.4.1
|
| 14 |
+
devDependencies:
|
| 15 |
+
typescript:
|
| 16 |
+
specifier: ^7.0.2
|
| 17 |
+
version: 7.0.2
|
| 18 |
+
vite:
|
| 19 |
+
specifier: ^8.3.4
|
| 20 |
+
version: 8.3.4
|
| 21 |
+
vitest:
|
| 22 |
+
specifier: ^5.0.3
|
| 23 |
+
version: 5.0.3(vite@8.3.4)
|
| 24 |
+
|
| 25 |
+
packages:
|
| 26 |
+
|
| 27 |
+
'@jridgewell/resolve-uri@3.1.2':
|
| 28 |
+
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
| 29 |
+
engines: {node: '>=6.0.0'}
|
| 30 |
+
|
| 31 |
+
'@jridgewell/sourcemap-codec@1.6.0':
|
| 32 |
+
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
|
| 33 |
+
|
| 34 |
+
'@jridgewell/trace-mapping@0.3.31':
|
| 35 |
+
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
| 36 |
+
|
| 37 |
+
'@oxc-project/types@0.153.0':
|
| 38 |
+
resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==}
|
| 39 |
+
|
| 40 |
+
'@rolldown/binding-android-arm-eabi@1.2.13':
|
| 41 |
+
resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==}
|
| 42 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 43 |
+
cpu: [arm]
|
| 44 |
+
os: [android]
|
| 45 |
+
|
| 46 |
+
'@rolldown/binding-android-arm64@1.2.13':
|
| 47 |
+
resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==}
|
| 48 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 49 |
+
cpu: [arm64]
|
| 50 |
+
os: [android]
|
| 51 |
+
|
| 52 |
+
'@rolldown/binding-darwin-arm64@1.2.13':
|
| 53 |
+
resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==}
|
| 54 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 55 |
+
cpu: [arm64]
|
| 56 |
+
os: [darwin]
|
| 57 |
+
|
| 58 |
+
'@rolldown/binding-darwin-x64@1.2.13':
|
| 59 |
+
resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==}
|
| 60 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 61 |
+
cpu: [x64]
|
| 62 |
+
os: [darwin]
|
| 63 |
+
|
| 64 |
+
'@rolldown/binding-freebsd-x64@1.2.13':
|
| 65 |
+
resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==}
|
| 66 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 67 |
+
cpu: [x64]
|
| 68 |
+
os: [freebsd]
|
| 69 |
+
|
| 70 |
+
'@rolldown/binding-linux-arm-gnueabihf@1.2.13':
|
| 71 |
+
resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==}
|
| 72 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 73 |
+
cpu: [arm]
|
| 74 |
+
os: [linux]
|
| 75 |
+
|
| 76 |
+
'@rolldown/binding-linux-arm64-gnu@1.2.13':
|
| 77 |
+
resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==}
|
| 78 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 79 |
+
cpu: [arm64]
|
| 80 |
+
os: [linux]
|
| 81 |
+
libc: [glibc]
|
| 82 |
+
|
| 83 |
+
'@rolldown/binding-linux-arm64-musl@1.2.13':
|
| 84 |
+
resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==}
|
| 85 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 86 |
+
cpu: [arm64]
|
| 87 |
+
os: [linux]
|
| 88 |
+
libc: [musl]
|
| 89 |
+
|
| 90 |
+
'@rolldown/binding-linux-ppc64-gnu@1.2.13':
|
| 91 |
+
resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==}
|
| 92 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 93 |
+
cpu: [ppc64]
|
| 94 |
+
os: [linux]
|
| 95 |
+
libc: [glibc]
|
| 96 |
+
|
| 97 |
+
'@rolldown/binding-linux-s390x-gnu@1.2.13':
|
| 98 |
+
resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==}
|
| 99 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 100 |
+
cpu: [s390x]
|
| 101 |
+
os: [linux]
|
| 102 |
+
libc: [glibc]
|
| 103 |
+
|
| 104 |
+
'@rolldown/binding-linux-x64-gnu@1.2.13':
|
| 105 |
+
resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==}
|
| 106 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 107 |
+
cpu: [x64]
|
| 108 |
+
os: [linux]
|
| 109 |
+
libc: [glibc]
|
| 110 |
+
|
| 111 |
+
'@rolldown/binding-linux-x64-musl@1.2.13':
|
| 112 |
+
resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==}
|
| 113 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 114 |
+
cpu: [x64]
|
| 115 |
+
os: [linux]
|
| 116 |
+
libc: [musl]
|
| 117 |
+
|
| 118 |
+
'@rolldown/binding-openharmony-arm64@1.2.13':
|
| 119 |
+
resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==}
|
| 120 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 121 |
+
cpu: [arm64]
|
| 122 |
+
os: [openharmony]
|
| 123 |
+
|
| 124 |
+
'@rolldown/binding-win32-arm64-msvc@1.2.13':
|
| 125 |
+
resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==}
|
| 126 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 127 |
+
cpu: [arm64]
|
| 128 |
+
os: [win32]
|
| 129 |
+
|
| 130 |
+
'@rolldown/binding-win32-x64-msvc@1.2.13':
|
| 131 |
+
resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==}
|
| 132 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 133 |
+
cpu: [x64]
|
| 134 |
+
os: [win32]
|
| 135 |
+
|
| 136 |
+
'@rolldown/pluginutils@1.0.1':
|
| 137 |
+
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
| 138 |
+
|
| 139 |
+
'@types/chai@5.2.3':
|
| 140 |
+
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
| 141 |
+
|
| 142 |
+
'@types/deep-eql@4.0.2':
|
| 143 |
+
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
| 144 |
+
|
| 145 |
+
'@types/estree@1.0.9':
|
| 146 |
+
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
| 147 |
+
|
| 148 |
+
'@typescript/typescript-aix-ppc64@7.0.2':
|
| 149 |
+
resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
|
| 150 |
+
engines: {node: '>=16.20.0'}
|
| 151 |
+
cpu: [ppc64]
|
| 152 |
+
os: [aix]
|
| 153 |
+
|
| 154 |
+
'@typescript/typescript-darwin-arm64@7.0.2':
|
| 155 |
+
resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
|
| 156 |
+
engines: {node: '>=16.20.0'}
|
| 157 |
+
cpu: [arm64]
|
| 158 |
+
os: [darwin]
|
| 159 |
+
|
| 160 |
+
'@typescript/typescript-darwin-x64@7.0.2':
|
| 161 |
+
resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
|
| 162 |
+
engines: {node: '>=16.20.0'}
|
| 163 |
+
cpu: [x64]
|
| 164 |
+
os: [darwin]
|
| 165 |
+
|
| 166 |
+
'@typescript/typescript-freebsd-arm64@7.0.2':
|
| 167 |
+
resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
|
| 168 |
+
engines: {node: '>=16.20.0'}
|
| 169 |
+
cpu: [arm64]
|
| 170 |
+
os: [freebsd]
|
| 171 |
+
|
| 172 |
+
'@typescript/typescript-freebsd-x64@7.0.2':
|
| 173 |
+
resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
|
| 174 |
+
engines: {node: '>=16.20.0'}
|
| 175 |
+
cpu: [x64]
|
| 176 |
+
os: [freebsd]
|
| 177 |
+
|
| 178 |
+
'@typescript/typescript-linux-arm64@7.0.2':
|
| 179 |
+
resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
|
| 180 |
+
engines: {node: '>=16.20.0'}
|
| 181 |
+
cpu: [arm64]
|
| 182 |
+
os: [linux]
|
| 183 |
+
|
| 184 |
+
'@typescript/typescript-linux-arm@7.0.2':
|
| 185 |
+
resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
|
| 186 |
+
engines: {node: '>=16.20.0'}
|
| 187 |
+
cpu: [arm]
|
| 188 |
+
os: [linux]
|
| 189 |
+
|
| 190 |
+
'@typescript/typescript-linux-loong64@7.0.2':
|
| 191 |
+
resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
|
| 192 |
+
engines: {node: '>=16.20.0'}
|
| 193 |
+
cpu: [loong64]
|
| 194 |
+
os: [linux]
|
| 195 |
+
|
| 196 |
+
'@typescript/typescript-linux-mips64el@7.0.2':
|
| 197 |
+
resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
|
| 198 |
+
engines: {node: '>=16.20.0'}
|
| 199 |
+
cpu: [mips64el]
|
| 200 |
+
os: [linux]
|
| 201 |
+
|
| 202 |
+
'@typescript/typescript-linux-ppc64@7.0.2':
|
| 203 |
+
resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
|
| 204 |
+
engines: {node: '>=16.20.0'}
|
| 205 |
+
cpu: [ppc64]
|
| 206 |
+
os: [linux]
|
| 207 |
+
|
| 208 |
+
'@typescript/typescript-linux-riscv64@7.0.2':
|
| 209 |
+
resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
|
| 210 |
+
engines: {node: '>=16.20.0'}
|
| 211 |
+
cpu: [riscv64]
|
| 212 |
+
os: [linux]
|
| 213 |
+
|
| 214 |
+
'@typescript/typescript-linux-s390x@7.0.2':
|
| 215 |
+
resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
|
| 216 |
+
engines: {node: '>=16.20.0'}
|
| 217 |
+
cpu: [s390x]
|
| 218 |
+
os: [linux]
|
| 219 |
+
|
| 220 |
+
'@typescript/typescript-linux-x64@7.0.2':
|
| 221 |
+
resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
|
| 222 |
+
engines: {node: '>=16.20.0'}
|
| 223 |
+
cpu: [x64]
|
| 224 |
+
os: [linux]
|
| 225 |
+
|
| 226 |
+
'@typescript/typescript-netbsd-arm64@7.0.2':
|
| 227 |
+
resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
|
| 228 |
+
engines: {node: '>=16.20.0'}
|
| 229 |
+
cpu: [arm64]
|
| 230 |
+
os: [netbsd]
|
| 231 |
+
|
| 232 |
+
'@typescript/typescript-netbsd-x64@7.0.2':
|
| 233 |
+
resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
|
| 234 |
+
engines: {node: '>=16.20.0'}
|
| 235 |
+
cpu: [x64]
|
| 236 |
+
os: [netbsd]
|
| 237 |
+
|
| 238 |
+
'@typescript/typescript-openbsd-arm64@7.0.2':
|
| 239 |
+
resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
|
| 240 |
+
engines: {node: '>=16.20.0'}
|
| 241 |
+
cpu: [arm64]
|
| 242 |
+
os: [openbsd]
|
| 243 |
+
|
| 244 |
+
'@typescript/typescript-openbsd-x64@7.0.2':
|
| 245 |
+
resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
|
| 246 |
+
engines: {node: '>=16.20.0'}
|
| 247 |
+
cpu: [x64]
|
| 248 |
+
os: [openbsd]
|
| 249 |
+
|
| 250 |
+
'@typescript/typescript-sunos-x64@7.0.2':
|
| 251 |
+
resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
|
| 252 |
+
engines: {node: '>=16.20.0'}
|
| 253 |
+
cpu: [x64]
|
| 254 |
+
os: [sunos]
|
| 255 |
+
|
| 256 |
+
'@typescript/typescript-win32-arm64@7.0.2':
|
| 257 |
+
resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
|
| 258 |
+
engines: {node: '>=16.20.0'}
|
| 259 |
+
cpu: [arm64]
|
| 260 |
+
os: [win32]
|
| 261 |
+
|
| 262 |
+
'@typescript/typescript-win32-x64@7.0.2':
|
| 263 |
+
resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
|
| 264 |
+
engines: {node: '>=16.20.0'}
|
| 265 |
+
cpu: [x64]
|
| 266 |
+
os: [win32]
|
| 267 |
+
|
| 268 |
+
'@vitest/mocker@5.0.3':
|
| 269 |
+
resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
|
| 270 |
+
peerDependencies:
|
| 271 |
+
msw: ^2.4.9
|
| 272 |
+
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
| 273 |
+
peerDependenciesMeta:
|
| 274 |
+
msw:
|
| 275 |
+
optional: true
|
| 276 |
+
vite:
|
| 277 |
+
optional: true
|
| 278 |
+
|
| 279 |
+
'@vitest/spy@5.0.3':
|
| 280 |
+
resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==}
|
| 281 |
+
|
| 282 |
+
assertion-error@2.0.1:
|
| 283 |
+
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
| 284 |
+
engines: {node: '>=12'}
|
| 285 |
+
|
| 286 |
+
chai@6.3.0:
|
| 287 |
+
resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
|
| 288 |
+
engines: {node: '>=18'}
|
| 289 |
+
|
| 290 |
+
detect-libc@2.1.2:
|
| 291 |
+
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
| 292 |
+
engines: {node: '>=8'}
|
| 293 |
+
|
| 294 |
+
es-module-lexer@2.3.2:
|
| 295 |
+
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
| 296 |
+
|
| 297 |
+
estree-walker@3.0.3:
|
| 298 |
+
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
| 299 |
+
|
| 300 |
+
expect-type@1.4.0:
|
| 301 |
+
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
|
| 302 |
+
engines: {node: '>=12.0.0'}
|
| 303 |
+
|
| 304 |
+
fdir@6.5.0:
|
| 305 |
+
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
| 306 |
+
engines: {node: '>=12.0.0'}
|
| 307 |
+
peerDependencies:
|
| 308 |
+
picomatch: ^3 || ^4
|
| 309 |
+
peerDependenciesMeta:
|
| 310 |
+
picomatch:
|
| 311 |
+
optional: true
|
| 312 |
+
|
| 313 |
+
fsevents@2.3.3:
|
| 314 |
+
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
| 315 |
+
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
| 316 |
+
os: [darwin]
|
| 317 |
+
|
| 318 |
+
lightningcss-android-arm64@1.33.0:
|
| 319 |
+
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
|
| 320 |
+
engines: {node: '>= 12.0.0'}
|
| 321 |
+
cpu: [arm64]
|
| 322 |
+
os: [android]
|
| 323 |
+
|
| 324 |
+
lightningcss-darwin-arm64@1.33.0:
|
| 325 |
+
resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
|
| 326 |
+
engines: {node: '>= 12.0.0'}
|
| 327 |
+
cpu: [arm64]
|
| 328 |
+
os: [darwin]
|
| 329 |
+
|
| 330 |
+
lightningcss-darwin-x64@1.33.0:
|
| 331 |
+
resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
|
| 332 |
+
engines: {node: '>= 12.0.0'}
|
| 333 |
+
cpu: [x64]
|
| 334 |
+
os: [darwin]
|
| 335 |
+
|
| 336 |
+
lightningcss-freebsd-x64@1.33.0:
|
| 337 |
+
resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
|
| 338 |
+
engines: {node: '>= 12.0.0'}
|
| 339 |
+
cpu: [x64]
|
| 340 |
+
os: [freebsd]
|
| 341 |
+
|
| 342 |
+
lightningcss-linux-arm-gnueabihf@1.33.0:
|
| 343 |
+
resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
|
| 344 |
+
engines: {node: '>= 12.0.0'}
|
| 345 |
+
cpu: [arm]
|
| 346 |
+
os: [linux]
|
| 347 |
+
|
| 348 |
+
lightningcss-linux-arm64-gnu@1.33.0:
|
| 349 |
+
resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
|
| 350 |
+
engines: {node: '>= 12.0.0'}
|
| 351 |
+
cpu: [arm64]
|
| 352 |
+
os: [linux]
|
| 353 |
+
libc: [glibc]
|
| 354 |
+
|
| 355 |
+
lightningcss-linux-arm64-musl@1.33.0:
|
| 356 |
+
resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
|
| 357 |
+
engines: {node: '>= 12.0.0'}
|
| 358 |
+
cpu: [arm64]
|
| 359 |
+
os: [linux]
|
| 360 |
+
libc: [musl]
|
| 361 |
+
|
| 362 |
+
lightningcss-linux-x64-gnu@1.33.0:
|
| 363 |
+
resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
|
| 364 |
+
engines: {node: '>= 12.0.0'}
|
| 365 |
+
cpu: [x64]
|
| 366 |
+
os: [linux]
|
| 367 |
+
libc: [glibc]
|
| 368 |
+
|
| 369 |
+
lightningcss-linux-x64-musl@1.33.0:
|
| 370 |
+
resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
|
| 371 |
+
engines: {node: '>= 12.0.0'}
|
| 372 |
+
cpu: [x64]
|
| 373 |
+
os: [linux]
|
| 374 |
+
libc: [musl]
|
| 375 |
+
|
| 376 |
+
lightningcss-win32-arm64-msvc@1.33.0:
|
| 377 |
+
resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
|
| 378 |
+
engines: {node: '>= 12.0.0'}
|
| 379 |
+
cpu: [arm64]
|
| 380 |
+
os: [win32]
|
| 381 |
+
|
| 382 |
+
lightningcss-win32-x64-msvc@1.33.0:
|
| 383 |
+
resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
|
| 384 |
+
engines: {node: '>= 12.0.0'}
|
| 385 |
+
cpu: [x64]
|
| 386 |
+
os: [win32]
|
| 387 |
+
|
| 388 |
+
lightningcss@1.33.0:
|
| 389 |
+
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
| 390 |
+
engines: {node: '>= 12.0.0'}
|
| 391 |
+
|
| 392 |
+
magic-string@1.4.3:
|
| 393 |
+
resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==}
|
| 394 |
+
|
| 395 |
+
mp4box@2.4.1:
|
| 396 |
+
resolution: {integrity: sha512-0HGX7nXoDIX6FKLVl4a3wtYjBlwqsN3xuQC3GXzNtKp98FXUOhDSq623azsz8DG5ptd9ZXcXodDkgbdMZOjWvw==}
|
| 397 |
+
engines: {node: '>=20.8.1'}
|
| 398 |
+
|
| 399 |
+
nanoid@3.3.20:
|
| 400 |
+
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
|
| 401 |
+
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
| 402 |
+
hasBin: true
|
| 403 |
+
|
| 404 |
+
obug@2.2.1:
|
| 405 |
+
resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
|
| 406 |
+
engines: {node: '>=12.20.0'}
|
| 407 |
+
|
| 408 |
+
picocolors@1.1.1:
|
| 409 |
+
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
| 410 |
+
|
| 411 |
+
picomatch@4.0.7:
|
| 412 |
+
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
| 413 |
+
engines: {node: '>=12'}
|
| 414 |
+
|
| 415 |
+
postcss@8.5.29:
|
| 416 |
+
resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
|
| 417 |
+
engines: {node: ^10 || ^12 || >=14}
|
| 418 |
+
|
| 419 |
+
rolldown@1.2.13:
|
| 420 |
+
resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==}
|
| 421 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 422 |
+
hasBin: true
|
| 423 |
+
|
| 424 |
+
source-map-js@1.2.2:
|
| 425 |
+
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
| 426 |
+
engines: {node: '>=0.10.0'}
|
| 427 |
+
|
| 428 |
+
std-env@4.3.0:
|
| 429 |
+
resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
|
| 430 |
+
|
| 431 |
+
tinybench@6.2.2:
|
| 432 |
+
resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
|
| 433 |
+
engines: {node: '>=20.0.0'}
|
| 434 |
+
|
| 435 |
+
tinyexec@1.3.1:
|
| 436 |
+
resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
|
| 437 |
+
engines: {node: '>=18'}
|
| 438 |
+
|
| 439 |
+
tinyglobby@0.2.17:
|
| 440 |
+
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
| 441 |
+
engines: {node: '>=12.0.0'}
|
| 442 |
+
|
| 443 |
+
typescript@7.0.2:
|
| 444 |
+
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
|
| 445 |
+
engines: {node: '>=16.20.0'}
|
| 446 |
+
hasBin: true
|
| 447 |
+
|
| 448 |
+
vite@8.3.4:
|
| 449 |
+
resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==}
|
| 450 |
+
engines: {node: ^20.19.0 || >=22.12.0}
|
| 451 |
+
hasBin: true
|
| 452 |
+
peerDependencies:
|
| 453 |
+
'@types/node': ^20.19.0 || >=22.12.0
|
| 454 |
+
'@vitejs/devtools': ^0.7.1
|
| 455 |
+
esbuild: ^0.27.0 || ^0.28.0
|
| 456 |
+
jiti: '>=1.21.0'
|
| 457 |
+
less: ^4.0.0
|
| 458 |
+
sass: ^1.70.0
|
| 459 |
+
sass-embedded: ^1.70.0
|
| 460 |
+
stylus: '>=0.54.8'
|
| 461 |
+
sugarss: ^5.0.0
|
| 462 |
+
terser: ^5.16.0
|
| 463 |
+
tsx: ^4.8.1
|
| 464 |
+
yaml: ^2.4.2
|
| 465 |
+
peerDependenciesMeta:
|
| 466 |
+
'@types/node':
|
| 467 |
+
optional: true
|
| 468 |
+
'@vitejs/devtools':
|
| 469 |
+
optional: true
|
| 470 |
+
esbuild:
|
| 471 |
+
optional: true
|
| 472 |
+
jiti:
|
| 473 |
+
optional: true
|
| 474 |
+
less:
|
| 475 |
+
optional: true
|
| 476 |
+
sass:
|
| 477 |
+
optional: true
|
| 478 |
+
sass-embedded:
|
| 479 |
+
optional: true
|
| 480 |
+
stylus:
|
| 481 |
+
optional: true
|
| 482 |
+
sugarss:
|
| 483 |
+
optional: true
|
| 484 |
+
terser:
|
| 485 |
+
optional: true
|
| 486 |
+
tsx:
|
| 487 |
+
optional: true
|
| 488 |
+
yaml:
|
| 489 |
+
optional: true
|
| 490 |
+
|
| 491 |
+
vitest@5.0.3:
|
| 492 |
+
resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==}
|
| 493 |
+
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
|
| 494 |
+
hasBin: true
|
| 495 |
+
peerDependencies:
|
| 496 |
+
'@edge-runtime/vm': '*'
|
| 497 |
+
'@opentelemetry/api': ^1.9.0
|
| 498 |
+
'@types/node': ^22.0.0 || >=24.0.0
|
| 499 |
+
'@vitest/browser-playwright': 5.0.3
|
| 500 |
+
'@vitest/browser-preview': 5.0.3
|
| 501 |
+
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
|
| 502 |
+
'@vitest/coverage-istanbul': 5.0.3
|
| 503 |
+
'@vitest/coverage-v8': 5.0.3
|
| 504 |
+
'@vitest/ui': 5.0.3
|
| 505 |
+
happy-dom: '*'
|
| 506 |
+
jsdom: '*'
|
| 507 |
+
vite: ^6.4.0 || ^7.0.0 || ^8.0.0
|
| 508 |
+
peerDependenciesMeta:
|
| 509 |
+
'@edge-runtime/vm':
|
| 510 |
+
optional: true
|
| 511 |
+
'@opentelemetry/api':
|
| 512 |
+
optional: true
|
| 513 |
+
'@types/node':
|
| 514 |
+
optional: true
|
| 515 |
+
'@vitest/browser-playwright':
|
| 516 |
+
optional: true
|
| 517 |
+
'@vitest/browser-preview':
|
| 518 |
+
optional: true
|
| 519 |
+
'@vitest/browser-webdriverio':
|
| 520 |
+
optional: true
|
| 521 |
+
'@vitest/coverage-istanbul':
|
| 522 |
+
optional: true
|
| 523 |
+
'@vitest/coverage-v8':
|
| 524 |
+
optional: true
|
| 525 |
+
'@vitest/ui':
|
| 526 |
+
optional: true
|
| 527 |
+
happy-dom:
|
| 528 |
+
optional: true
|
| 529 |
+
jsdom:
|
| 530 |
+
optional: true
|
| 531 |
+
|
| 532 |
+
why-is-node-running@3.2.1:
|
| 533 |
+
resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
|
| 534 |
+
engines: {node: '>=20.11'}
|
| 535 |
+
hasBin: true
|
| 536 |
+
|
| 537 |
+
snapshots:
|
| 538 |
+
|
| 539 |
+
'@jridgewell/resolve-uri@3.1.2': {}
|
| 540 |
+
|
| 541 |
+
'@jridgewell/sourcemap-codec@1.6.0': {}
|
| 542 |
+
|
| 543 |
+
'@jridgewell/trace-mapping@0.3.31':
|
| 544 |
+
dependencies:
|
| 545 |
+
'@jridgewell/resolve-uri': 3.1.2
|
| 546 |
+
'@jridgewell/sourcemap-codec': 1.6.0
|
| 547 |
+
|
| 548 |
+
'@oxc-project/types@0.153.0': {}
|
| 549 |
+
|
| 550 |
+
'@rolldown/binding-android-arm-eabi@1.2.13':
|
| 551 |
+
optional: true
|
| 552 |
+
|
| 553 |
+
'@rolldown/binding-android-arm64@1.2.13':
|
| 554 |
+
optional: true
|
| 555 |
+
|
| 556 |
+
'@rolldown/binding-darwin-arm64@1.2.13':
|
| 557 |
+
optional: true
|
| 558 |
+
|
| 559 |
+
'@rolldown/binding-darwin-x64@1.2.13':
|
| 560 |
+
optional: true
|
| 561 |
+
|
| 562 |
+
'@rolldown/binding-freebsd-x64@1.2.13':
|
| 563 |
+
optional: true
|
| 564 |
+
|
| 565 |
+
'@rolldown/binding-linux-arm-gnueabihf@1.2.13':
|
| 566 |
+
optional: true
|
| 567 |
+
|
| 568 |
+
'@rolldown/binding-linux-arm64-gnu@1.2.13':
|
| 569 |
+
optional: true
|
| 570 |
+
|
| 571 |
+
'@rolldown/binding-linux-arm64-musl@1.2.13':
|
| 572 |
+
optional: true
|
| 573 |
+
|
| 574 |
+
'@rolldown/binding-linux-ppc64-gnu@1.2.13':
|
| 575 |
+
optional: true
|
| 576 |
+
|
| 577 |
+
'@rolldown/binding-linux-s390x-gnu@1.2.13':
|
| 578 |
+
optional: true
|
| 579 |
+
|
| 580 |
+
'@rolldown/binding-linux-x64-gnu@1.2.13':
|
| 581 |
+
optional: true
|
| 582 |
+
|
| 583 |
+
'@rolldown/binding-linux-x64-musl@1.2.13':
|
| 584 |
+
optional: true
|
| 585 |
+
|
| 586 |
+
'@rolldown/binding-openharmony-arm64@1.2.13':
|
| 587 |
+
optional: true
|
| 588 |
+
|
| 589 |
+
'@rolldown/binding-win32-arm64-msvc@1.2.13':
|
| 590 |
+
optional: true
|
| 591 |
+
|
| 592 |
+
'@rolldown/binding-win32-x64-msvc@1.2.13':
|
| 593 |
+
optional: true
|
| 594 |
+
|
| 595 |
+
'@rolldown/pluginutils@1.0.1': {}
|
| 596 |
+
|
| 597 |
+
'@types/chai@5.2.3':
|
| 598 |
+
dependencies:
|
| 599 |
+
'@types/deep-eql': 4.0.2
|
| 600 |
+
assertion-error: 2.0.1
|
| 601 |
+
|
| 602 |
+
'@types/deep-eql@4.0.2': {}
|
| 603 |
+
|
| 604 |
+
'@types/estree@1.0.9': {}
|
| 605 |
+
|
| 606 |
+
'@typescript/typescript-aix-ppc64@7.0.2':
|
| 607 |
+
optional: true
|
| 608 |
+
|
| 609 |
+
'@typescript/typescript-darwin-arm64@7.0.2':
|
| 610 |
+
optional: true
|
| 611 |
+
|
| 612 |
+
'@typescript/typescript-darwin-x64@7.0.2':
|
| 613 |
+
optional: true
|
| 614 |
+
|
| 615 |
+
'@typescript/typescript-freebsd-arm64@7.0.2':
|
| 616 |
+
optional: true
|
| 617 |
+
|
| 618 |
+
'@typescript/typescript-freebsd-x64@7.0.2':
|
| 619 |
+
optional: true
|
| 620 |
+
|
| 621 |
+
'@typescript/typescript-linux-arm64@7.0.2':
|
| 622 |
+
optional: true
|
| 623 |
+
|
| 624 |
+
'@typescript/typescript-linux-arm@7.0.2':
|
| 625 |
+
optional: true
|
| 626 |
+
|
| 627 |
+
'@typescript/typescript-linux-loong64@7.0.2':
|
| 628 |
+
optional: true
|
| 629 |
+
|
| 630 |
+
'@typescript/typescript-linux-mips64el@7.0.2':
|
| 631 |
+
optional: true
|
| 632 |
+
|
| 633 |
+
'@typescript/typescript-linux-ppc64@7.0.2':
|
| 634 |
+
optional: true
|
| 635 |
+
|
| 636 |
+
'@typescript/typescript-linux-riscv64@7.0.2':
|
| 637 |
+
optional: true
|
| 638 |
+
|
| 639 |
+
'@typescript/typescript-linux-s390x@7.0.2':
|
| 640 |
+
optional: true
|
| 641 |
+
|
| 642 |
+
'@typescript/typescript-linux-x64@7.0.2':
|
| 643 |
+
optional: true
|
| 644 |
+
|
| 645 |
+
'@typescript/typescript-netbsd-arm64@7.0.2':
|
| 646 |
+
optional: true
|
| 647 |
+
|
| 648 |
+
'@typescript/typescript-netbsd-x64@7.0.2':
|
| 649 |
+
optional: true
|
| 650 |
+
|
| 651 |
+
'@typescript/typescript-openbsd-arm64@7.0.2':
|
| 652 |
+
optional: true
|
| 653 |
+
|
| 654 |
+
'@typescript/typescript-openbsd-x64@7.0.2':
|
| 655 |
+
optional: true
|
| 656 |
+
|
| 657 |
+
'@typescript/typescript-sunos-x64@7.0.2':
|
| 658 |
+
optional: true
|
| 659 |
+
|
| 660 |
+
'@typescript/typescript-win32-arm64@7.0.2':
|
| 661 |
+
optional: true
|
| 662 |
+
|
| 663 |
+
'@typescript/typescript-win32-x64@7.0.2':
|
| 664 |
+
optional: true
|
| 665 |
+
|
| 666 |
+
'@vitest/mocker@5.0.3(vite@8.3.4)':
|
| 667 |
+
dependencies:
|
| 668 |
+
'@jridgewell/trace-mapping': 0.3.31
|
| 669 |
+
'@vitest/spy': 5.0.3
|
| 670 |
+
estree-walker: 3.0.3
|
| 671 |
+
magic-string: 1.4.3
|
| 672 |
+
optionalDependencies:
|
| 673 |
+
vite: 8.3.4
|
| 674 |
+
|
| 675 |
+
'@vitest/spy@5.0.3': {}
|
| 676 |
+
|
| 677 |
+
assertion-error@2.0.1: {}
|
| 678 |
+
|
| 679 |
+
chai@6.3.0: {}
|
| 680 |
+
|
| 681 |
+
detect-libc@2.1.2: {}
|
| 682 |
+
|
| 683 |
+
es-module-lexer@2.3.2: {}
|
| 684 |
+
|
| 685 |
+
estree-walker@3.0.3:
|
| 686 |
+
dependencies:
|
| 687 |
+
'@types/estree': 1.0.9
|
| 688 |
+
|
| 689 |
+
expect-type@1.4.0: {}
|
| 690 |
+
|
| 691 |
+
fdir@6.5.0(picomatch@4.0.7):
|
| 692 |
+
optionalDependencies:
|
| 693 |
+
picomatch: 4.0.7
|
| 694 |
+
|
| 695 |
+
fsevents@2.3.3:
|
| 696 |
+
optional: true
|
| 697 |
+
|
| 698 |
+
lightningcss-android-arm64@1.33.0:
|
| 699 |
+
optional: true
|
| 700 |
+
|
| 701 |
+
lightningcss-darwin-arm64@1.33.0:
|
| 702 |
+
optional: true
|
| 703 |
+
|
| 704 |
+
lightningcss-darwin-x64@1.33.0:
|
| 705 |
+
optional: true
|
| 706 |
+
|
| 707 |
+
lightningcss-freebsd-x64@1.33.0:
|
| 708 |
+
optional: true
|
| 709 |
+
|
| 710 |
+
lightningcss-linux-arm-gnueabihf@1.33.0:
|
| 711 |
+
optional: true
|
| 712 |
+
|
| 713 |
+
lightningcss-linux-arm64-gnu@1.33.0:
|
| 714 |
+
optional: true
|
| 715 |
+
|
| 716 |
+
lightningcss-linux-arm64-musl@1.33.0:
|
| 717 |
+
optional: true
|
| 718 |
+
|
| 719 |
+
lightningcss-linux-x64-gnu@1.33.0:
|
| 720 |
+
optional: true
|
| 721 |
+
|
| 722 |
+
lightningcss-linux-x64-musl@1.33.0:
|
| 723 |
+
optional: true
|
| 724 |
+
|
| 725 |
+
lightningcss-win32-arm64-msvc@1.33.0:
|
| 726 |
+
optional: true
|
| 727 |
+
|
| 728 |
+
lightningcss-win32-x64-msvc@1.33.0:
|
| 729 |
+
optional: true
|
| 730 |
+
|
| 731 |
+
lightningcss@1.33.0:
|
| 732 |
+
dependencies:
|
| 733 |
+
detect-libc: 2.1.2
|
| 734 |
+
optionalDependencies:
|
| 735 |
+
lightningcss-android-arm64: 1.33.0
|
| 736 |
+
lightningcss-darwin-arm64: 1.33.0
|
| 737 |
+
lightningcss-darwin-x64: 1.33.0
|
| 738 |
+
lightningcss-freebsd-x64: 1.33.0
|
| 739 |
+
lightningcss-linux-arm-gnueabihf: 1.33.0
|
| 740 |
+
lightningcss-linux-arm64-gnu: 1.33.0
|
| 741 |
+
lightningcss-linux-arm64-musl: 1.33.0
|
| 742 |
+
lightningcss-linux-x64-gnu: 1.33.0
|
| 743 |
+
lightningcss-linux-x64-musl: 1.33.0
|
| 744 |
+
lightningcss-win32-arm64-msvc: 1.33.0
|
| 745 |
+
lightningcss-win32-x64-msvc: 1.33.0
|
| 746 |
+
|
| 747 |
+
magic-string@1.4.3:
|
| 748 |
+
dependencies:
|
| 749 |
+
'@jridgewell/sourcemap-codec': 1.6.0
|
| 750 |
+
|
| 751 |
+
mp4box@2.4.1: {}
|
| 752 |
+
|
| 753 |
+
nanoid@3.3.20: {}
|
| 754 |
+
|
| 755 |
+
obug@2.2.1: {}
|
| 756 |
+
|
| 757 |
+
picocolors@1.1.1: {}
|
| 758 |
+
|
| 759 |
+
picomatch@4.0.7: {}
|
| 760 |
+
|
| 761 |
+
postcss@8.5.29:
|
| 762 |
+
dependencies:
|
| 763 |
+
nanoid: 3.3.20
|
| 764 |
+
picocolors: 1.1.1
|
| 765 |
+
source-map-js: 1.2.2
|
| 766 |
+
|
| 767 |
+
rolldown@1.2.13:
|
| 768 |
+
dependencies:
|
| 769 |
+
'@oxc-project/types': 0.153.0
|
| 770 |
+
'@rolldown/pluginutils': 1.0.1
|
| 771 |
+
optionalDependencies:
|
| 772 |
+
'@rolldown/binding-android-arm-eabi': 1.2.13
|
| 773 |
+
'@rolldown/binding-android-arm64': 1.2.13
|
| 774 |
+
'@rolldown/binding-darwin-arm64': 1.2.13
|
| 775 |
+
'@rolldown/binding-darwin-x64': 1.2.13
|
| 776 |
+
'@rolldown/binding-freebsd-x64': 1.2.13
|
| 777 |
+
'@rolldown/binding-linux-arm-gnueabihf': 1.2.13
|
| 778 |
+
'@rolldown/binding-linux-arm64-gnu': 1.2.13
|
| 779 |
+
'@rolldown/binding-linux-arm64-musl': 1.2.13
|
| 780 |
+
'@rolldown/binding-linux-ppc64-gnu': 1.2.13
|
| 781 |
+
'@rolldown/binding-linux-s390x-gnu': 1.2.13
|
| 782 |
+
'@rolldown/binding-linux-x64-gnu': 1.2.13
|
| 783 |
+
'@rolldown/binding-linux-x64-musl': 1.2.13
|
| 784 |
+
'@rolldown/binding-openharmony-arm64': 1.2.13
|
| 785 |
+
'@rolldown/binding-win32-arm64-msvc': 1.2.13
|
| 786 |
+
'@rolldown/binding-win32-x64-msvc': 1.2.13
|
| 787 |
+
|
| 788 |
+
source-map-js@1.2.2: {}
|
| 789 |
+
|
| 790 |
+
std-env@4.3.0: {}
|
| 791 |
+
|
| 792 |
+
tinybench@6.2.2: {}
|
| 793 |
+
|
| 794 |
+
tinyexec@1.3.1: {}
|
| 795 |
+
|
| 796 |
+
tinyglobby@0.2.17:
|
| 797 |
+
dependencies:
|
| 798 |
+
fdir: 6.5.0(picomatch@4.0.7)
|
| 799 |
+
picomatch: 4.0.7
|
| 800 |
+
|
| 801 |
+
typescript@7.0.2:
|
| 802 |
+
optionalDependencies:
|
| 803 |
+
'@typescript/typescript-aix-ppc64': 7.0.2
|
| 804 |
+
'@typescript/typescript-darwin-arm64': 7.0.2
|
| 805 |
+
'@typescript/typescript-darwin-x64': 7.0.2
|
| 806 |
+
'@typescript/typescript-freebsd-arm64': 7.0.2
|
| 807 |
+
'@typescript/typescript-freebsd-x64': 7.0.2
|
| 808 |
+
'@typescript/typescript-linux-arm': 7.0.2
|
| 809 |
+
'@typescript/typescript-linux-arm64': 7.0.2
|
| 810 |
+
'@typescript/typescript-linux-loong64': 7.0.2
|
| 811 |
+
'@typescript/typescript-linux-mips64el': 7.0.2
|
| 812 |
+
'@typescript/typescript-linux-ppc64': 7.0.2
|
| 813 |
+
'@typescript/typescript-linux-riscv64': 7.0.2
|
| 814 |
+
'@typescript/typescript-linux-s390x': 7.0.2
|
| 815 |
+
'@typescript/typescript-linux-x64': 7.0.2
|
| 816 |
+
'@typescript/typescript-netbsd-arm64': 7.0.2
|
| 817 |
+
'@typescript/typescript-netbsd-x64': 7.0.2
|
| 818 |
+
'@typescript/typescript-openbsd-arm64': 7.0.2
|
| 819 |
+
'@typescript/typescript-openbsd-x64': 7.0.2
|
| 820 |
+
'@typescript/typescript-sunos-x64': 7.0.2
|
| 821 |
+
'@typescript/typescript-win32-arm64': 7.0.2
|
| 822 |
+
'@typescript/typescript-win32-x64': 7.0.2
|
| 823 |
+
|
| 824 |
+
vite@8.3.4:
|
| 825 |
+
dependencies:
|
| 826 |
+
lightningcss: 1.33.0
|
| 827 |
+
picomatch: 4.0.7
|
| 828 |
+
postcss: 8.5.29
|
| 829 |
+
rolldown: 1.2.13
|
| 830 |
+
tinyglobby: 0.2.17
|
| 831 |
+
optionalDependencies:
|
| 832 |
+
fsevents: 2.3.3
|
| 833 |
+
|
| 834 |
+
vitest@5.0.3(vite@8.3.4):
|
| 835 |
+
dependencies:
|
| 836 |
+
'@types/chai': 5.2.3
|
| 837 |
+
'@vitest/mocker': 5.0.3(vite@8.3.4)
|
| 838 |
+
chai: 6.3.0
|
| 839 |
+
es-module-lexer: 2.3.2
|
| 840 |
+
expect-type: 1.4.0
|
| 841 |
+
magic-string: 1.4.3
|
| 842 |
+
obug: 2.2.1
|
| 843 |
+
picomatch: 4.0.7
|
| 844 |
+
std-env: 4.3.0
|
| 845 |
+
tinybench: 6.2.2
|
| 846 |
+
tinyexec: 1.3.1
|
| 847 |
+
tinyglobby: 0.2.17
|
| 848 |
+
vite: 8.3.4
|
| 849 |
+
why-is-node-running: 3.2.1
|
| 850 |
+
transitivePeerDependencies:
|
| 851 |
+
- msw
|
| 852 |
+
|
| 853 |
+
why-is-node-running@3.2.1: {}
|
public/demo/poster.png
ADDED
|
Git LFS Details
|
public/demo/video_01.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:749b2d89b1c06d0f64458ba7747e1e3e01003efed4be4711627dea30186f7d25
|
| 3 |
+
size 15095453
|
recording-comic/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# README 首屏演示录制页
|
| 2 |
+
|
| 3 |
+
这个文件夹保存的是一个独立、临时的三格漫画页面,用于制作 Orbit Video Viewer 开源项目在 GitHub 和 Hugging Face 上的首屏演示视频或 GIF。
|
| 4 |
+
|
| 5 |
+
## 为什么制作这个页面
|
| 6 |
+
|
| 7 |
+
Orbit Video Viewer 的核心能力是:把一段已经生成好的环绕视频转换成可由鼠标拖动视角的网页交互。
|
| 8 |
+
|
| 9 |
+
普通产品界面能够证明功能可以运行,但不容易在 README 的第一眼同时展示“漫画应用场景”和“拖动后视角发生变化”。因此增加了这个专门用于录屏的页面:静止时看起来是一张完整的黑白漫画页,鼠标进入第一格并拖动时,主画面的观察角度会连续变化。
|
| 10 |
+
|
| 11 |
+
这个页面不是新的产品功能,也不是漫画编辑器。它只是开源发布阶段的展示素材制作工具,与正式 Demo 和 Core SDK 分开保存。
|
| 12 |
+
|
| 13 |
+
## 页面具体做了什么
|
| 14 |
+
|
| 15 |
+
- 使用“上方一个大横格、下方两个并排格”的三格漫画布局。
|
| 16 |
+
- 第一格接入现有 `OrbitViewer` SDK,可以用鼠标水平拖动改变视角。
|
| 17 |
+
- 第一格直接复用项目已有的 `video_01.mp4` 环绕视频,没有重新生成视频。
|
| 18 |
+
- 第一格加载前使用 `img_01.png` 作为静态画面。
|
| 19 |
+
- 下方两格使用 `img_02.png` 和 `img_03.png`,保持静止并尽量完整显示。
|
| 20 |
+
- 加入少量英文漫画文字,避免页面变成技术说明页。
|
| 21 |
+
- 页面外围使用纯白背景,即使录屏或截图时上下带有留白,也会呈现漫画纸张的感觉。
|
| 22 |
+
- 没有修改或重新实现 Core SDK,也没有覆盖原 Orbit Comic Demo。
|
| 23 |
+
|
| 24 |
+
## 两种使用模式
|
| 25 |
+
|
| 26 |
+
### 1. 正常交互模式
|
| 27 |
+
|
| 28 |
+
打开:
|
| 29 |
+
|
| 30 |
+
```text
|
| 31 |
+
http://127.0.0.1:5173/recording-comic/
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
用户可以亲自把鼠标移入第一格并左右拖动。这个模式适合手动录制新的演示素材。
|
| 35 |
+
|
| 36 |
+
### 2. 自动录制模式
|
| 37 |
+
|
| 38 |
+
打开:
|
| 39 |
+
|
| 40 |
+
```text
|
| 41 |
+
http://127.0.0.1:5173/recording-comic/?capture=1
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
这个模式会自动模拟一段短演示:
|
| 45 |
+
|
| 46 |
+
1. 箭头光标从页面留白处进入第一格;
|
| 47 |
+
2. 光标变成抓手;
|
| 48 |
+
3. 抓手左右拖动;
|
| 49 |
+
4. 第一格随之切换环绕视角;
|
| 50 |
+
5. 下方两个分镜始终保持静止。
|
| 51 |
+
|
| 52 |
+
自动模式只用于录制,不会出现在普通交互页面中。
|
| 53 |
+
|
| 54 |
+
## 本地启动
|
| 55 |
+
|
| 56 |
+
在 `orbit-panel-viewer` 项目根目录运行:
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
pnpm install
|
| 60 |
+
pnpm dev
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
然后打开上面的正常模式或自动录制模式地址。不要直接双击 HTML 文件,因为 MP4 解码和模块加载需要通过本地开发服务器运行。
|
| 64 |
+
|
| 65 |
+
## 已生成的展示文件
|
| 66 |
+
|
| 67 |
+
`output/` 目录中包含当前生成的成品:
|
| 68 |
+
|
| 69 |
+
- `orbit-comic-demo.mp4`:约 8.3 秒,1200×832,适合视频上传或进一步压缩;
|
| 70 |
+
- `orbit-comic-demo.gif`:约 8.3 秒,800×555,可直接作为 README 动图预览。
|
| 71 |
+
|
| 72 |
+
如果重新录制,可以替换这些输出文件,但不需要改动 SDK 或原始环绕视频。
|
| 73 |
+
|
| 74 |
+
## 文件关系
|
| 75 |
+
|
| 76 |
+
```text
|
| 77 |
+
recording-comic/
|
| 78 |
+
├── assets/ # 录制页下方两格的静态分镜
|
| 79 |
+
├── index.html # 三格漫画结构
|
| 80 |
+
├── main.ts # SDK 接入和自动演示流程
|
| 81 |
+
├── style.css # 漫画分镜、白底和光标样式
|
| 82 |
+
├── output/ # 已生成的 MP4 和 GIF
|
| 83 |
+
└── README.md # 本说明
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
页面通过现有工程读取以下素材:
|
| 87 |
+
|
| 88 |
+
```text
|
| 89 |
+
../public/demo/video_01.mp4
|
| 90 |
+
../public/demo/poster.png
|
| 91 |
+
./assets/shot.png
|
| 92 |
+
./assets/impact.png
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
录制页所需文件现在都位于公开仓库内部,可以在独立 clone 后复现,不再依赖仓库外的相邻目录。`assets/shot.png` 和 `assets/impact.png` 只服务录制页与生成的首屏素材,不属于 SDK,也不会进入正式 Demo 的生产构建。
|
| 96 |
+
|
| 97 |
+
## 开源发布时的定位
|
| 98 |
+
|
| 99 |
+
建议将生成的视频或 GIF 放在项目主 README 的首屏,用来快速说明交互效果;这个录制页面本身可以保留为可复现展示素材的辅助页面,但不应被描述成 SDK 的必要组成部分。
|
| 100 |
+
|
| 101 |
+
核心开源内容仍然是 `OrbitViewer` SDK。这个文件夹只负责把核心能力放进一个容易理解、容易录制的漫画场景中。
|
recording-comic/assets/impact.png
ADDED
|
Git LFS Details
|
recording-comic/assets/shot.png
ADDED
|
Git LFS Details
|
recording-comic/index.html
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="theme-color" content="#d9d6ce" />
|
| 7 |
+
<meta
|
| 8 |
+
name="description"
|
| 9 |
+
content="A temporary three-panel Orbit Comic page for recording the README demo."
|
| 10 |
+
/>
|
| 11 |
+
<title>Orbit Comic — Recording Page</title>
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
<main class="capture-stage" aria-label="Three-panel interactive comic">
|
| 15 |
+
<article class="comic-page">
|
| 16 |
+
<section
|
| 17 |
+
id="orbit-panel"
|
| 18 |
+
class="panel panel--hero is-loading"
|
| 19 |
+
tabindex="0"
|
| 20 |
+
role="application"
|
| 21 |
+
aria-label="Interactive desert comic panel. Drag horizontally to change the view."
|
| 22 |
+
>
|
| 23 |
+
<img id="hero-poster" class="panel-image hero-poster" alt="A man and motorcycle in a desert" />
|
| 24 |
+
<canvas id="orbit-canvas" aria-hidden="true"></canvas>
|
| 25 |
+
<div id="load-state" class="load-state" aria-live="polite">
|
| 26 |
+
<span id="load-label">Preparing scene</span>
|
| 27 |
+
<span class="load-track" aria-hidden="true"><span id="load-progress"></span></span>
|
| 28 |
+
</div>
|
| 29 |
+
</section>
|
| 30 |
+
|
| 31 |
+
<div class="lower-panels">
|
| 32 |
+
<section class="panel panel--detail panel--shot" aria-label="A close-up of the gunshot">
|
| 33 |
+
<img id="shot-image" class="panel-image" alt="A close-up of the man firing a handgun" />
|
| 34 |
+
<div class="speech-bubble" aria-label="Ellipsis">…</div>
|
| 35 |
+
</section>
|
| 36 |
+
|
| 37 |
+
<section class="panel panel--detail panel--impact" aria-label="A bullet strikes a hat">
|
| 38 |
+
<img id="impact-image" class="panel-image" alt="A bullet striking a hat above the desert" />
|
| 39 |
+
<p class="caption-box">Something’s off.</p>
|
| 40 |
+
</section>
|
| 41 |
+
</div>
|
| 42 |
+
</article>
|
| 43 |
+
|
| 44 |
+
<p id="status" class="sr-only" aria-live="polite">Preparing the interactive panel.</p>
|
| 45 |
+
<div id="demo-cursor" class="demo-cursor" aria-hidden="true">
|
| 46 |
+
<svg class="demo-cursor__pointer" viewBox="0 0 32 38" role="presentation">
|
| 47 |
+
<path d="M3 2.5v28l7.1-6.2 5.2 10.4 5-2.6-5.2-9.9 10.2-.8z" />
|
| 48 |
+
</svg>
|
| 49 |
+
<svg class="demo-cursor__hand" viewBox="0 0 50 58" role="presentation">
|
| 50 |
+
<rect x="9" y="10" width="8" height="29" rx="4" />
|
| 51 |
+
<rect x="18" y="4" width="8" height="35" rx="4" />
|
| 52 |
+
<rect x="27" y="7" width="8" height="32" rx="4" />
|
| 53 |
+
<rect x="36" y="13" width="8" height="27" rx="4" />
|
| 54 |
+
<path d="M10 30h29c5 0 8 4 8 9v2c0 9-7 14-17 14h-9c-9 0-14-4-18-12L.9 38c-1.1-2.5 0-5.4 2.5-6.5 2.1-.9 4.5-.2 5.8 1.8l4 6.1" />
|
| 55 |
+
</svg>
|
| 56 |
+
</div>
|
| 57 |
+
</main>
|
| 58 |
+
|
| 59 |
+
<script type="module" src="./main.ts"></script>
|
| 60 |
+
</body>
|
| 61 |
+
</html>
|
recording-comic/main.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import './style.css';
|
| 2 |
+
import { OrbitViewer, createMp4Source } from '../src/library';
|
| 3 |
+
import shotImageUrl from './assets/shot.png?url';
|
| 4 |
+
import impactImageUrl from './assets/impact.png?url';
|
| 5 |
+
|
| 6 |
+
const find = <T extends Element>(selector: string): T => {
|
| 7 |
+
const element = document.querySelector<T>(selector);
|
| 8 |
+
if (!element) throw new Error(`Missing element: ${selector}`);
|
| 9 |
+
return element;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
const panel = find<HTMLElement>('#orbit-panel');
|
| 13 |
+
const canvas = find<HTMLCanvasElement>('#orbit-canvas');
|
| 14 |
+
const poster = find<HTMLImageElement>('#hero-poster');
|
| 15 |
+
const shotImage = find<HTMLImageElement>('#shot-image');
|
| 16 |
+
const impactImage = find<HTMLImageElement>('#impact-image');
|
| 17 |
+
const loadState = find<HTMLElement>('#load-state');
|
| 18 |
+
const loadLabel = find<HTMLElement>('#load-label');
|
| 19 |
+
const loadProgress = find<HTMLElement>('#load-progress');
|
| 20 |
+
const status = find<HTMLElement>('#status');
|
| 21 |
+
const demoCursor = find<HTMLElement>('#demo-cursor');
|
| 22 |
+
|
| 23 |
+
poster.src = '../demo/poster.png';
|
| 24 |
+
shotImage.src = shotImageUrl;
|
| 25 |
+
impactImage.src = impactImageUrl;
|
| 26 |
+
|
| 27 |
+
const viewer = new OrbitViewer({
|
| 28 |
+
element: panel,
|
| 29 |
+
canvas,
|
| 30 |
+
cacheSize: 18,
|
| 31 |
+
prefetchFrames: 5,
|
| 32 |
+
sensitivity: 0.34,
|
| 33 |
+
onProgress(progress) {
|
| 34 |
+
const percent = Math.round(progress * 100);
|
| 35 |
+
loadProgress.style.width = `${percent}%`;
|
| 36 |
+
loadLabel.textContent = `Preparing scene · ${percent}%`;
|
| 37 |
+
},
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
const captureMode = new URLSearchParams(window.location.search).has('capture');
|
| 41 |
+
|
| 42 |
+
const wait = (duration: number): Promise<void> =>
|
| 43 |
+
new Promise((resolve) => window.setTimeout(resolve, duration));
|
| 44 |
+
|
| 45 |
+
async function moveDemoCursor(x: number, y: number, duration: number): Promise<void> {
|
| 46 |
+
const animation = demoCursor.animate(
|
| 47 |
+
{ left: `${x}px`, top: `${y}px` },
|
| 48 |
+
{ duration, easing: 'cubic-bezier(.22, 1, .36, 1)', fill: 'forwards' },
|
| 49 |
+
);
|
| 50 |
+
await animation.finished;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
async function runCaptureDemo(): Promise<void> {
|
| 54 |
+
const bounds = panel.getBoundingClientRect();
|
| 55 |
+
const y = bounds.top + bounds.height * 0.49;
|
| 56 |
+
const center = bounds.left + bounds.width * 0.51;
|
| 57 |
+
|
| 58 |
+
demoCursor.style.left = `${bounds.left - 78}px`;
|
| 59 |
+
demoCursor.style.top = `${y + 56}px`;
|
| 60 |
+
demoCursor.classList.add('is-visible');
|
| 61 |
+
|
| 62 |
+
await wait(550);
|
| 63 |
+
await moveDemoCursor(center, y, 850);
|
| 64 |
+
await wait(230);
|
| 65 |
+
demoCursor.classList.add('is-hand');
|
| 66 |
+
await wait(260);
|
| 67 |
+
|
| 68 |
+
demoCursor.classList.add('is-grabbing');
|
| 69 |
+
viewer.setAngle(132, { animate: true, duration: 1050 });
|
| 70 |
+
await moveDemoCursor(bounds.left + bounds.width * 0.73, y, 1050);
|
| 71 |
+
await wait(180);
|
| 72 |
+
|
| 73 |
+
viewer.setAngle(298, { animate: true, duration: 1450 });
|
| 74 |
+
await moveDemoCursor(bounds.left + bounds.width * 0.27, y, 1450);
|
| 75 |
+
await wait(200);
|
| 76 |
+
|
| 77 |
+
viewer.setAngle(40, { animate: true, duration: 1050 });
|
| 78 |
+
await moveDemoCursor(center, y, 1050);
|
| 79 |
+
demoCursor.classList.remove('is-grabbing');
|
| 80 |
+
await wait(550);
|
| 81 |
+
demoCursor.classList.remove('is-visible');
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
async function prepareScene(): Promise<void> {
|
| 85 |
+
try {
|
| 86 |
+
await viewer.load(createMp4Source('../demo/video_01.mp4'), {
|
| 87 |
+
width: 852,
|
| 88 |
+
height: 480,
|
| 89 |
+
quality: 0.92,
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
panel.classList.remove('is-loading');
|
| 93 |
+
panel.classList.add('is-ready');
|
| 94 |
+
loadState.hidden = true;
|
| 95 |
+
poster.hidden = true;
|
| 96 |
+
status.textContent = 'Interactive panel ready. Drag horizontally to change the view.';
|
| 97 |
+
|
| 98 |
+
if (captureMode) {
|
| 99 |
+
viewer.setInteractionEnabled(false);
|
| 100 |
+
void runCaptureDemo();
|
| 101 |
+
}
|
| 102 |
+
} catch (error) {
|
| 103 |
+
if (error instanceof DOMException && error.name === 'AbortError') return;
|
| 104 |
+
console.error(error);
|
| 105 |
+
panel.classList.remove('is-loading');
|
| 106 |
+
panel.classList.add('has-error');
|
| 107 |
+
loadLabel.textContent = 'Scene unavailable';
|
| 108 |
+
status.textContent = error instanceof Error ? error.message : 'Unable to prepare the scene.';
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
window.addEventListener('beforeunload', () => viewer.destroy());
|
| 113 |
+
|
| 114 |
+
void prepareScene();
|
recording-comic/output/orbit-comic-demo.gif
ADDED
|
Git LFS Details
|
recording-comic/output/orbit-comic-demo.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a38ad46c1dc6b2e06b862b8e8a99a3b4da47383343053d345c86fb221f27f0f6
|
| 3 |
+
size 1740871
|
recording-comic/style.css
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color: #0a0a09;
|
| 3 |
+
background: #fff;
|
| 4 |
+
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", "Helvetica Neue", sans-serif;
|
| 5 |
+
font-synthesis: none;
|
| 6 |
+
text-rendering: geometricPrecision;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
* { box-sizing: border-box; }
|
| 10 |
+
|
| 11 |
+
html {
|
| 12 |
+
min-width: 320px;
|
| 13 |
+
min-height: 100%;
|
| 14 |
+
background: #fff;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
body {
|
| 18 |
+
min-width: 320px;
|
| 19 |
+
min-height: 100vh;
|
| 20 |
+
margin: 0;
|
| 21 |
+
background: #fff;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.capture-stage {
|
| 25 |
+
display: grid;
|
| 26 |
+
min-height: 100vh;
|
| 27 |
+
place-items: center;
|
| 28 |
+
padding: clamp(14px, 2.5vw, 34px);
|
| 29 |
+
background: #fff;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.comic-page {
|
| 33 |
+
width: min(1080px, calc(100vw - 28px), calc((100vh - 28px) * .88));
|
| 34 |
+
padding: clamp(10px, 1.25vw, 18px);
|
| 35 |
+
background: #fff;
|
| 36 |
+
box-shadow:
|
| 37 |
+
0 2px 0 rgba(255, 255, 255, .8) inset,
|
| 38 |
+
0 16px 48px rgba(25, 22, 17, .06);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.panel {
|
| 42 |
+
position: relative;
|
| 43 |
+
isolation: isolate;
|
| 44 |
+
overflow: hidden;
|
| 45 |
+
border: clamp(5px, .62vw, 8px) solid #090909;
|
| 46 |
+
background: #f5f4f0;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.panel--hero {
|
| 50 |
+
width: 100%;
|
| 51 |
+
aspect-ratio: 16 / 9;
|
| 52 |
+
cursor: grab;
|
| 53 |
+
outline: 0;
|
| 54 |
+
touch-action: pan-y;
|
| 55 |
+
user-select: none;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.panel--hero:active,
|
| 59 |
+
.panel--hero.is-dragging { cursor: grabbing; }
|
| 60 |
+
|
| 61 |
+
.panel--hero:focus-visible {
|
| 62 |
+
outline: 3px solid #fff;
|
| 63 |
+
outline-offset: -9px;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.lower-panels {
|
| 67 |
+
display: grid;
|
| 68 |
+
grid-template-columns: 1fr 1fr;
|
| 69 |
+
gap: clamp(12px, 1.65vw, 22px);
|
| 70 |
+
margin-top: clamp(12px, 1.65vw, 22px);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.panel--detail {
|
| 74 |
+
aspect-ratio: 1 / 1;
|
| 75 |
+
background: #fff;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.panel-image,
|
| 79 |
+
canvas {
|
| 80 |
+
position: absolute;
|
| 81 |
+
inset: 0;
|
| 82 |
+
display: block;
|
| 83 |
+
width: 100%;
|
| 84 |
+
height: 100%;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.panel-image {
|
| 88 |
+
object-fit: cover;
|
| 89 |
+
filter: grayscale(1) contrast(1.08);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.hero-poster { z-index: 0; }
|
| 93 |
+
|
| 94 |
+
canvas {
|
| 95 |
+
z-index: 1;
|
| 96 |
+
opacity: 0;
|
| 97 |
+
filter: grayscale(1) contrast(1.06);
|
| 98 |
+
transition: opacity 180ms ease;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.is-ready canvas { opacity: 1; }
|
| 102 |
+
|
| 103 |
+
.panel--shot .panel-image {
|
| 104 |
+
object-fit: contain;
|
| 105 |
+
object-position: center;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.panel--impact .panel-image {
|
| 109 |
+
object-fit: contain;
|
| 110 |
+
object-position: center;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.speech-bubble {
|
| 114 |
+
position: absolute;
|
| 115 |
+
z-index: 2;
|
| 116 |
+
top: 8%;
|
| 117 |
+
right: 7%;
|
| 118 |
+
display: grid;
|
| 119 |
+
width: clamp(42px, 5.5vw, 70px);
|
| 120 |
+
aspect-ratio: 1.35;
|
| 121 |
+
place-items: center;
|
| 122 |
+
border: clamp(3px, .32vw, 4px) solid #080808;
|
| 123 |
+
border-radius: 50%;
|
| 124 |
+
background: #fff;
|
| 125 |
+
font-family: Georgia, "Times New Roman", serif;
|
| 126 |
+
font-size: clamp(20px, 2.25vw, 32px);
|
| 127 |
+
font-weight: 800;
|
| 128 |
+
line-height: .7;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.speech-bubble::after {
|
| 132 |
+
position: absolute;
|
| 133 |
+
right: 3%;
|
| 134 |
+
bottom: -17%;
|
| 135 |
+
width: 25%;
|
| 136 |
+
height: 32%;
|
| 137 |
+
border-right: 3px solid #080808;
|
| 138 |
+
background: #fff;
|
| 139 |
+
content: "";
|
| 140 |
+
transform: rotate(31deg) skewY(-26deg);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.caption-box {
|
| 144 |
+
position: absolute;
|
| 145 |
+
z-index: 2;
|
| 146 |
+
top: 5%;
|
| 147 |
+
left: 5%;
|
| 148 |
+
margin: 0;
|
| 149 |
+
padding: clamp(6px, .72vw, 10px) clamp(9px, 1vw, 14px);
|
| 150 |
+
border: clamp(3px, .32vw, 4px) solid #090909;
|
| 151 |
+
background: #fff;
|
| 152 |
+
font-family: "Arial Narrow", "Helvetica Neue", sans-serif;
|
| 153 |
+
font-size: clamp(11px, 1.25vw, 17px);
|
| 154 |
+
font-style: italic;
|
| 155 |
+
font-weight: 800;
|
| 156 |
+
letter-spacing: .025em;
|
| 157 |
+
line-height: 1;
|
| 158 |
+
text-transform: uppercase;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.load-state {
|
| 162 |
+
position: absolute;
|
| 163 |
+
z-index: 3;
|
| 164 |
+
right: 3.2%;
|
| 165 |
+
bottom: 5%;
|
| 166 |
+
left: 3.2%;
|
| 167 |
+
display: flex;
|
| 168 |
+
align-items: center;
|
| 169 |
+
gap: 14px;
|
| 170 |
+
padding: 8px 10px;
|
| 171 |
+
background: rgba(255, 255, 255, .92);
|
| 172 |
+
font-family: "Arial Narrow", "Helvetica Neue", sans-serif;
|
| 173 |
+
font-size: clamp(9px, .85vw, 12px);
|
| 174 |
+
font-weight: 800;
|
| 175 |
+
letter-spacing: .08em;
|
| 176 |
+
text-transform: uppercase;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.load-state[hidden] { display: none; }
|
| 180 |
+
|
| 181 |
+
.load-track {
|
| 182 |
+
display: block;
|
| 183 |
+
flex: 1;
|
| 184 |
+
height: 3px;
|
| 185 |
+
background: #c8c7c2;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.load-track span {
|
| 189 |
+
display: block;
|
| 190 |
+
width: 0;
|
| 191 |
+
height: 100%;
|
| 192 |
+
background: #090909;
|
| 193 |
+
transition: width 100ms linear;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.has-error .load-state { color: #fff; background: #090909; }
|
| 197 |
+
.has-error .load-track { display: none; }
|
| 198 |
+
|
| 199 |
+
.demo-cursor {
|
| 200 |
+
position: fixed;
|
| 201 |
+
z-index: 20;
|
| 202 |
+
width: 42px;
|
| 203 |
+
height: 48px;
|
| 204 |
+
opacity: 0;
|
| 205 |
+
pointer-events: none;
|
| 206 |
+
transform: translate(-9px, -7px) scale(.96);
|
| 207 |
+
transition: opacity 180ms ease, transform 180ms ease;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.demo-cursor.is-visible {
|
| 211 |
+
opacity: 1;
|
| 212 |
+
transform: translate(-9px, -7px) scale(1);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.demo-cursor svg {
|
| 216 |
+
position: absolute;
|
| 217 |
+
inset: 0;
|
| 218 |
+
width: 100%;
|
| 219 |
+
height: 100%;
|
| 220 |
+
overflow: visible;
|
| 221 |
+
filter: drop-shadow(0 2px 2px rgba(255, 255, 255, .8));
|
| 222 |
+
fill: #fff;
|
| 223 |
+
stroke: #050505;
|
| 224 |
+
stroke-linecap: round;
|
| 225 |
+
stroke-linejoin: round;
|
| 226 |
+
stroke-width: 2.4px;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.demo-cursor__hand { display: none; }
|
| 230 |
+
.demo-cursor.is-hand .demo-cursor__pointer { display: none; }
|
| 231 |
+
.demo-cursor.is-hand .demo-cursor__hand { display: block; }
|
| 232 |
+
|
| 233 |
+
.demo-cursor.is-grabbing .demo-cursor__hand {
|
| 234 |
+
transform: rotate(-8deg) scale(.88, .8);
|
| 235 |
+
transform-origin: 50% 60%;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.sr-only {
|
| 239 |
+
position: absolute;
|
| 240 |
+
width: 1px;
|
| 241 |
+
height: 1px;
|
| 242 |
+
padding: 0;
|
| 243 |
+
overflow: hidden;
|
| 244 |
+
clip: rect(0, 0, 0, 0);
|
| 245 |
+
white-space: nowrap;
|
| 246 |
+
border: 0;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
@media (max-width: 680px) {
|
| 250 |
+
.capture-stage { padding: 8px; }
|
| 251 |
+
.comic-page { width: 100%; padding: 8px; }
|
| 252 |
+
.lower-panels { gap: 8px; margin-top: 8px; }
|
| 253 |
+
.panel { border-width: 4px; }
|
| 254 |
+
.caption-box { border-width: 2px; }
|
| 255 |
+
.speech-bubble { border-width: 2px; }
|
| 256 |
+
.speech-bubble::after { border-right-width: 2px; }
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
@media (prefers-reduced-motion: reduce) {
|
| 260 |
+
*, *::before, *::after { transition-duration: .01ms !important; }
|
| 261 |
+
}
|
skills/integrate-orbit-video/SKILL.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: integrate-orbit-video
|
| 3 |
+
description: Integrate a user's pre-rendered orbit MP4 into an existing web project as a drag-controlled view using orbit-video-viewer. Use when the user wants to add, replace, or wire an orbit video into a website, game UI, product page, interactive story, exhibit, or other browser experience without building a 3D scene.
|
| 4 |
+
license: MIT
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Integrate Orbit Video
|
| 8 |
+
|
| 9 |
+
Add the SDK to the user's existing product. Do not copy the Orbit Comic Demo page or rebuild the SDK internals.
|
| 10 |
+
|
| 11 |
+
Requires a browser-based project, Node.js 20 or newer, a supported JavaScript package manager, and access to the `orbit-video-viewer` package or repository.
|
| 12 |
+
|
| 13 |
+
## Inputs
|
| 14 |
+
|
| 15 |
+
Find or obtain:
|
| 16 |
+
|
| 17 |
+
- the host web project;
|
| 18 |
+
- the user's orbit MP4;
|
| 19 |
+
- the intended page or component.
|
| 20 |
+
|
| 21 |
+
Inspect the project before editing. Detect its framework, package manager, build command, styling conventions, and component lifecycle. Preserve its visual language and existing behavior. Ask the user only when the video or intended placement cannot be found and a safe inference is not possible.
|
| 22 |
+
|
| 23 |
+
Read [references/sdk-api.md](references/sdk-api.md) before implementation.
|
| 24 |
+
|
| 25 |
+
## Workflow
|
| 26 |
+
|
| 27 |
+
1. Confirm the host is a browser project and identify where the interactive view belongs.
|
| 28 |
+
2. Inspect the MP4 with available media tools when possible. Record its codec, dimensions, duration, and whether it appears to form a continuous orbit. Do not upload or alter the user's source unless requested.
|
| 29 |
+
3. Install `orbit-video-viewer` from the source available in the environment:
|
| 30 |
+
- use the existing installed dependency when present;
|
| 31 |
+
- use the published package when available;
|
| 32 |
+
- during local SDK development, build the SDK and use a file dependency.
|
| 33 |
+
4. Add one viewer container and the minimum CSS required for sizing, pointer input, and Canvas display. Adapt these rules to the host project instead of replacing its page styles.
|
| 34 |
+
5. Create one `OrbitViewer`, then load the MP4 through `createMp4Source()`.
|
| 35 |
+
6. Connect loading and error feedback to the host product quietly. Add Reset only when it fits the product. Treat reset as returning to the current video's configured initial angle, never as restoring the Demo video.
|
| 36 |
+
7. Dispose the Viewer when the component or page is removed. If the user changes video, call `load()` on the same Viewer unless the host architecture makes replacement cleaner.
|
| 37 |
+
8. Run the host project's type check, tests, and production build. Fix integration errors without changing SDK internals.
|
| 38 |
+
9. When browser control is available, verify initial render, horizontal drag, both directions, reset or programmatic angle control, and the absence of console errors.
|
| 39 |
+
|
| 40 |
+
## Framework lifecycle
|
| 41 |
+
|
| 42 |
+
- Vanilla JavaScript: create after the container exists; call `destroy()` during page cleanup.
|
| 43 |
+
- React: create in an effect after the ref is mounted; return `viewer.destroy()` from the effect.
|
| 44 |
+
- Vue or Svelte: create in the mounted lifecycle; destroy in the matching unmount lifecycle.
|
| 45 |
+
- Server-rendered apps: import and instantiate only in browser code. The SDK requires DOM, Canvas, and browser media APIs.
|
| 46 |
+
|
| 47 |
+
## Product boundaries
|
| 48 |
+
|
| 49 |
+
- Keep labels, game events, hotspots, sounds, and product-specific state in the host application. Use `onChange` or `getAngle()` to connect them.
|
| 50 |
+
- Do not claim real 3D, arbitrary camera movement, relighting, collision, or unseen viewpoints.
|
| 51 |
+
- Do not add a preprocessing pipeline, backend upload, large framework, or unrelated UI unless the user explicitly requests it.
|
| 52 |
+
- Do not expose the Orbit Comic title, credits, buttons, or artwork in the host product unless the user specifically wants the Demo.
|
| 53 |
+
|
| 54 |
+
## Completion report
|
| 55 |
+
|
| 56 |
+
State plainly:
|
| 57 |
+
|
| 58 |
+
1. where the viewer was added;
|
| 59 |
+
2. which video is used;
|
| 60 |
+
3. how the user interacts with it;
|
| 61 |
+
4. which checks passed;
|
| 62 |
+
5. any real browser or codec limitation that remains.
|
skills/integrate-orbit-video/agents/openai.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface:
|
| 2 |
+
display_name: "Integrate Orbit Video"
|
| 3 |
+
short_description: "Add drag-controlled orbit video to an existing app"
|
| 4 |
+
brand_color: "#C84A34"
|
| 5 |
+
default_prompt: "Use $integrate-orbit-video to add my orbit MP4 to this project without changing its existing visual style."
|
skills/integrate-orbit-video/references/sdk-api.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SDK reference
|
| 2 |
+
|
| 3 |
+
## Install
|
| 4 |
+
|
| 5 |
+
Published package, after it becomes available:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pnpm add orbit-video-viewer
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
Local SDK repository:
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
pnpm --dir /absolute/path/to/orbit-panel-viewer install
|
| 15 |
+
pnpm --dir /absolute/path/to/orbit-panel-viewer build:library
|
| 16 |
+
pnpm add /absolute/path/to/orbit-panel-viewer
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
Use the host project's package manager when it is not pnpm.
|
| 20 |
+
|
| 21 |
+
## Minimal integration
|
| 22 |
+
|
| 23 |
+
```html
|
| 24 |
+
<div id="viewer" tabindex="0"></div>
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
```css
|
| 28 |
+
#viewer {
|
| 29 |
+
aspect-ratio: 16 / 9;
|
| 30 |
+
overflow: hidden;
|
| 31 |
+
cursor: grab;
|
| 32 |
+
touch-action: pan-y;
|
| 33 |
+
user-select: none;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
#viewer canvas { display: block; width: 100%; height: 100%; }
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
```ts
|
| 40 |
+
import { OrbitViewer, createMp4Source } from 'orbit-video-viewer';
|
| 41 |
+
|
| 42 |
+
const element = document.querySelector<HTMLElement>('#viewer');
|
| 43 |
+
if (!element) throw new Error('Missing orbit view container.');
|
| 44 |
+
|
| 45 |
+
const viewer = new OrbitViewer({
|
| 46 |
+
element,
|
| 47 |
+
initialAngle: 0,
|
| 48 |
+
onProgress(progress) {
|
| 49 |
+
console.log(`Loading ${Math.round(progress * 100)}%`);
|
| 50 |
+
},
|
| 51 |
+
onChange({ angle, frame }) {
|
| 52 |
+
console.log(angle, frame);
|
| 53 |
+
},
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
await viewer.load(createMp4Source('/assets/orbit.mp4'));
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Public controls
|
| 60 |
+
|
| 61 |
+
```ts
|
| 62 |
+
viewer.getAngle();
|
| 63 |
+
viewer.getState();
|
| 64 |
+
viewer.setAngle(180);
|
| 65 |
+
viewer.setAngle(180, { animate: true, duration: 500 });
|
| 66 |
+
viewer.rotateBy(30);
|
| 67 |
+
viewer.reset({ animate: true });
|
| 68 |
+
viewer.setInteractionEnabled(false);
|
| 69 |
+
viewer.destroy();
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
`createMp4Source()` accepts a URL, `File`, or `Blob`. Calling `load()` again replaces the current video and makes that video's configured `initialAngle` the reset target.
|
| 73 |
+
|
| 74 |
+
Useful constructor options: `initialAngle`, `cacheSize`, `sensitivity`, `inertia`, `resetDuration`, `prefetchFrames`, `onProgress`, and `onChange`.
|
| 75 |
+
|
| 76 |
+
Useful load options: `frameCount`, `width`, `height`, and `quality`.
|
| 77 |
+
|
| 78 |
+
## Source guidance
|
| 79 |
+
|
| 80 |
+
Prefer H.264 MP4 with one continuous horizontal orbit, stable subject scale and camera height, no cuts, and an ending close to the opening view when wraparound is desired. Audio is ignored. The SDK samples only viewpoints already present in the video.
|
src/core/FrameExtractor.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { DataStream, createFile, type Movie, type Sample, type VisualSampleEntry } from 'mp4box';
|
| 2 |
+
|
| 3 |
+
export type ExtractionMode = 'WebCodecs + MP4Box' | 'video fallback';
|
| 4 |
+
|
| 5 |
+
export interface ExtractionOptions {
|
| 6 |
+
count: number;
|
| 7 |
+
width: number;
|
| 8 |
+
height: number;
|
| 9 |
+
quality: number;
|
| 10 |
+
signal?: AbortSignal;
|
| 11 |
+
onProgress?: (progress: number, phase: string) => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export interface ExtractionResult {
|
| 15 |
+
frames: Blob[];
|
| 16 |
+
mode: ExtractionMode;
|
| 17 |
+
sourceWidth: number;
|
| 18 |
+
sourceHeight: number;
|
| 19 |
+
duration: number;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function aborted(signal?: AbortSignal): void {
|
| 23 |
+
if (signal?.aborted) throw new DOMException('Frame extraction aborted', 'AbortError');
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function canvasToBlob(canvas: HTMLCanvasElement, quality: number): Promise<Blob> {
|
| 27 |
+
return new Promise((resolve, reject) => canvas.toBlob(
|
| 28 |
+
(blob) => blob ? resolve(blob) : reject(new Error('The browser could not encode a JPEG frame.')),
|
| 29 |
+
'image/jpeg', quality,
|
| 30 |
+
));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
async function encodeSource(
|
| 34 |
+
source: CanvasImageSource,
|
| 35 |
+
canvas: HTMLCanvasElement,
|
| 36 |
+
context: CanvasRenderingContext2D,
|
| 37 |
+
quality: number,
|
| 38 |
+
): Promise<Blob> {
|
| 39 |
+
context.drawImage(source, 0, 0, canvas.width, canvas.height);
|
| 40 |
+
return canvasToBlob(canvas, quality);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function avcDescription(entry: VisualSampleEntry): Uint8Array | undefined {
|
| 44 |
+
const box = entry.avcC;
|
| 45 |
+
if (!box) return undefined;
|
| 46 |
+
const stream = new DataStream();
|
| 47 |
+
box.write(stream as never);
|
| 48 |
+
return new Uint8Array(stream.buffer).slice(8);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
async function extractWithWebCodecs(file: Blob, options: ExtractionOptions): Promise<ExtractionResult | null> {
|
| 52 |
+
if (!('VideoDecoder' in window) || !('EncodedVideoChunk' in window)) return null;
|
| 53 |
+
const source = await file.arrayBuffer();
|
| 54 |
+
aborted(options.signal);
|
| 55 |
+
const mp4 = createFile();
|
| 56 |
+
|
| 57 |
+
return new Promise<ExtractionResult | null>((resolve, reject) => {
|
| 58 |
+
let settled = false;
|
| 59 |
+
const finish = (value: ExtractionResult | null) => {
|
| 60 |
+
if (!settled) { settled = true; resolve(value); }
|
| 61 |
+
};
|
| 62 |
+
mp4.onError = (_module, message) => {
|
| 63 |
+
if (!settled) reject(new Error(`MP4 parsing failed: ${message}`));
|
| 64 |
+
};
|
| 65 |
+
mp4.onReady = (info: Movie) => {
|
| 66 |
+
const track = info.videoTracks[0];
|
| 67 |
+
if (!track || !track.video || !track.codec.startsWith('avc1')) { finish(null); return; }
|
| 68 |
+
const videoInfo = track.video;
|
| 69 |
+
mp4.setExtractionOptions(track.id, undefined, { nbSamples: track.nb_samples });
|
| 70 |
+
mp4.onSamples = async (_id, _user, samples: Sample[]) => {
|
| 71 |
+
const first = samples[0];
|
| 72 |
+
const description = first ? avcDescription(first.description as VisualSampleEntry) : undefined;
|
| 73 |
+
if (!first || !description) { finish(null); return; }
|
| 74 |
+
const config: VideoDecoderConfig = {
|
| 75 |
+
codec: track.codec,
|
| 76 |
+
codedWidth: videoInfo.width,
|
| 77 |
+
codedHeight: videoInfo.height,
|
| 78 |
+
description,
|
| 79 |
+
};
|
| 80 |
+
const support = await VideoDecoder.isConfigSupported(config);
|
| 81 |
+
if (!support.supported) { finish(null); return; }
|
| 82 |
+
|
| 83 |
+
const canvas = document.createElement('canvas');
|
| 84 |
+
canvas.width = options.width; canvas.height = options.height;
|
| 85 |
+
const context = canvas.getContext('2d', { alpha: false });
|
| 86 |
+
if (!context) { reject(new Error('Unable to create a Canvas 2D context.')); return; }
|
| 87 |
+
const frames = new Array<Blob>(options.count);
|
| 88 |
+
const jobs: Promise<void>[] = [];
|
| 89 |
+
let encodeChain = Promise.resolve();
|
| 90 |
+
const durationUs = (track.duration / track.timescale) * 1_000_000;
|
| 91 |
+
let nextTarget = 0;
|
| 92 |
+
let decoderFailed = false;
|
| 93 |
+
|
| 94 |
+
const decoder = new VideoDecoder({
|
| 95 |
+
output: (frame) => {
|
| 96 |
+
const targetTime = ((nextTarget + 0.5) / options.count) * durationUs;
|
| 97 |
+
if (nextTarget < options.count && frame.timestamp >= targetTime) {
|
| 98 |
+
const target = nextTarget;
|
| 99 |
+
nextTarget += 1;
|
| 100 |
+
const bitmapPromise = createImageBitmap(frame);
|
| 101 |
+
encodeChain = encodeChain.then(async () => {
|
| 102 |
+
const bitmap = await bitmapPromise;
|
| 103 |
+
frames[target] = await encodeSource(bitmap, canvas, context, options.quality);
|
| 104 |
+
bitmap.close();
|
| 105 |
+
options.onProgress?.((target + 1) / options.count, 'Extracting frames with WebCodecs');
|
| 106 |
+
});
|
| 107 |
+
jobs.push(encodeChain);
|
| 108 |
+
}
|
| 109 |
+
frame.close();
|
| 110 |
+
},
|
| 111 |
+
error: () => { decoderFailed = true; },
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
try {
|
| 115 |
+
decoder.configure(config);
|
| 116 |
+
for (const sample of samples) {
|
| 117 |
+
aborted(options.signal);
|
| 118 |
+
decoder.decode(new EncodedVideoChunk({
|
| 119 |
+
type: sample.is_sync ? 'key' : 'delta',
|
| 120 |
+
timestamp: Math.round((sample.cts / sample.timescale) * 1_000_000),
|
| 121 |
+
duration: Math.round((sample.duration / sample.timescale) * 1_000_000),
|
| 122 |
+
data: sample.data!,
|
| 123 |
+
}));
|
| 124 |
+
if (decoder.decodeQueueSize > 18) await new Promise((done) => setTimeout(done, 0));
|
| 125 |
+
}
|
| 126 |
+
await decoder.flush();
|
| 127 |
+
await Promise.all(jobs);
|
| 128 |
+
decoder.close();
|
| 129 |
+
if (decoderFailed || frames.some((frame) => !frame)) { finish(null); return; }
|
| 130 |
+
finish({
|
| 131 |
+
frames,
|
| 132 |
+
mode: 'WebCodecs + MP4Box',
|
| 133 |
+
sourceWidth: videoInfo.width,
|
| 134 |
+
sourceHeight: videoInfo.height,
|
| 135 |
+
duration: track.duration / track.timescale,
|
| 136 |
+
});
|
| 137 |
+
} catch (error) {
|
| 138 |
+
try { decoder.close(); } catch { /* already closed */ }
|
| 139 |
+
if (error instanceof DOMException && error.name === 'AbortError') reject(error);
|
| 140 |
+
else finish(null);
|
| 141 |
+
}
|
| 142 |
+
};
|
| 143 |
+
mp4.start();
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
const buffer = source as ArrayBuffer & { fileStart: number };
|
| 147 |
+
buffer.fileStart = 0;
|
| 148 |
+
mp4.appendBuffer(buffer, true);
|
| 149 |
+
mp4.flush();
|
| 150 |
+
});
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
async function waitForEvent(target: HTMLVideoElement, event: 'loadedmetadata' | 'seeked'): Promise<void> {
|
| 154 |
+
return new Promise((resolve, reject) => {
|
| 155 |
+
const clean = () => { target.removeEventListener(event, success); target.removeEventListener('error', failure); };
|
| 156 |
+
const success = () => { clean(); resolve(); };
|
| 157 |
+
const failure = () => { clean(); reject(new Error('The browser could not decode this video.')); };
|
| 158 |
+
target.addEventListener(event, success, { once: true });
|
| 159 |
+
target.addEventListener('error', failure, { once: true });
|
| 160 |
+
});
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
async function extractWithVideo(file: Blob, options: ExtractionOptions): Promise<ExtractionResult> {
|
| 164 |
+
const url = URL.createObjectURL(file);
|
| 165 |
+
const video = document.createElement('video');
|
| 166 |
+
video.muted = true; video.playsInline = true; video.preload = 'auto'; video.src = url;
|
| 167 |
+
try {
|
| 168 |
+
await waitForEvent(video, 'loadedmetadata');
|
| 169 |
+
if (!Number.isFinite(video.duration) || video.duration <= 0) throw new Error('The video duration is invalid.');
|
| 170 |
+
const canvas = document.createElement('canvas');
|
| 171 |
+
canvas.width = options.width; canvas.height = options.height;
|
| 172 |
+
const context = canvas.getContext('2d', { alpha: false });
|
| 173 |
+
if (!context) throw new Error('Unable to create a Canvas 2D context.');
|
| 174 |
+
const frames: Blob[] = [];
|
| 175 |
+
for (let index = 0; index < options.count; index += 1) {
|
| 176 |
+
aborted(options.signal);
|
| 177 |
+
const target = Math.min(video.duration - 0.001, ((index + 0.5) / options.count) * video.duration);
|
| 178 |
+
video.currentTime = target;
|
| 179 |
+
await waitForEvent(video, 'seeked');
|
| 180 |
+
frames.push(await encodeSource(video, canvas, context, options.quality));
|
| 181 |
+
options.onProgress?.((index + 1) / options.count, 'Extracting frames with the video fallback');
|
| 182 |
+
}
|
| 183 |
+
return {
|
| 184 |
+
frames,
|
| 185 |
+
mode: 'video fallback',
|
| 186 |
+
sourceWidth: video.videoWidth,
|
| 187 |
+
sourceHeight: video.videoHeight,
|
| 188 |
+
duration: video.duration,
|
| 189 |
+
};
|
| 190 |
+
} finally {
|
| 191 |
+
video.removeAttribute('src'); video.load(); URL.revokeObjectURL(url);
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
export async function extractFrames(file: Blob, options: ExtractionOptions): Promise<ExtractionResult> {
|
| 196 |
+
options.onProgress?.(0, 'Parsing MP4');
|
| 197 |
+
const fast = await extractWithWebCodecs(file, options).catch((error) => {
|
| 198 |
+
if (error instanceof DOMException && error.name === 'AbortError') throw error;
|
| 199 |
+
console.warn('WebCodecs fast path unavailable, using video fallback.', error);
|
| 200 |
+
return null;
|
| 201 |
+
});
|
| 202 |
+
return fast ?? extractWithVideo(file, options);
|
| 203 |
+
}
|
src/core/FrameSource.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface OrbitPrepareOptions {
|
| 2 |
+
count: number;
|
| 3 |
+
width: number;
|
| 4 |
+
height: number;
|
| 5 |
+
quality: number;
|
| 6 |
+
signal?: AbortSignal;
|
| 7 |
+
onProgress?: (progress: number, phase: string) => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export interface PreparedOrbitSource {
|
| 11 |
+
readonly frameCount: number;
|
| 12 |
+
readonly width: number;
|
| 13 |
+
readonly height: number;
|
| 14 |
+
readonly duration?: number;
|
| 15 |
+
getFrame(index: number): Promise<Blob>;
|
| 16 |
+
dispose(): void;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface OrbitSource {
|
| 20 |
+
prepare(options: OrbitPrepareOptions): Promise<PreparedOrbitSource>;
|
| 21 |
+
}
|
src/core/FrameStore.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { PreparedOrbitSource } from './FrameSource';
|
| 2 |
+
|
| 3 |
+
export class FrameStore {
|
| 4 |
+
private source: PreparedOrbitSource | null = null;
|
| 5 |
+
private readonly bitmaps = new Map<number, ImageBitmap>();
|
| 6 |
+
private readonly pending = new Map<number, Promise<ImageBitmap | null>>();
|
| 7 |
+
private generation = 0;
|
| 8 |
+
|
| 9 |
+
constructor(private readonly cacheLimit = 16) {}
|
| 10 |
+
|
| 11 |
+
setSource(source: PreparedOrbitSource): void {
|
| 12 |
+
this.clear();
|
| 13 |
+
this.source = source;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
get count(): number { return this.source?.frameCount ?? 0; }
|
| 17 |
+
get cacheSize(): number { return this.bitmaps.size; }
|
| 18 |
+
|
| 19 |
+
peek(index: number): ImageBitmap | undefined {
|
| 20 |
+
const bitmap = this.bitmaps.get(index);
|
| 21 |
+
if (!bitmap) return undefined;
|
| 22 |
+
this.bitmaps.delete(index);
|
| 23 |
+
this.bitmaps.set(index, bitmap);
|
| 24 |
+
return bitmap;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async get(index: number): Promise<ImageBitmap | null> {
|
| 28 |
+
const cached = this.peek(index);
|
| 29 |
+
if (cached) return cached;
|
| 30 |
+
const active = this.pending.get(index);
|
| 31 |
+
if (active) return active;
|
| 32 |
+
const source = this.source;
|
| 33 |
+
if (!source || index < 0 || index >= source.frameCount) return null;
|
| 34 |
+
const token = this.generation;
|
| 35 |
+
const task = source.getFrame(index).then((blob) => createImageBitmap(blob)).then((bitmap) => {
|
| 36 |
+
if (token !== this.generation) {
|
| 37 |
+
bitmap.close();
|
| 38 |
+
return null;
|
| 39 |
+
}
|
| 40 |
+
this.bitmaps.set(index, bitmap);
|
| 41 |
+
while (this.bitmaps.size > this.cacheLimit) {
|
| 42 |
+
const oldest = this.bitmaps.keys().next().value as number;
|
| 43 |
+
this.bitmaps.get(oldest)?.close();
|
| 44 |
+
this.bitmaps.delete(oldest);
|
| 45 |
+
}
|
| 46 |
+
return bitmap;
|
| 47 |
+
}).finally(() => this.pending.delete(index));
|
| 48 |
+
this.pending.set(index, task);
|
| 49 |
+
return task;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
prefetch(index: number, direction: number, count = 4): void {
|
| 53 |
+
if (!this.source?.frameCount) return;
|
| 54 |
+
for (let step = 1; step <= count; step += 1) {
|
| 55 |
+
const next = ((index + step * direction) % this.source.frameCount + this.source.frameCount) % this.source.frameCount;
|
| 56 |
+
void this.get(next);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
clear(): void {
|
| 61 |
+
this.generation += 1;
|
| 62 |
+
this.bitmaps.forEach((bitmap) => bitmap.close());
|
| 63 |
+
this.bitmaps.clear();
|
| 64 |
+
this.pending.clear();
|
| 65 |
+
this.source?.dispose();
|
| 66 |
+
this.source = null;
|
| 67 |
+
}
|
| 68 |
+
}
|
src/core/OrbitPanel.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FrameStore } from './FrameStore';
|
| 2 |
+
import { easeOutCubic, frameIndex, normalizedDegrees, pixelsPerTurn } from './orbitMath';
|
| 3 |
+
|
| 4 |
+
export interface OrbitDebugState {
|
| 5 |
+
frame: number;
|
| 6 |
+
angle: number;
|
| 7 |
+
turn: number;
|
| 8 |
+
cacheSize: number;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export interface OrbitPanelOptions {
|
| 12 |
+
initialAngle?: number;
|
| 13 |
+
sensitivity?: number;
|
| 14 |
+
inertia?: boolean;
|
| 15 |
+
resetDuration?: number;
|
| 16 |
+
prefetchFrames?: number;
|
| 17 |
+
onChange?: (state: OrbitDebugState) => void;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export interface SetAngleOptions {
|
| 21 |
+
animate?: boolean;
|
| 22 |
+
duration?: number;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export class OrbitPanel {
|
| 26 |
+
private readonly context: CanvasRenderingContext2D;
|
| 27 |
+
private turn = 0;
|
| 28 |
+
private velocity = 0;
|
| 29 |
+
private dragging = false;
|
| 30 |
+
private lastX = 0;
|
| 31 |
+
private samples: Array<[number, number]> = [];
|
| 32 |
+
private raf = 0;
|
| 33 |
+
private lastTick = 0;
|
| 34 |
+
private direction = 1;
|
| 35 |
+
private lastIndex = -1;
|
| 36 |
+
private wantIndex = 0;
|
| 37 |
+
private angleAnimation: { from: number; to: number; started: number; duration: number } | null = null;
|
| 38 |
+
private disposed = false;
|
| 39 |
+
private interactionEnabled = true;
|
| 40 |
+
private readonly initialTurn: number;
|
| 41 |
+
private readonly sensitivity: number;
|
| 42 |
+
private readonly inertia: boolean;
|
| 43 |
+
private readonly resetDuration: number;
|
| 44 |
+
private readonly prefetchFrames: number;
|
| 45 |
+
private readonly onChange: (state: OrbitDebugState) => void;
|
| 46 |
+
|
| 47 |
+
constructor(
|
| 48 |
+
private readonly element: HTMLElement,
|
| 49 |
+
private readonly canvas: HTMLCanvasElement,
|
| 50 |
+
private readonly store: FrameStore,
|
| 51 |
+
options: OrbitPanelOptions = {},
|
| 52 |
+
) {
|
| 53 |
+
const context = canvas.getContext('2d', { alpha: false });
|
| 54 |
+
if (!context) throw new Error('Unable to create a Canvas 2D context.');
|
| 55 |
+
this.context = context;
|
| 56 |
+
this.initialTurn = (options.initialAngle ?? 0) / 360;
|
| 57 |
+
this.turn = this.initialTurn;
|
| 58 |
+
this.sensitivity = options.sensitivity ?? 1;
|
| 59 |
+
this.inertia = options.inertia ?? true;
|
| 60 |
+
this.resetDuration = options.resetDuration ?? 600;
|
| 61 |
+
this.prefetchFrames = options.prefetchFrames ?? 4;
|
| 62 |
+
this.onChange = options.onChange ?? (() => {});
|
| 63 |
+
element.addEventListener('pointerdown', this.onPointerDown);
|
| 64 |
+
element.addEventListener('pointermove', this.onPointerMove);
|
| 65 |
+
element.addEventListener('pointerup', this.onPointerUp);
|
| 66 |
+
element.addEventListener('pointercancel', this.onPointerUp);
|
| 67 |
+
element.addEventListener('keydown', this.onKeyDown);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
async start(): Promise<void> { await this.draw(true); }
|
| 71 |
+
|
| 72 |
+
getAngle(): number { return normalizedDegrees(this.turn); }
|
| 73 |
+
|
| 74 |
+
getState(): OrbitDebugState {
|
| 75 |
+
return { frame: frameIndex(this.turn, this.store.count), angle: this.getAngle(), turn: this.turn, cacheSize: this.store.cacheSize };
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
setAngle(angle: number, options: SetAngleOptions = {}): void {
|
| 79 |
+
if (!Number.isFinite(angle)) throw new TypeError('Angle must be a finite number.');
|
| 80 |
+
this.velocity = 0;
|
| 81 |
+
const targetBase = angle / 360;
|
| 82 |
+
const target = this.turn + ((((targetBase - this.turn) + 0.5) % 1 + 1) % 1 - 0.5);
|
| 83 |
+
if (options.animate) {
|
| 84 |
+
this.angleAnimation = {
|
| 85 |
+
from: this.turn,
|
| 86 |
+
to: target,
|
| 87 |
+
started: performance.now(),
|
| 88 |
+
duration: Math.max(0, options.duration ?? this.resetDuration),
|
| 89 |
+
};
|
| 90 |
+
this.kick();
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
this.angleAnimation = null;
|
| 94 |
+
this.turn = target;
|
| 95 |
+
void this.draw(true);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
rotateBy(degrees: number, options: SetAngleOptions = {}): void {
|
| 99 |
+
this.setAngle(this.getAngle() + degrees, options);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
reset(options: SetAngleOptions = { animate: true }): void {
|
| 103 |
+
if (this.dragging) return;
|
| 104 |
+
this.setAngle(this.initialTurn * 360, options);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
setInteractionEnabled(enabled: boolean): void {
|
| 108 |
+
this.interactionEnabled = enabled;
|
| 109 |
+
if (!enabled) {
|
| 110 |
+
this.dragging = false;
|
| 111 |
+
this.velocity = 0;
|
| 112 |
+
this.element.classList.remove('is-dragging');
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
dispose(): void {
|
| 117 |
+
this.disposed = true;
|
| 118 |
+
cancelAnimationFrame(this.raf);
|
| 119 |
+
this.element.removeEventListener('pointerdown', this.onPointerDown);
|
| 120 |
+
this.element.removeEventListener('pointermove', this.onPointerMove);
|
| 121 |
+
this.element.removeEventListener('pointerup', this.onPointerUp);
|
| 122 |
+
this.element.removeEventListener('pointercancel', this.onPointerUp);
|
| 123 |
+
this.element.removeEventListener('keydown', this.onKeyDown);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
private readonly onPointerDown = (event: PointerEvent) => {
|
| 127 |
+
if (!this.interactionEnabled || !this.store.count) return;
|
| 128 |
+
this.angleAnimation = null;
|
| 129 |
+
this.velocity = 0;
|
| 130 |
+
this.dragging = true;
|
| 131 |
+
this.lastX = event.clientX;
|
| 132 |
+
this.samples = [[event.timeStamp, event.clientX]];
|
| 133 |
+
this.element.setPointerCapture(event.pointerId);
|
| 134 |
+
this.element.classList.add('is-dragging');
|
| 135 |
+
this.kick();
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
private readonly onPointerMove = (event: PointerEvent) => {
|
| 139 |
+
if (!this.dragging) return;
|
| 140 |
+
const previous = this.turn;
|
| 141 |
+
this.turn += (event.clientX - this.lastX) / pixelsPerTurn(this.element.getBoundingClientRect().width, this.sensitivity);
|
| 142 |
+
this.direction = this.turn >= previous ? 1 : -1;
|
| 143 |
+
this.lastX = event.clientX;
|
| 144 |
+
this.samples.push([event.timeStamp, event.clientX]);
|
| 145 |
+
this.samples = this.samples.filter(([time]) => event.timeStamp - time < 90);
|
| 146 |
+
this.kick();
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
private readonly onPointerUp = () => {
|
| 150 |
+
if (!this.dragging) return;
|
| 151 |
+
this.dragging = false;
|
| 152 |
+
this.element.classList.remove('is-dragging');
|
| 153 |
+
const first = this.samples[0];
|
| 154 |
+
const last = this.samples.at(-1);
|
| 155 |
+
if (first && last && first !== last) {
|
| 156 |
+
const seconds = (last[0] - first[0]) / 1000;
|
| 157 |
+
if (seconds > 0.02) {
|
| 158 |
+
const velocity = ((last[1] - first[1]) / seconds) / pixelsPerTurn(this.element.getBoundingClientRect().width, this.sensitivity);
|
| 159 |
+
this.velocity = this.inertia ? Math.max(-0.9, Math.min(0.9, velocity)) : 0;
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
this.kick();
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
private readonly onKeyDown = (event: KeyboardEvent) => {
|
| 166 |
+
if (!this.interactionEnabled || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;
|
| 167 |
+
event.preventDefault();
|
| 168 |
+
const previous = this.turn;
|
| 169 |
+
this.turn += event.key === 'ArrowRight' ? 1 / this.store.count : -1 / this.store.count;
|
| 170 |
+
this.direction = this.turn >= previous ? 1 : -1;
|
| 171 |
+
void this.draw();
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
private kick(): void {
|
| 175 |
+
if (!this.raf && !this.disposed) {
|
| 176 |
+
this.lastTick = performance.now();
|
| 177 |
+
this.raf = requestAnimationFrame(this.tick);
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
private readonly tick = (now: number) => {
|
| 182 |
+
const delta = Math.min(0.05, (now - this.lastTick) / 1000);
|
| 183 |
+
this.lastTick = now;
|
| 184 |
+
this.raf = 0;
|
| 185 |
+
const before = this.turn;
|
| 186 |
+
if (this.angleAnimation) {
|
| 187 |
+
const progress = this.angleAnimation.duration === 0 ? 1 : Math.min(1, (now - this.angleAnimation.started) / this.angleAnimation.duration);
|
| 188 |
+
this.turn = this.angleAnimation.from + (this.angleAnimation.to - this.angleAnimation.from) * easeOutCubic(progress);
|
| 189 |
+
if (progress >= 1) { this.turn = this.angleAnimation.to; this.angleAnimation = null; }
|
| 190 |
+
} else if (!this.dragging) {
|
| 191 |
+
if (Math.abs(this.velocity) > 0.0005) {
|
| 192 |
+
this.turn += this.velocity * delta;
|
| 193 |
+
this.velocity *= Math.exp(-3.2 * delta);
|
| 194 |
+
} else this.velocity = 0;
|
| 195 |
+
}
|
| 196 |
+
if (this.turn !== before) this.direction = this.turn > before ? 1 : -1;
|
| 197 |
+
void this.draw();
|
| 198 |
+
if (this.dragging || this.velocity || this.angleAnimation) this.raf = requestAnimationFrame(this.tick);
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
+
private async draw(force = false): Promise<void> {
|
| 202 |
+
const index = frameIndex(this.turn, this.store.count);
|
| 203 |
+
this.wantIndex = index;
|
| 204 |
+
this.store.prefetch(index, this.direction, this.prefetchFrames);
|
| 205 |
+
if (force || index !== this.lastIndex) {
|
| 206 |
+
const bitmap = this.store.peek(index) ?? await this.store.get(index);
|
| 207 |
+
if (bitmap && this.wantIndex === index && !this.disposed) {
|
| 208 |
+
this.context.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
| 209 |
+
this.lastIndex = index;
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
this.onChange({ frame: index, angle: normalizedDegrees(this.turn), turn: this.turn, cacheSize: this.store.cacheSize });
|
| 213 |
+
}
|
| 214 |
+
}
|
src/core/index.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type { OrbitPrepareOptions, OrbitSource, PreparedOrbitSource } from './FrameSource';
|
| 2 |
+
export { FRAME_COUNT, frameIndex, pixelsPerTurn, normalizedDegrees } from './orbitMath';
|